public inbox for [email protected]  
help / color / mirror / Atom feed
Re: Custom oauth validator options
7+ messages / 2 participants
[nested] [flat]

* Re: Custom oauth validator options
@ 2026-01-19 20:30  Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Zsolt Parragi @ 2026-01-19 20:30 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

> I agree it could be, but is it any more confusing than if you were to
> set work_mem in postgresql.conf today, and then `ALTER ROLE ALL SET
> work_mem` to something completely different?

I would say yes, because in the ALTER ROLE case, it's clear that a
role specific setting is more specific. But I also understand this
reasoning, I'll update the patch to follow this approach.

> Right. This goes back to your question upthread as to why I brought
> session_preload_libraries into all this -- I thought
> session_preload_libraries already had handling for this, but it
> doesn't.

I looked into the previous idea I mentioned, about using child
processes for the purpose, and got that working.

* to prevent pg_hba reloads if something is invalid in it
* possibly to print out a warning (or error/fatal) during postmaster
startup along/instead of the connection warning
* possibly to do the same during sighup

The "instead of connection warning" (removing the placeholders from
postmaster) part is a bit complex or limited, as the postmaster can't
use dsm, and there can be any number of variables.

This is again a bit of a different topic, but I could make that a
proper patch from this prototype.

The important part for this thread is that if you would prefer a
version which completely verifies the pg_hba configuration before
accepting it, it's not that difficult to implement, or at least it's
not as complex as I originally imagined it. But even that won't
guarantee that the configuration always remains valid, because session
preload libraries can change without a server restart/reload... but
that's a rare corner case, and it could be a useful check most of the
time.






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-20 18:02  Jacob Champion <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Jacob Champion @ 2026-01-20 18:02 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

On Mon, Jan 19, 2026 at 12:30 PM Zsolt Parragi
<[email protected]> wrote:
> This is again a bit of a different topic, but I could make that a
> proper patch from this prototype.

Let's separate the "verify session-preloaded GUCs" question from this
feature request, yeah.

> The important part for this thread is that if you would prefer a
> version which completely verifies the pg_hba configuration before
> accepting it, it's not that difficult to implement, or at least it's
> not as complex as I originally imagined it. But even that won't
> guarantee that the configuration always remains valid, because session
> preload libraries can change without a server restart/reload... but
> that's a rare corner case, and it could be a useful check most of the
> time.

Right. I've been thinking about strategy here, and I'm not sure I've
solidified my thoughts yet, but I don't want to make you wait for
that. So here goes:

The inability to verify the HBA settings, without actively loading the
extension, is a drawback whether we introduce a PGC_HBA or not. I feel
pretty strongly that we can't require shared_preload_libraries for
this use case. And given the choice between "you cannot modify per-HBA
settings at all" and "we can't tell you until you test them whether
they're valid or not", most people would probably prefer the latter
limitation. Especially since the *values* for many existing HBA
parameters cannot truly be "validated" without testing anyway;
consider ldapserver etc.

Since session_preload_libraries already can't do this, I don't feel
too bad about us not doing it for a first version of the feature, but
this limitation is likely to remain for a long time. Unless you think
that there's a technical solution where the benefit easily outweighs
the maintenance cost. And my guess is that this conversation is about
to collide at high speed with the Postgres-threading work that's in
progress.

I like the idea of a PGC_HBA. I think it makes a lot of sense to be
able set other GUC overrides here -- authentication_timeout,
log_connections, pre/post_auth_delay. It seems architectually sound
and generically useful.

I'm worried that it's about to make a different decision from the
decision that is being made for the pg_hosts.conf file for SNI. So
this is not going to feel cohesive at first, and that's only "okay" if
it becomes cohesive very quickly, which requires a larger audience.

I'm also worried about namespace collisions between GUC and HBA. If we
scope it to OAuth then that becomes easier (e.g. just prepend
`validator.` or something to the setting name in HBA and then it's
obvious what's going on). But if someone decides in PG20 that
pam_use_hostname is a good GUC name for something, we're in trouble,
because the existing HBA options do not plug into the GUC system.

That's a lot of risk. High revert potential without multiple
maintainers saying "yes", IMO, and if that happens we will have no
improvement here for PG19.

So,
1) how close to the sun do you feel like flying today?
2) do you agree with the above?
3) can your option (b) or (c) make enough use of existing GUC
infrastructure, so that a future PGC_HBA could easily subsume an
OAuth-specific solution, if people want to continue down that path in
a less OAuth-centric thread?

--Jacob






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-20 20:31  Zsolt Parragi <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Zsolt Parragi @ 2026-01-20 20:31 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

> I'm not sure I've solidified my thoughts yet

I can wait if you would prefer more time to think about the problem, I
can work on other things in the meantime and we still have several
months before the code freeze. I mainly wanted to share my findings
with this experiment, and that was also a fun side project to try.

> 2) do you agree with the above?

Yes. I'm fine with not verifying everything perfectly (or as close as
perfectly as we can). I like my currently submitted version better
than the child process verification version, but I wanted to see if it
is doable or not, and to see what challenges there are.

> But if someone decides in PG20 that
> pam_use_hostname is a good GUC name for something, we're in trouble,
> because the existing HBA options do not plug into the GUC system.

We could make them reserved names? Or maybe even accessible as GUC
variables, even if we leave the current parsing/validation logic as
is. Making them proper GUC variables seemed like a clear follow up
patch to me, even if not for pg19.

> I'm worried that it's about to make a different decision from the
> decision that is being made for the pg_hosts.conf file for SNI.

I probably should read that thread in more detail, but I assume that
your worry is about pg_hosts being a hardcoded configuration instead
of using a similarly customizable GUC context? Shouldn't that be
fixable in the future similarly?

> 3) can your option (b) or (c) make enough use of existing GUC
> infrastructure, so that a future PGC_HBA could easily subsume an
> OAuth-specific solution, if people want to continue down that path in
> a less OAuth-centric thread?

I'm not sure about reusing existing GUC infrastructure, but  I could
make it look similar from the users perspective for example by adding
a function DefineCustomValidatorStringVariable that has a similar
interface to DefineCustomStringVariable, and in the future, this
function could simply forward to DefineCustomStringVariable.

That would limit the variable to be only definable in pg_hba, not
postgresql.conf, but otherwise should work similarly for
validators/users.

I think this would be a larger patch than the real PGC_GUC, but it
would be limited to the pg_hba parser.

> 1) how close to the sun do you feel like flying today?

Now that I have tried the PGC_HBA approach, I like how that works and
integrates with everything, this is a much better solution than my
original ideas.

On the other hand, it would be great to get something working in PG19.
By that time more libraries/tools should actually start to support
oidc, and we will see more use of the feature, and the way we
configure these parameters is important for the validators.






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-24 00:04  Jacob Champion <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Jacob Champion @ 2026-01-24 00:04 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

On Tue, Jan 20, 2026 at 12:31 PM Zsolt Parragi
<[email protected]> wrote:
> > But if someone decides in PG20 that
> > pam_use_hostname is a good GUC name for something, we're in trouble,
> > because the existing HBA options do not plug into the GUC system.
>
> We could make them reserved names?

I'm wondering if we should maybe do the opposite, and namespace the
GUCs instead? The vast majority of settings in an HBA are not going to
be GUCs, they're going to be method-specific parameters. So maybe it's
okay to have to do more typing to do the uncommon thing, and reference
them like `guc.log_connections` or something.

> Or maybe even accessible as GUC
> variables, even if we leave the current parsing/validation logic as
> is. Making them proper GUC variables seemed like a clear follow up
> patch to me, even if not for pg19.

Hmm... we may want to discuss my (e) option derailment more seriously,
if we're planning to go in that direction (and if other people like
that direction).

> > I'm worried that it's about to make a different decision from the
> > decision that is being made for the pg_hosts.conf file for SNI.
>
> I probably should read that thread in more detail, but I assume that
> your worry is about pg_hosts being a hardcoded configuration instead
> of using a similarly customizable GUC context? Shouldn't that be
> fixable in the future similarly?

"Fixable" in what sense? pg_hosts.conf is currently similar to
pg_ident.conf in that it has no place for key=value pairs, and if you
add them after as an optional "column" for compatibility, you still
have to write something for all of those columns that you were trying
to replace with the GUC settings.

> > 3) can your option (b) or (c) make enough use of existing GUC
> > infrastructure, so that a future PGC_HBA could easily subsume an
> > OAuth-specific solution, if people want to continue down that path in
> > a less OAuth-centric thread?
>
> I'm not sure about reusing existing GUC infrastructure, but  I could
> make it look similar from the users perspective for example by adding
> a function DefineCustomValidatorStringVariable that has a similar
> interface to DefineCustomStringVariable, and in the future, this
> function could simply forward to DefineCustomStringVariable.

Might work, yeah.

--Jacob






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-26 09:51  Zsolt Parragi <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Zsolt Parragi @ 2026-01-26 09:51 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

> Hmm... we may want to discuss my (e) option derailment more seriously,
> if we're planning to go in that direction (and if other people like
> that direction).

I know you wrote that you are only half serious about it, and I
definitely do not want to go in the "lets completely refactor pg_hba
in this patch" direction, but keeping that idea in mind seems like a
good idea to me. The choosing authentication method part would already
be useful with OAuth, and now Joel also started a thread about fido2,
which also brings the question of MFA. Pluggable generic
authentication would also require generic GUC variables at this level.

Scoping validators to a specific prefix fixes the collision issue, but
it also goes in a different direction. Because of this I like the
other alternative idea (DefineCustomValidatorStringVariable) better,
if we want to go with a smaller change for this, but I still have to
implement that and see how it behaves in practice.

> "Fixable" in what sense? pg_hosts.conf is currently similar to
> pg_ident.conf in that it has no place for key=value pairs, and if you
> add them after as an optional "column" for compatibility, you still
> have to write something for all of those columns that you were trying
> to replace with the GUC settings.


pg_hba has the same issue, even if it has custom key=value data
already. What I meant is similarly how we could turn currently hard
coded pg_hba settings into GUC variables, the same is doable with
pg_hosts, either at a separate level or integrating it into the HBA
context. And later either both should get a new line style and
deprecate the old one, or maybe these settings should be configured
completely differently.






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-27 17:40  Jacob Champion <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 7+ messages in thread

From: Jacob Champion @ 2026-01-27 17:40 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

On Mon, Jan 26, 2026 at 1:51 AM Zsolt Parragi <[email protected]> wrote:
> The choosing authentication method part would already
> be useful with OAuth, and now Joel also started a thread about fido2,
> which also brings the question of MFA.

Or just the ability to offer a choice between two authentication
methods for a single user, yeah.

> pg_hba has the same issue, even if it has custom key=value data
> already. What I meant is similarly how we could turn currently hard
> coded pg_hba settings into GUC variables, the same is doable with
> pg_hosts, either at a separate level or integrating it into the HBA
> context. And later either both should get a new line style and
> deprecate the old one, or maybe these settings should be configured
> completely differently.

Sure; at this point I think we're violently agreeing. If we suspect
the configuration UX needs to be refactored, that's not going to be a
decision made unilaterally in this thread, which is why I said I was
worried about the scope creep.

--Jacob






^ permalink  raw  reply  [nested|flat] 7+ messages in thread

* Re: Custom oauth validator options
@ 2026-01-28 16:04  Zsolt Parragi <[email protected]>
  parent: Jacob Champion <[email protected]>
  0 siblings, 0 replies; 7+ messages in thread

From: Zsolt Parragi @ 2026-01-28 16:04 UTC (permalink / raw)
  To: Jacob Champion <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>; [email protected]; Robert Haas <[email protected]>; [email protected]

I implemented a DefineCustomValidatorStringVariable PoC - I don't like
it that much. It adds too much boilerplate code for a very specific
feature. If you say we should go with a more limited approach, I think
my earlier simple version is better, because it is simple. I'll also
try to think about other approaches.

And also let me go back to my concern that

> Scoping validators to a specific prefix fixes the collision issue, but
> it also goes in a different direction.

I wrote this because of the simple "guc.some_name" example, as the
fixed guc prefix - and previously I also looked into
MarkGUCPrefixReserved, and I realized that there's no easy way to use
that for enforcing prefixes for a library.

And then I realized that maybe that needs an improvement, the behavior
of MarkGUCPrefixReserved and DefineCustom*Variable seems like a legacy
thing and not something that was intentionally designed that way.

What do you think about the following patches?

0001: defines a new guc, guc_prefix_enforcement that potentially
changes the behavior of prefix reservation. It has a few modes, based
on which missing prefix reservations or variables defined outside the
reserved prefix result in warnings or errors during library load time.
This is unrelated to pgc_hba, and applies to all custom variables.

0002: the same patch as before, with your comment (su_backend,
backend, suset, user can be set in pg_hba) addressed, and also always
enforces proper prefix reservation for pg_hba variables using 0001.

* We don't have to worry about collisions, because prefixes are always
enforced in pg_hba, so people can't "redefine" the fixed key/value
pairs or columns
* It also introduces the idea of enforcing guc prefixes for
extensions. In theory this setting should start with a relaxed default
(I would say warning mode), and changed to strict in a later major
version, enforcing proper guc rules by default. That way, third party
extensions won't be able to define gucs like pam_use_hostname.

I realize that

1. This is also scope creep
2. 0001 probably should be a separate thread/discussion

But I first wanted to ask your opinion about the idea / what do you
think about the interaction of the two patches.


Attachments:

  [application/octet-stream] 0001-Guc-prefix-enforcement.patch (33.9K, 2-0001-Guc-prefix-enforcement.patch)
  download | inline diff:
From 223cb59b2558edcef39e88482c0e0b4f093ba75b Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Wed, 28 Jan 2026 15:38:25 +0100
Subject: [PATCH 1/2] Guc prefix enforcement

This patch introduces a new guc variable, guc_prefix_enforcement.
This variable aims to enforce proper naming/structuring of guc
variables, by checking the following conditions:

* libraries should reserve at least one prefix if they define guc
  variables
* if a library defined one or more prefixes, all variables should be
  defined within those prefixes
* libraries shouldn't define variables in prefixes reserved by other
  libraries (even if they don't reserve a prefix)

It has
4 possible values:

* off, which is the existing earlier behavior, it does nothing
* warning, in which violation of any of the above conditions results in
  an appropriate warning message
* prefix, in which case the first condition is still only a warning, but
  the second and third result in an error
* strict, in which case any violation results in an error

The current patch sets the default of this value to off (or maybe it
should be warning?), and later major versions can increase it to strict,
after we are sure that most extensions follow these rules properly.
---
 src/backend/utils/fmgr/dfmgr.c                |  38 ++++
 src/backend/utils/init/miscinit.c             |   4 +
 src/backend/utils/misc/guc.c                  | 167 +++++++++++++++++-
 src/backend/utils/misc/guc_parameters.dat     |   9 +
 src/backend/utils/misc/guc_tables.c           |  13 ++
 src/include/fmgr.h                            |   1 +
 src/include/utils/guc.h                       |  17 ++
 src/include/utils/guc_tables.h                |   2 +
 src/test/modules/Makefile                     |   3 +
 src/test/modules/meson.build                  |   1 +
 .../test_guc_prefix_enforcement/Makefile      |  17 ++
 .../test_guc_prefix_enforcement/meson.build   |  80 +++++++++
 .../t/001_prefix_enforcement.pl               | 161 +++++++++++++++++
 .../test_guc_no_prefix.c                      |  45 +++++
 .../test_guc_no_reserve.c                     |  42 +++++
 .../test_guc_prefix_enforcement.c             |  44 +++++
 .../test_guc_wrong_prefix.c                   |  44 +++++
 17 files changed, 682 insertions(+), 6 deletions(-)
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/Makefile
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/meson.build
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c

diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index e636cc81cf8..44408f2f697 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -59,6 +59,12 @@ struct DynamicFileList
 static DynamicFileList *file_list = NULL;
 static DynamicFileList *file_tail = NULL;
 
+/*
+ * Track the library currently being loaded (during _PG_init execution).
+ * This allows GUC code to know which library is defining custom variables.
+ */
+static const char *current_loading_library_name = NULL;
+
 /* stat() call under Win32 returns an st_ino field, but it has no meaning */
 #ifndef WIN32
 #define SAME_INODE(A,B) ((A).st_ino == (B).inode && (A).st_dev == (B).device)
@@ -293,11 +299,33 @@ internal_load_library(const char *libname)
 
 		/*
 		 * If the library has a _PG_init() function, call it.
+		 *
+		 * Set current_loading_library_name so that GUC code can track which
+		 * library is defining custom variables. Use the module name from the
+		 * magic block if available, otherwise extract from the filename.
 		 */
 		PG_init = (PG_init_t) dlsym(file_scanner->handle, "_PG_init");
 		if (PG_init)
+		{
+			if (file_scanner->magic->name != NULL)
+				current_loading_library_name = file_scanner->magic->name;
+			else
+			{
+				/* Extract module name from library path */
+				const char *basename = strrchr(libname, '/');
+
+				if (basename)
+					basename++;
+				else
+					basename = libname;
+				current_loading_library_name = basename;
+			}
+
 			(*PG_init) ();
 
+			current_loading_library_name = NULL;
+		}
+
 		/* OK to link it into list */
 		if (file_list == NULL)
 			file_list = file_scanner;
@@ -746,3 +774,13 @@ RestoreLibraryState(char *start_address)
 		start_address += strlen(start_address) + 1;
 	}
 }
+
+/*
+ * Return the name of the library currently being loaded (during _PG_init),
+ * or NULL if no library is currently being loaded.
+ */
+const char *
+get_current_loading_library_name(void)
+{
+	return current_loading_library_name;
+}
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..aaffe943b2b 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1856,6 +1856,8 @@ process_shared_preload_libraries(void)
 				   false);
 	process_shared_preload_libraries_in_progress = false;
 	process_shared_preload_libraries_done = true;
+
+	check_guc_prefix_reservations();
 }
 
 /*
@@ -1870,6 +1872,8 @@ process_session_preload_libraries(void)
 	load_libraries(local_preload_libraries_string,
 				   "local_preload_libraries",
 				   true);
+
+	check_guc_prefix_reservations();
 }
 
 /*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ae9d5f3fb70..1bd573a7e2a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "access/xlog.h"
 #include "catalog/objectaccess.h"
+#include "fmgr.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/pg_type.h"
@@ -76,6 +77,12 @@
 
 static int	GUC_check_errcode_value;
 
+typedef struct ReservedGUCPrefix
+{
+	char	   *prefix;			/* the reserved prefix (e.g., "myext") */
+	char	   *library_name;	/* library that reserved this prefix, or NULL */
+} ReservedGUCPrefix;
+
 static List *reserved_class_prefix = NIL;
 
 /* global variables for check hook support */
@@ -259,6 +266,8 @@ static void replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **
 static bool valid_custom_variable_name(const char *name);
 static bool assignable_custom_variable_name(const char *name, bool skip_errors,
 											int elevel);
+static ReservedGUCPrefix *find_reserved_prefix_for_variable(const char *varname);
+static bool library_has_reserved_prefix(const char *library_name);
 static void do_serialize(char **destptr, Size *maxbytes,
 						 const char *fmt,...) pg_attribute_printf(3, 4);
 static bool call_bool_check_hook(const struct config_generic *conf, bool *newval,
@@ -1022,10 +1031,10 @@ assignable_custom_variable_name(const char *name, bool skip_errors, int elevel)
 		/* ... and it must not match any previously-reserved prefix */
 		foreach(lc, reserved_class_prefix)
 		{
-			const char *rcprefix = lfirst(lc);
+			ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
 
-			if (strlen(rcprefix) == classLen &&
-				strncmp(name, rcprefix, classLen) == 0)
+			if (strlen(reservation->prefix) == classLen &&
+				strncmp(name, reservation->prefix, classLen) == 0)
 			{
 				if (!skip_errors)
 					ereport(elevel,
@@ -1033,7 +1042,7 @@ assignable_custom_variable_name(const char *name, bool skip_errors, int elevel)
 							 errmsg("invalid configuration parameter name \"%s\"",
 									name),
 							 errdetail("\"%s\" is a reserved prefix.",
-									   rcprefix)));
+									   reservation->prefix)));
 				return false;
 			}
 		}
@@ -4788,6 +4797,19 @@ init_custom_variable(const char *name,
 	gen->flags = flags;
 	gen->vartype = type;
 
+	/*
+	 * Record which library defined this variable, for GUC prefix enforcement.
+	 * This will be NULL for variables defined outside of _PG_init context.
+	 */
+	{
+		const char *library_name = get_current_loading_library_name();
+
+		if (library_name)
+			gen->library_name = guc_strdup(FATAL, library_name);
+		else
+			gen->library_name = NULL;
+	}
+
 	return gen;
 }
 
@@ -5143,6 +5165,8 @@ DefineCustomEnumVariable(const char *name,
  * and then prevents new ones from being created.
  * Extensions should call this after they've defined all of their custom
  * GUCs, to help catch misspelled config-file entries.
+ *
+ * Also records the library that reserved this prefix for enforcement purposes.
  */
 void
 MarkGUCPrefixReserved(const char *className)
@@ -5151,6 +5175,8 @@ MarkGUCPrefixReserved(const char *className)
 	HASH_SEQ_STATUS status;
 	GUCHashEntry *hentry;
 	MemoryContext oldcontext;
+	ReservedGUCPrefix *reservation;
+	const char *library_name = get_current_loading_library_name();
 
 	/*
 	 * Check for existing placeholders.  We must actually remove invalid
@@ -5183,12 +5209,141 @@ MarkGUCPrefixReserved(const char *className)
 		}
 	}
 
-	/* And remember the name so we can prevent future mistakes. */
+	/*
+	 * Remember the prefix and its associated library so we can prevent
+	 * future mistakes and enforce prefix ownership.
+	 */
 	oldcontext = MemoryContextSwitchTo(GUCMemoryContext);
-	reserved_class_prefix = lappend(reserved_class_prefix, pstrdup(className));
+	reservation = (ReservedGUCPrefix *) palloc(sizeof(ReservedGUCPrefix));
+	reservation->prefix = pstrdup(className);
+	if (library_name)
+		reservation->library_name = pstrdup(library_name);
+	else
+		reservation->library_name = NULL;
+	reserved_class_prefix = lappend(reserved_class_prefix, reservation);
 	MemoryContextSwitchTo(oldcontext);
 }
 
+static ReservedGUCPrefix *
+find_reserved_prefix_for_variable(const char *varname)
+{
+	ListCell   *lc;
+	const char *sep;
+	int			classLen;
+
+	/* Find the class (prefix) portion of the variable name */
+	sep = strchr(varname, GUC_QUALIFIER_SEPARATOR);
+	if (sep == NULL)
+		return NULL;
+
+	classLen = sep - varname;
+
+	foreach(lc, reserved_class_prefix)
+	{
+		ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
+
+		if (strlen(reservation->prefix) == classLen &&
+			strncmp(varname, reservation->prefix, classLen) == 0)
+			return reservation;
+	}
+
+	return NULL;
+}
+
+static bool
+library_has_reserved_prefix(const char *library_name)
+{
+	ListCell   *lc;
+
+	if (library_name == NULL)
+		return false;
+
+	foreach(lc, reserved_class_prefix)
+	{
+		ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
+
+		if (reservation->library_name != NULL &&
+			strcmp(reservation->library_name, library_name) == 0)
+			return true;
+	}
+
+	return false;
+}
+
+/*
+ * Check GUC prefix reservations after library loading.
+ *
+ * This function validates that:
+ * - In "prefix" mode: all custom GUCs are defined under a reserved prefix
+ * - In "strict" mode: all libraries that define custom GUCs have called
+ *   MarkGUCPrefixReserved()
+ */
+void
+check_guc_prefix_reservations(void)
+{
+	HASH_SEQ_STATUS status;
+	GUCHashEntry *hentry;
+
+	if (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_OFF)
+		return;
+
+	hash_seq_init(&status, guc_hashtab);
+	while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
+	{
+		struct config_generic *gconf = hentry->gucvar;
+		ReservedGUCPrefix *reservation;
+		int			strict_elevel;
+		int			prefix_elevel;
+		bool		has_reserved_prefix;
+
+		/* Skip placeholders and core variables */
+		if (gconf->flags & GUC_CUSTOM_PLACEHOLDER)
+			continue;
+		if (gconf->library_name == NULL)
+			continue;
+
+		strict_elevel = (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_STRICT)
+			? FATAL : WARNING;
+		prefix_elevel = (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_WARN)
+			? WARNING : FATAL;
+
+		has_reserved_prefix = library_has_reserved_prefix(gconf->library_name);
+		if (!has_reserved_prefix)
+		{
+			ereport(strict_elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extension \"%s\" defines GUC variables without calling MarkGUCPrefixReserved()",
+							gconf->library_name),
+					 errdetail("Variable \"%s\" was defined without prefix reservation.",
+							   gconf->name),
+					 errhint("Extensions should call MarkGUCPrefixReserved() after defining their GUC variables.")));
+		}
+
+		reservation = find_reserved_prefix_for_variable(gconf->name);
+
+		if (reservation == NULL)
+		{
+			if (has_reserved_prefix)
+			{
+				ereport(prefix_elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("extension \"%s\" defines GUC variable \"%s\" outside any reserved prefix",
+								gconf->library_name, gconf->name),
+						 errhint("Extensions should call MarkGUCPrefixReserved() to reserve a prefix for their variables.")));
+			}
+		}
+		else if (reservation->library_name != NULL &&
+				 strcmp(reservation->library_name, gconf->library_name) != 0)
+		{
+			/* Variable is under a prefix reserved by a different library */
+			ereport(prefix_elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extension \"%s\" defines GUC variable \"%s\" under prefix reserved by \"%s\"",
+							gconf->library_name, gconf->name, reservation->library_name)));
+		}
+	}
+}
+
 
 /*
  * Return an array of modified GUC options to show in EXPLAIN.
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..b1ea6121206 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,15 @@
   boot_val => 'false',
 },
 
+{ name => 'guc_prefix_enforcement', type => 'enum', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS',
+  short_desc => 'Enforcement mode for GUC prefix reservations.',
+  long_desc => 'Controls whether violations of GUC prefix reservations generate warnings or errors.',
+  flags => 'GUC_NOT_IN_SAMPLE',
+  variable => 'guc_prefix_enforcement',
+  boot_val => 'GUC_PREFIX_ENFORCEMENT_OFF',
+  options => 'guc_prefix_enforcement_options',
+},
+
 { name => 'hash_mem_multiplier', type => 'real', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Multiple of "work_mem" to use for hash tables.',
   flags => 'GUC_EXPLAIN',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 73ff6ad0a32..0c492fd4fc9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,17 @@ static const struct config_enum_entry file_copy_method_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry guc_prefix_enforcement_options[] = {
+	{"off", GUC_PREFIX_ENFORCEMENT_OFF, false},
+	{"warn", GUC_PREFIX_ENFORCEMENT_WARN, false},
+	{"prefix", GUC_PREFIX_ENFORCEMENT_PREFIX, false},
+	{"strict", GUC_PREFIX_ENFORCEMENT_STRICT, false},
+	{NULL, 0, false}
+};
+
+StaticAssertDecl(lengthof(guc_prefix_enforcement_options) == (GUC_PREFIX_ENFORCEMENT_STRICT + 2),
+				 "array length mismatch");
+
 /*
  * Options for enum values stored in other modules
  */
@@ -581,6 +592,8 @@ int			huge_pages = HUGE_PAGES_TRY;
 int			huge_page_size;
 int			huge_pages_status = HUGE_PAGES_UNKNOWN;
 
+int			guc_prefix_enforcement = GUC_PREFIX_ENFORCEMENT_OFF;
+
 /*
  * These variables are all dummies that don't do anything, except in some
  * cases provide the value for SHOW to display.  The real state is elsewhere
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 22dd6526169..5a4dffc6e30 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -797,6 +797,7 @@ extern void get_loaded_module_details(DynamicFileList *dfptr,
 									  const char **module_name,
 									  const char **module_version);
 extern void **find_rendezvous_variable(const char *varName);
+extern const char *get_current_loading_library_name(void);
 extern Size EstimateLibraryStateSpace(void);
 extern void SerializeLibraryState(Size maxsize, char *start_address);
 extern void RestoreLibraryState(char *start_address);
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index bf39878c43e..ee02b9aa987 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -126,6 +126,20 @@ typedef enum
 	PGC_S_SESSION,				/* SET command */
 } GucSource;
 
+/*
+ * Enforcement modes for GUC prefix reservations.
+ *
+ * This controls how strictly we enforce that extensions call
+ * MarkGUCPrefixReserved() and only define GUCs under their reserved prefix.
+ */
+typedef enum
+{
+	GUC_PREFIX_ENFORCEMENT_OFF,		/* no enforcement (earlier behavior) */
+	GUC_PREFIX_ENFORCEMENT_WARN,	/* emit warnings on violations */
+	GUC_PREFIX_ENFORCEMENT_PREFIX,	/* ERROR if GUC defined outside reserved prefix */
+	GUC_PREFIX_ENFORCEMENT_STRICT,	/* ERROR if extension doesn't call MarkGUCPrefixReserved */
+} GucPrefixEnforcement;
+
 /*
  * Parsing the configuration file(s) will return a list of name-value pairs
  * with source location info.  We also abuse this data structure to carry
@@ -287,6 +301,8 @@ extern PGDLLIMPORT bool log_statement_stats;
 extern PGDLLIMPORT bool log_btree_build_stats;
 extern PGDLLIMPORT char *event_source;
 
+extern PGDLLIMPORT int guc_prefix_enforcement;
+
 extern PGDLLIMPORT bool check_function_bodies;
 extern PGDLLIMPORT bool current_role_is_superuser;
 
@@ -456,6 +472,7 @@ extern int	set_config_with_handle(const char *name, config_handle *handle,
 								   GucAction action, bool changeVal,
 								   int elevel, bool is_reload);
 extern config_handle *get_config_handle(const char *name);
+extern void check_guc_prefix_reservations(void);
 extern void AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt);
 extern char *GetConfigOptionByName(const char *name, const char **varname,
 								   bool missing_ok);
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 71a80161961..1bbaa09212f 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -278,6 +278,8 @@ struct config_generic
 	char	   *sourcefile;		/* file current setting is from (NULL if not
 								 * set in config file) */
 	int			sourceline;		/* line in source file */
+	const char *library_name;	/* library that defined this variable, or NULL
+								 * for core variables */
 
 	/* fields for specific variable types */
 	union
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4c6d56d97d8..bf811e2ca7e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -29,6 +29,9 @@ SUBDIRS = \
 		  test_escape \
 		  test_extensions \
 		  test_ginpostinglist \
+		  test_guc_prefix_enforcement \
+		  test_hba_guc \
+		  test_hba_guc_contexts \
 		  test_int128 \
 		  test_integerset \
 		  test_json_parser \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 1b31c5b98d6..da545219a92 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -29,6 +29,7 @@ subdir('test_dsm_registry')
 subdir('test_escape')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
+subdir('test_guc_prefix_enforcement')
 subdir('test_int128')
 subdir('test_integerset')
 subdir('test_json_parser')
diff --git a/src/test/modules/test_guc_prefix_enforcement/Makefile b/src/test/modules/test_guc_prefix_enforcement/Makefile
new file mode 100644
index 00000000000..5265c6ee37d
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/Makefile
@@ -0,0 +1,17 @@
+# src/test/modules/test_guc_prefix_enforcement/Makefile
+
+MODULES = test_guc_prefix_enforcement test_guc_no_reserve test_guc_wrong_prefix test_guc_no_prefix
+PGFILEDESC = "test_guc_prefix_enforcement - test module for GUC prefix reservation enforcement"
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_guc_prefix_enforcement
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_guc_prefix_enforcement/meson.build b/src/test/modules/test_guc_prefix_enforcement/meson.build
new file mode 100644
index 00000000000..ccf59f5330a
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/meson.build
@@ -0,0 +1,80 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Main test module (good behavior)
+test_guc_prefix_enforcement_sources = files(
+  'test_guc_prefix_enforcement.c',
+)
+
+if host_system == 'windows'
+  test_guc_prefix_enforcement_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_prefix_enforcement',
+    '--FILEDESC', 'test_guc_prefix_enforcement - test module for GUC prefix reservation enforcement',])
+endif
+
+test_guc_prefix_enforcement = shared_module('test_guc_prefix_enforcement',
+  test_guc_prefix_enforcement_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_prefix_enforcement
+
+# Test module without reservation
+test_guc_no_reserve_sources = files(
+  'test_guc_no_reserve.c',
+)
+
+if host_system == 'windows'
+  test_guc_no_reserve_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_no_reserve',
+    '--FILEDESC', 'test_guc_no_reserve - test module without prefix reservation',])
+endif
+
+test_guc_no_reserve = shared_module('test_guc_no_reserve',
+  test_guc_no_reserve_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_no_reserve
+
+# Test module with wrong prefix
+test_guc_wrong_prefix_sources = files(
+  'test_guc_wrong_prefix.c',
+)
+
+if host_system == 'windows'
+  test_guc_wrong_prefix_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_wrong_prefix',
+    '--FILEDESC', 'test_guc_wrong_prefix - test module with wrong prefix reservation',])
+endif
+
+test_guc_wrong_prefix = shared_module('test_guc_wrong_prefix',
+  test_guc_wrong_prefix_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_wrong_prefix
+
+# Test module with variable without prefix (no dot)
+test_guc_no_prefix_sources = files(
+  'test_guc_no_prefix.c',
+)
+
+if host_system == 'windows'
+  test_guc_no_prefix_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_no_prefix',
+    '--FILEDESC', 'test_guc_no_prefix - test module with variable without prefix',])
+endif
+
+test_guc_no_prefix = shared_module('test_guc_no_prefix',
+  test_guc_no_prefix_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_no_prefix
+
+tests += {
+  'name': 'test_guc_prefix_enforcement',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_prefix_enforcement.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl b/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
new file mode 100644
index 00000000000..273f9b18fa1
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
@@ -0,0 +1,161 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test GUC prefix reservation enforcement modes
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Test 1: Default mode (off) - all extensions should load without errors
+#
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+my $result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads with default enforcement (off)');
+
+$node->stop;
+
+#
+# Test 2: warn mode with good extension - should load with no warnings
+#
+$node = PostgreSQL::Test::Cluster->new('warn_good');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads with warn enforcement');
+
+# Verify no warnings were emitted
+my $log = $node->logfile;
+my $log_contents = slurp_file($log);
+unlike($log_contents, qr/WARNING.*MarkGUCPrefixReserved/,
+	 'no warning about MarkGUCPrefixReserved for good extension');
+unlike($log_contents, qr/WARNING.*reserved prefix/,
+	 'no warning about reserved prefix for good extension');
+
+$node->stop;
+
+#
+# Test 3: warn mode with no_reserve extension - should load with warning
+#
+$node = PostgreSQL::Test::Cluster->new('warn_no_reserve');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_reserve'");
+
+# Start should succeed but log warnings
+$node->start;
+$result = $node->safe_psql('postgres', 'SHOW test_guc_no_reserve.some_var');
+is($result, 'default', 'no_reserve extension loads with warn enforcement');
+
+# Check log for warning
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/WARNING.*without calling MarkGUCPrefixReserved/,
+	 'warn mode emits warning for extension without prefix reservation');
+
+$node->stop;
+
+#
+# Test 4: strict mode with no_reserve extension - should fail to start
+#
+$node = PostgreSQL::Test::Cluster->new('strict_no_reserve');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'strict'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_reserve'");
+
+# Start should fail
+my $ret = $node->start(fail_ok => 1);
+is($ret, 0, 'strict mode prevents startup with non-compliant extension');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*without calling MarkGUCPrefixReserved/,
+	 'strict mode emits error for extension without prefix reservation');
+
+#
+# Test 5: prefix mode with wrong_prefix extension - should fail to start
+#
+$node = PostgreSQL::Test::Cluster->new('prefix_wrong');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'prefix'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_wrong_prefix'");
+
+# Start should fail
+$ret = $node->start(fail_ok => 1);
+is($ret, 0, 'prefix mode prevents startup with wrong prefix extension');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*outside any reserved prefix/,
+	 'prefix mode emits error for extension defining GUC outside reserved prefix');
+
+#
+# Test 6: warn mode with no_prefix extension - should load with warning
+# (extension reserves a prefix but defines variable without any prefix)
+#
+$node = PostgreSQL::Test::Cluster->new('warn_no_prefix');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_prefix'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_no_prefix_var');
+is($result, 'default', 'no_prefix extension loads with warn enforcement');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/WARNING.*outside any reserved prefix/,
+	 'warn mode emits warning for extension defining GUC without prefix');
+
+$node->stop;
+
+#
+# Test 7: prefix mode with no_prefix extension - should fail to start
+# (extension reserves a prefix but defines variable without any prefix)
+#
+$node = PostgreSQL::Test::Cluster->new('prefix_no_prefix');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'prefix'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_prefix'");
+
+# Start should fail
+$ret = $node->start(fail_ok => 1);
+is($ret, 0, 'prefix mode prevents startup with unprefixed variable');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*outside any reserved prefix/,
+	 'prefix mode emits error for extension defining GUC without prefix');
+
+#
+# Test 8: verify good extension works in strict mode
+#
+$node = PostgreSQL::Test::Cluster->new('strict_good');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'strict'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads successfully in strict mode');
+
+# Verify no warnings or errors were emitted
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+unlike($log_contents, qr/(WARNING|FATAL).*MarkGUCPrefixReserved/,
+	 'no warning/error about MarkGUCPrefixReserved for good extension in strict mode');
+unlike($log_contents, qr/(WARNING|FATAL).*reserved prefix/,
+	 'no warning/error about reserved prefix for good extension in strict mode');
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
new file mode 100644
index 00000000000..610c9e15430
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_no_prefix.c
+ *		Test extension that defines a GUC variable without a prefix.
+ *
+ * This extension reserves a prefix but also defines a variable without
+ * a prefix, which should be flagged by prefix enforcement.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_no_prefix",
+	.version = PG_VERSION,
+);
+
+static char *test_var = NULL;
+
+void
+_PG_init(void)
+{
+	/* Define a variable without any prefix */
+	DefineCustomStringVariable("test_guc_no_prefix_var",
+							   "Test variable without prefix",
+							   NULL,
+							   &test_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_no_prefix");
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
new file mode 100644
index 00000000000..c57887ba88c
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
@@ -0,0 +1,42 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_no_reserve.c
+ *		Test module that defines GUCs without calling MarkGUCPrefixReserved
+ *
+ * This module intentionally does NOT call MarkGUCPrefixReserved() after
+ * defining its custom GUC variables.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_no_reserve",
+	.version = PG_VERSION,
+);
+
+static char *no_reserve_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_no_reserve.some_var",
+							   "A variable without prefix reservation",
+							   NULL,
+							   &no_reserve_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
new file mode 100644
index 00000000000..1ccbbb1fb8e
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_prefix_enforcement.c
+ *		Test extension that properly reserves its GUC prefix.
+ *
+ * This extension defines a GUC variable and calls MarkGUCPrefixReserved(),
+ * demonstrating correct behavior.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_prefix_enforcement",
+	.version = PG_VERSION,
+);
+
+static char *test_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_prefix_enforcement.test_var",
+							   "Test variable",
+							   NULL,
+							   &test_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_prefix_enforcement");
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
new file mode 100644
index 00000000000..4e94b367bb9
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_wrong_prefix.c
+ *		Test module that reserves one prefix but defines GUCs under another
+ *
+ * This module reserves the prefix "test_guc_wrong" but defines a variable
+ * under "test_guc_other".
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_wrong_prefix",
+	.version = PG_VERSION,
+);
+
+static char *wrong_prefix_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_other.some_var",
+							   "A variable under a different prefix than reserved",
+							   NULL,
+							   &wrong_prefix_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_wrong");
+}
-- 
2.43.0



  [application/octet-stream] 0002-Introduce-PGC_HBA-GUC-variables-settable-in-pg_hba.c.patch (45.0K, 3-0002-Introduce-PGC_HBA-GUC-variables-settable-in-pg_hba.c.patch)
  download | inline diff:
From 5fc38d2d922c3e201210a42e8546cbeb1587771d Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Wed, 7 Jan 2026 19:25:11 +0000
Subject: [PATCH 2/2] Introduce PGC_HBA: GUC variables settable in pg_hba.conf

Add a new GUC context level PGC_HBA that allows custom variables
to be set based on which pg_hba.conf line matches during
authentication. This enables authentication plugins and extensions
to receive configuration parameters that vary based on HBA matching
rules (client address, database, user, authentication method, etc.).

Motivation:

OAuth support introduced several OAuth-specific configuration
parameters to pg_hba.conf, and added support for third-party
validator plugins. These plugins often require additional
configuration, which is only possible with GUC variables. As these
plugins execute before authentication completes, they can only
depend on GUC variables defined in postgresql.conf or
postgresql.auto.conf.

This limitation creates a configuration problem: pg_hba.conf allows
administrators to define several different OAuth configurations for
a single PostgreSQL instance, but validator plugin-specific
variables can only be configured once per server.

With the changes in this commit, validator plugins can now define
their GUC variables with PGC_HBA context, allowing different
settings per HBA line.

Another use case is that extensions loaded via
shared_preload_libraries or session_preload_libraries can define
these variables, allowing administrators to configure
authentication/authorization-related settings per HBA line, for
example to help with row level security policies.

User interface:

The new parameters reuse the existing syntax of pg_hba.conf; GUC
variables can be defined the same way as existing hardcoded
parameters:

    host all all 192.168.1.0/24 oauth myext.setting=value1
    host all all 0.0.0.0/0      oauth myext.setting=value2

This also provides an easy migration path in the future to convert
existing pg_hba parameters to GUC variables.

Extension interface:

We define a new GUC context, PGC_HBA, and a new source, PGC_S_HBA.
PGC_HBA variables must be defined during shared_preload_libraries
or session_preload_libraries. This ensures all custom variables
are registered before connections are accepted.

PGC_HBA variables can be set from:
  - postgresql.conf
  - postgresql.auto.conf (ALTER SYSTEM)
  - pg_hba.conf (new)

PGC_HBA variables cannot be set via:
  - ALTER USER SET / ALTER DATABASE SET
  - SET command
  - Connection parameters (PGOPTIONS)

Additionally to PGC_HBA variables, SU_BACKEND, BACKEND, SUSET and USER
variables can also be set in pg_hba.

Error handling change:

This feature requires a behavior change in pg_hba.conf error
handling which may affect users not using the new feature.

Since we allow both shared_preload_libraries and
session_preload_libraries to define PGC_HBA variables (enabling
updates of potentially security-related libraries without a server
restart), we can no longer reject unknown parameters in pg_hba.conf
at postmaster startup.

Instead, unknown parameters are treated as placeholders (similar to
custom GUC variables) and error handling is delayed until after
session_preload_libraries completes. If any placeholder HBA
variables remain undefined at that point, the connection is aborted
with a FATAL error, even if authentication previously succeeded.
---
 src/backend/libpq/hba.c                       |  59 +++++-
 src/backend/utils/init/miscinit.c             |   7 +
 src/backend/utils/init/postinit.c             |   8 +
 src/backend/utils/misc/guc.c                  | 115 +++++++++++
 src/backend/utils/misc/guc_tables.c           |   2 +
 src/include/libpq/hba.h                       |   7 +
 src/include/miscadmin.h                       |   2 +
 src/include/utils/guc.h                       |   3 +
 src/test/modules/meson.build                  |   2 +
 src/test/modules/test_hba_guc/Makefile        |  21 ++
 src/test/modules/test_hba_guc/meson.build     |  35 ++++
 .../test_hba_guc/t/001_hba_guc_variables.pl   |  56 ++++++
 .../test_hba_guc/t/002_hba_guc_sources.pl     | 187 ++++++++++++++++++
 .../test_hba_guc/t/003_hba_guc_precedence.pl  | 105 ++++++++++
 .../test_hba_guc/test_hba_guc--1.0.sql        |  22 +++
 src/test/modules/test_hba_guc/test_hba_guc.c  |  93 +++++++++
 .../modules/test_hba_guc/test_hba_guc.conf    |   3 +
 .../modules/test_hba_guc/test_hba_guc.control |   5 +
 .../modules/test_hba_guc_contexts/Makefile    |  18 ++
 .../modules/test_hba_guc_contexts/meson.build |  28 +++
 .../t/001_context_validation.pl               |  85 ++++++++
 .../test_hba_guc_contexts.c                   |  89 +++++++++
 22 files changed, 943 insertions(+), 9 deletions(-)
 create mode 100644 src/test/modules/test_hba_guc/Makefile
 create mode 100644 src/test/modules/test_hba_guc/meson.build
 create mode 100644 src/test/modules/test_hba_guc/t/001_hba_guc_variables.pl
 create mode 100644 src/test/modules/test_hba_guc/t/002_hba_guc_sources.pl
 create mode 100644 src/test/modules/test_hba_guc/t/003_hba_guc_precedence.pl
 create mode 100644 src/test/modules/test_hba_guc/test_hba_guc--1.0.sql
 create mode 100644 src/test/modules/test_hba_guc/test_hba_guc.c
 create mode 100644 src/test/modules/test_hba_guc/test_hba_guc.conf
 create mode 100644 src/test/modules/test_hba_guc/test_hba_guc.control
 create mode 100644 src/test/modules/test_hba_guc_contexts/Makefile
 create mode 100644 src/test/modules/test_hba_guc_contexts/meson.build
 create mode 100644 src/test/modules/test_hba_guc_contexts/t/001_context_validation.pl
 create mode 100644 src/test/modules/test_hba_guc_contexts/test_hba_guc_contexts.c

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 87ee541e880..6fc4debf567 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2507,15 +2507,21 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 	}
 	else
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("unrecognized authentication option name: \"%s\"",
-						name),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, file_name)));
-		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
-							name);
-		return false;
+		/*
+		 * Unrecognized option name - treat it as a potential GUC variable.
+		 * Store the name=value pair in the HbaLine's guc_options list.
+		 * Actual validation (checking if the GUC is defined) happens at
+		 * connection time after session_preload_libraries completes.
+		 */
+		HbaOption  *opt;
+
+		opt = palloc(sizeof(HbaOption));
+		opt->name = pstrdup(name);
+		opt->value = pstrdup(val);
+		hbaline->guc_options = lappend(hbaline->guc_options, opt);
+
+		elog(DEBUG2, "pg_hba.conf line %d: storing GUC option %s = %s",
+			 line_num, name, val);
 	}
 	return true;
 }
@@ -3113,11 +3119,44 @@ load_ident(void)
 
 
 
+/*
+ * apply_hba_guc_options
+ *		Apply GUC variable settings from the matched HBA line.
+ *
+ * This function processes the guc_options list from the matched pg_hba.conf
+ * line and applies each GUC setting. Variables will either be set (if already
+ * defined) or create placeholders (if not yet defined). Validation of
+ * undefined variables and context checking happens later, after
+ * session_preload_libraries completes in postinit.c.
+ */
+static void
+apply_hba_guc_options(Port *port)
+{
+	ListCell   *lc;
+
+	if (port->hba->guc_options == NIL)
+		return;
+
+	foreach(lc, port->hba->guc_options)
+	{
+		HbaOption  *opt = (HbaOption *) lfirst(lc);
+
+		elog(DEBUG2, "Applying HBA GUC option: %s = %s", opt->name, opt->value);
+
+		(void) set_config_option(opt->name, opt->value,
+								 PGC_HBA, PGC_S_HBA,
+								 GUC_ACTION_SET, true, ERROR, false);
+	}
+}
+
 /*
  *	Determine what authentication method should be used when accessing database
  *	"database" from frontend "raddr", user "user".  Return the method and
  *	an optional argument (stored in fields of *port), and STATUS_OK.
  *
+ *	Also applies all GUC variables from the matched HBA line, as these variables
+ *	might immediately required by authentication plugins.
+ *
  *	If the file does not contain any entry matching the request, we return
  *	method = uaImplicitReject.
  */
@@ -3125,6 +3164,8 @@ void
 hba_getauthmethod(Port *port)
 {
 	check_hba(port);
+
+	apply_hba_guc_options(port);
 }
 
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index aaffe943b2b..2a714d308ce 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1786,6 +1786,10 @@ char	   *local_preload_libraries_string = NULL;
 bool		process_shared_preload_libraries_in_progress = false;
 bool		process_shared_preload_libraries_done = false;
 
+/* Flag telling that we are loading session_preload_libraries */
+bool		process_session_preload_libraries_in_progress = false;
+bool		process_session_preload_libraries_done = false;
+
 shmem_request_hook_type shmem_request_hook = NULL;
 bool		process_shmem_requests_in_progress = false;
 
@@ -1866,9 +1870,12 @@ process_shared_preload_libraries(void)
 void
 process_session_preload_libraries(void)
 {
+	process_session_preload_libraries_in_progress = true;
 	load_libraries(session_preload_libraries_string,
 				   "session_preload_libraries",
 				   false);
+	process_session_preload_libraries_in_progress = false;
+	process_session_preload_libraries_done = true;
 	load_libraries(local_preload_libraries_string,
 				   "local_preload_libraries",
 				   true);
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d3bb88243c6 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -33,6 +33,7 @@
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "catalog/pg_tablespace.h"
+#include "lib/stringinfo.h"
 #include "libpq/auth.h"
 #include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
@@ -1225,6 +1226,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if ((flags & INIT_PG_LOAD_SESSION_LIBS) != 0)
 		process_session_preload_libraries();
 
+	/*
+	 * Now that session_preload_libraries has completed, validate that all
+	 * GUC variables set from pg_hba.conf are properly defined with the
+	 * correct context.
+	 */
+	check_hba_guc_variables();
+
 	/* fill in the remainder of this entry in the PgBackendStatus array */
 	if (!bootstrap)
 		pgstat_bestart_final();
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1bd573a7e2a..3aef1a4d498 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -3403,6 +3403,17 @@ set_config_with_handle(const char *name, config_handle *handle,
 			 * signals to individual backends only.
 			 */
 			break;
+		case PGC_HBA:
+			if (context != PGC_SIGHUP && context != PGC_POSTMASTER &&
+				source != PGC_S_HBA)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
+						 errmsg("parameter \"%s\" cannot be changed now",
+								record->name)));
+				return 0;
+			}
+			break;
 		case PGC_SU_BACKEND:
 			if (context == PGC_BACKEND)
 			{
@@ -3456,6 +3467,7 @@ set_config_with_handle(const char *name, config_handle *handle,
 			else if (context != PGC_POSTMASTER &&
 					 context != PGC_BACKEND &&
 					 context != PGC_SU_BACKEND &&
+					 context != PGC_HBA &&
 					 source != PGC_S_CLIENT)
 			{
 				ereport(elevel,
@@ -4164,6 +4176,87 @@ get_config_handle(const char *name)
 	return NULL;
 }
 
+/*
+ * check_hba_guc_variables
+ *
+ * Check if any GUC variables set from pg_hba.conf (source = PGC_S_HBA)
+ * are still placeholders (undefined) or have the wrong context.
+ *
+ * Also enforces strict GUC prefix reservation for PGC_HBA variables:
+ * extensions that define PGC_HBA variables MUST call MarkGUCPrefixReserved()
+ * and their variables MUST be under the reserved prefix. This is always
+ * enforced regardless of the guc_prefix_enforcement setting, because
+ * PGC_HBA variables can affect authentication security.
+ *
+ * For each invalid variable, we emit a FATAL error with an appropriate
+ * message explaining the problem.
+ *
+ * This should be called after session_preload_libraries completes to
+ * ensure extensions have had a chance to define their PGC_HBA variables.
+ */
+void
+check_hba_guc_variables(void)
+{
+	HASH_SEQ_STATUS status;
+	GUCHashEntry *hentry;
+
+	hash_seq_init(&status, guc_hashtab);
+	while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
+	{
+		struct config_generic *gconf = hentry->gucvar;
+
+		if (gconf->source != PGC_S_HBA)
+			continue;
+
+		if (gconf->flags & GUC_CUSTOM_PLACEHOLDER)
+		{
+			ereport(FATAL,
+					(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+					 errmsg("authentication configuration error"),
+					 errdetail("pg_hba.conf references undefined GUC variable \"%s\"",
+							   gconf->name),
+					 errhint("Ensure the extension defining this variable is loaded in session_preload_libraries or shared_preload_libraries.")));
+		}
+
+		if (gconf->context < PGC_HBA)
+		{
+			ereport(FATAL,
+					(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
+					 errmsg("parameter \"%s\" cannot be set in pg_hba.conf",
+							gconf->name),
+					 errdetail("Only variables with context PGC_HBA or below can be set from pg_hba.conf."),
+					 errhint("This variable has context \"%s\".",
+							 GucContext_Names[gconf->context])));
+		}
+
+		if (gconf->library_name != NULL)
+		{
+			ReservedGUCPrefix *reservation;
+
+			reservation = find_reserved_prefix_for_variable(gconf->name);
+
+			if (reservation == NULL)
+			{
+				ereport(FATAL,
+						(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+						 errmsg("authentication configuration error"),
+						 errdetail("Extension \"%s\" defines PGC_HBA variable \"%s\" without a reserved prefix.",
+								   gconf->library_name, gconf->name),
+						 errhint("Extensions that define PGC_HBA variables must call MarkGUCPrefixReserved().")));
+			}
+			else if (reservation->library_name != NULL &&
+					 strcmp(reservation->library_name, gconf->library_name) != 0)
+			{
+				ereport(FATAL,
+						(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+						 errmsg("authentication configuration error"),
+						 errdetail("Extension \"%s\" defines PGC_HBA variable \"%s\" under prefix reserved by \"%s\".",
+								   gconf->library_name, gconf->name, reservation->library_name)));
+			}
+		}
+	}
+}
+
 
 /*
  * Set the fields for source file and line number the setting came from.
@@ -4765,6 +4858,19 @@ init_custom_variable(const char *name,
 		!process_shared_preload_libraries_in_progress)
 		elog(FATAL, "cannot create PGC_POSTMASTER variables after startup");
 
+	/*
+	 * Only allow custom PGC_HBA variables to be created before
+	 * session_preload_libraries completes. After that point, authentication
+	 * has already occurred and check_hba_guc_variables has validated all
+	 * PGC_HBA variables, so defining new ones would bypass validation.
+	 */
+	if (context == PGC_HBA && process_session_preload_libraries_done)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("PGC_HBA variables must be defined before session_preload_libraries completes"),
+				 errdetail("Attempted to define \"%s\" after session preload", name),
+				 errhint("Move the extension defining this variable to shared_preload_libraries or session_preload_libraries")));
+
 	/*
 	 * We can't support custom GUC_LIST_QUOTE variables, because the wrong
 	 * things would happen if such a variable were set or pg_dump'd when the
@@ -6758,6 +6864,15 @@ validate_option_array_item(const char *name, const char *value,
 			 (superuser() ||
 			  pg_parameter_aclcheck(name, GetUserId(), ACL_SET) == ACLCHECK_OK))
 		 /* ok */ ;
+	else if (gconf->context == PGC_HBA)
+	{
+		if (skipIfNoPermissions)
+			return false;
+		ereport(ERROR,
+				(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
+				 errmsg("parameter \"%s\" cannot be set by ALTER USER or ALTER DATABASE", name),
+				 errhint("Use postgresql.conf, ALTER SYSTEM, or pg_hba.conf to set this parameter.")));
+	}
 	else if (skipIfNoPermissions)
 		return false;
 	/* if a permissions error should be thrown, let set_config_option do it */
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 0c492fd4fc9..58386ecc585 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -671,6 +671,7 @@ const char *const GucContext_Names[] =
 	[PGC_INTERNAL] = "internal",
 	[PGC_POSTMASTER] = "postmaster",
 	[PGC_SIGHUP] = "sighup",
+	[PGC_HBA] = "hba",
 	[PGC_SU_BACKEND] = "superuser-backend",
 	[PGC_BACKEND] = "backend",
 	[PGC_SUSET] = "superuser",
@@ -697,6 +698,7 @@ const char *const GucSource_Names[] =
 	[PGC_S_USER] = "user",
 	[PGC_S_DATABASE_USER] = "database user",
 	[PGC_S_CLIENT] = "client",
+	[PGC_S_HBA] = "pg_hba.conf",
 	[PGC_S_OVERRIDE] = "override",
 	[PGC_S_INTERACTIVE] = "interactive",
 	[PGC_S_TEST] = "test",
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7b93ba4a709..e1848ad03d0 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -92,6 +92,12 @@ typedef struct AuthToken
 	regex_t    *regex;
 } AuthToken;
 
+typedef struct HbaOption
+{
+	char	   *name;
+	char	   *value;
+} HbaOption;
+
 typedef struct HbaLine
 {
 	char	   *sourcefile;
@@ -140,6 +146,7 @@ typedef struct HbaLine
 	char	   *oauth_scope;
 	char	   *oauth_validator;
 	bool		oauth_skip_usermap;
+	List	   *guc_options;
 } HbaLine;
 
 typedef struct IdentLine
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index db559b39c4d..0a5df62f99a 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -512,6 +512,8 @@ extern void BaseInit(void);
 extern PGDLLIMPORT bool IgnoreSystemIndexes;
 extern PGDLLIMPORT bool process_shared_preload_libraries_in_progress;
 extern PGDLLIMPORT bool process_shared_preload_libraries_done;
+extern PGDLLIMPORT bool process_session_preload_libraries_in_progress;
+extern PGDLLIMPORT bool process_session_preload_libraries_done;
 extern PGDLLIMPORT bool process_shmem_requests_in_progress;
 extern PGDLLIMPORT char *session_preload_libraries_string;
 extern PGDLLIMPORT char *shared_preload_libraries_string;
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ee02b9aa987..26bda29444b 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -73,6 +73,7 @@ typedef enum
 	PGC_INTERNAL,
 	PGC_POSTMASTER,
 	PGC_SIGHUP,
+	PGC_HBA,
 	PGC_SU_BACKEND,
 	PGC_BACKEND,
 	PGC_SUSET,
@@ -115,6 +116,7 @@ typedef enum
 	PGC_S_ENV_VAR,				/* postmaster environment variable */
 	PGC_S_FILE,					/* postgresql.conf */
 	PGC_S_ARGV,					/* postmaster command line */
+	PGC_S_HBA,					/* from pg_hba.conf */
 	PGC_S_GLOBAL,				/* global in-database setting */
 	PGC_S_DATABASE,				/* per-database setting */
 	PGC_S_USER,					/* per-user setting */
@@ -473,6 +475,7 @@ extern int	set_config_with_handle(const char *name, config_handle *handle,
 								   int elevel, bool is_reload);
 extern config_handle *get_config_handle(const char *name);
 extern void check_guc_prefix_reservations(void);
+extern void check_hba_guc_variables(void);
 extern void AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt);
 extern char *GetConfigOptionByName(const char *name, const char **varname,
 								   bool missing_ok);
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index da545219a92..f0e51bb4a5d 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -30,6 +30,8 @@ subdir('test_escape')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
 subdir('test_guc_prefix_enforcement')
+subdir('test_hba_guc')
+subdir('test_hba_guc_contexts')
 subdir('test_int128')
 subdir('test_integerset')
 subdir('test_json_parser')
diff --git a/src/test/modules/test_hba_guc/Makefile b/src/test/modules/test_hba_guc/Makefile
new file mode 100644
index 00000000000..2b89088e338
--- /dev/null
+++ b/src/test/modules/test_hba_guc/Makefile
@@ -0,0 +1,21 @@
+# src/test/modules/test_hba_guc/Makefile
+
+MODULE_big = test_hba_guc
+OBJS = \
+	$(WIN32RES) \
+	test_hba_guc.o
+PGFILEDESC = "test_hba_guc - test module for PGC_HBA GUC variables"
+
+EXTENSION = test_hba_guc
+DATA = test_hba_guc--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_hba_guc
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_hba_guc/meson.build b/src/test/modules/test_hba_guc/meson.build
new file mode 100644
index 00000000000..d8bf0233056
--- /dev/null
+++ b/src/test/modules/test_hba_guc/meson.build
@@ -0,0 +1,35 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_hba_guc_sources = files(
+  'test_hba_guc.c',
+)
+
+if host_system == 'windows'
+  test_hba_guc_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_hba_guc',
+    '--FILEDESC', 'test_hba_guc - test module for PGC_HBA GUC variables',])
+endif
+
+test_hba_guc = shared_module('test_hba_guc',
+  test_hba_guc_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_hba_guc
+
+test_install_data += files(
+  'test_hba_guc.control',
+  'test_hba_guc--1.0.sql',
+)
+
+tests += {
+  'name': 'test_hba_guc',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_hba_guc_variables.pl',
+      't/002_hba_guc_sources.pl',
+      't/003_hba_guc_precedence.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_hba_guc/t/001_hba_guc_variables.pl b/src/test/modules/test_hba_guc/t/001_hba_guc_variables.pl
new file mode 100644
index 00000000000..625f3be1e51
--- /dev/null
+++ b/src/test/modules/test_hba_guc/t/001_hba_guc_variables.pl
@@ -0,0 +1,56 @@
+# Test PGC_HBA GUC variables in pg_hba.conf
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+$node->append_conf('postgresql.conf',
+	"session_preload_libraries = 'test_hba_guc'");
+
+my $hba_conf = $node->data_dir . '/pg_hba.conf';
+open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+print $hba_fh "# Test HBA configuration with GUC variables\n";
+print $hba_fh "local all all trust test_hba_guc.string_var=from_hba test_hba_guc.int_var=999\n";
+close $hba_fh;
+
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_hba_guc;');
+my $result = $node->safe_psql('postgres',
+	"SELECT current_setting('test_hba_guc.string_var'), current_setting('test_hba_guc.int_var');"
+);
+is($result, 'from_hba|999',
+	'GUC variables set from pg_hba.conf are accessible');
+
+$result = $node->safe_psql('postgres',
+	"SELECT source FROM pg_settings WHERE name = 'test_hba_guc.string_var';"
+);
+is($result, 'pg_hba.conf',
+	'GUC variable source shows pg_hba.conf');
+
+my $node_no_ext = PostgreSQL::Test::Cluster->new('no_extension');
+$node_no_ext->init;
+
+my $hba_conf_no_ext = $node_no_ext->data_dir . '/pg_hba.conf';
+open my $hba_no_ext_fh, '>', $hba_conf_no_ext or die "Could not open $hba_conf_no_ext: $!";
+print $hba_no_ext_fh "# Test HBA configuration with undefined GUC variables\n";
+print $hba_no_ext_fh "local all all trust test_hba_guc.undefined_var=value\n";
+close $hba_no_ext_fh;
+
+$node_no_ext->start;
+
+my ($ret, $stdout, $stderr) = $node_no_ext->psql('postgres', 'SELECT 1;');
+isnt($ret, 0, 'Connection rejected when HBA GUC variable is undefined');
+like($stderr, qr/authentication configuration error/,
+	'Error message indicates authentication configuration problem');
+like($stderr, qr/undefined GUC variable/,
+	'Error message mentions undefined GUC variable');
+
+$node_no_ext->stop;
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_hba_guc/t/002_hba_guc_sources.pl b/src/test/modules/test_hba_guc/t/002_hba_guc_sources.pl
new file mode 100644
index 00000000000..e762b8a96d8
--- /dev/null
+++ b/src/test/modules/test_hba_guc/t/002_hba_guc_sources.pl
@@ -0,0 +1,187 @@
+# Test that PGC_HBA variables can only be set from appropriate sources
+#
+# PGC_HBA variables should be settable from:
+# - postgresql.conf
+# - postgresql.auto.conf (ALTER SYSTEM)
+# - pg_hba.conf
+#
+# But NOT from:
+# - ALTER USER SET
+# - ALTER DATABASE SET
+# - Connection parameters (PGOPTIONS)
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test 1: PGC_HBA variable CAN be set in postgresql.conf
+{
+	my $node = PostgreSQL::Test::Cluster->new('conf_allowed');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	$node->append_conf('postgresql.conf',
+		"test_hba_guc.string_var = 'from_postgresql_conf'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my $result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'from_postgresql_conf',
+		'PGC_HBA variable can be set in postgresql.conf');
+
+	$result = $node->safe_psql('postgres',
+		"SELECT source FROM pg_settings WHERE name = 'test_hba_guc.string_var';");
+	is($result, 'configuration file',
+		'Source shows configuration file for postgresql.conf setting');
+
+	$node->stop;
+}
+
+# Test 2: PGC_HBA variable CAN be set via ALTER SYSTEM (postgresql.auto.conf)
+{
+	my $node = PostgreSQL::Test::Cluster->new('alter_system_allowed');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET test_hba_guc.string_var = 'from_alter_system';");
+
+	$node->reload;
+
+	my $result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'from_alter_system',
+		'PGC_HBA variable can be set via ALTER SYSTEM');
+
+	$result = $node->safe_psql('postgres',
+		"SELECT source FROM pg_settings WHERE name = 'test_hba_guc.string_var';");
+	is($result, 'configuration file',
+		'Source shows configuration file for ALTER SYSTEM setting');
+
+	$node->stop;
+}
+
+# Test 3: PGC_HBA variable CANNOT be set via ALTER USER SET
+{
+	my $node = PostgreSQL::Test::Cluster->new('alter_user_rejected');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	$node->safe_psql('postgres', "CREATE USER testuser;");
+
+	my ($ret, $stdout, $stderr) = $node->psql('postgres',
+		"ALTER USER testuser SET test_hba_guc.string_var = 'from_alter_user';");
+	isnt($ret, 0, 'ALTER USER SET rejected for PGC_HBA variable');
+	like($stderr, qr/cannot be set by ALTER USER or ALTER DATABASE/,
+		'Error message indicates ALTER USER is not allowed for PGC_HBA');
+
+	$node->stop;
+}
+
+# Test 4: PGC_HBA variable CANNOT be set via ALTER DATABASE SET
+{
+	my $node = PostgreSQL::Test::Cluster->new('alter_database_rejected');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my ($ret, $stdout, $stderr) = $node->psql('postgres',
+		"ALTER DATABASE postgres SET test_hba_guc.string_var = 'from_alter_database';");
+	isnt($ret, 0, 'ALTER DATABASE SET rejected for PGC_HBA variable');
+	like($stderr, qr/cannot be set by ALTER USER or ALTER DATABASE/,
+		'Error message indicates ALTER DATABASE is not allowed for PGC_HBA');
+
+	$node->stop;
+}
+
+# Test 5: PGC_HBA variable CANNOT be set via connection parameter (PGOPTIONS)
+{
+	my $node = PostgreSQL::Test::Cluster->new('pgoptions_rejected');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	# Connection succeeds but parameter is not set (gets default value)
+	local $ENV{PGOPTIONS} = '-c test_hba_guc.string_var=from_pgoptions';
+	my $result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'default_value',
+		'PGC_HBA variable from PGOPTIONS is not set, uses default value');
+
+	my $logfile = $node->logfile;
+	my $log_content = slurp_file($logfile);
+	like($log_content, qr/parameter "test_hba_guc\.string_var" cannot be changed now/,
+		'Warning logged when trying to set PGC_HBA via PGOPTIONS');
+
+	$node->stop;
+}
+
+# Test 6: PGC_HBA variable CANNOT be set via SET command
+{
+	my $node = PostgreSQL::Test::Cluster->new('set_rejected');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my ($ret, $stdout, $stderr) = $node->psql('postgres',
+		"SET test_hba_guc.string_var = 'from_set';");
+	isnt($ret, 0, 'SET command rejected for PGC_HBA variable');
+	like($stderr, qr/cannot be changed|cannot be set/,
+		'Error message indicates SET is not allowed for PGC_HBA');
+
+	$node->stop;
+}
+
+done_testing();
diff --git a/src/test/modules/test_hba_guc/t/003_hba_guc_precedence.pl b/src/test/modules/test_hba_guc/t/003_hba_guc_precedence.pl
new file mode 100644
index 00000000000..4afaad95c73
--- /dev/null
+++ b/src/test/modules/test_hba_guc/t/003_hba_guc_precedence.pl
@@ -0,0 +1,105 @@
+# Test that PGC_HBA variables from pg_hba.conf respect line precedence
+#
+# pg_hba.conf is evaluated top-to-bottom, and the first matching line wins.
+# This test verifies that GUC values are taken from the correct matching line.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test 1: First matching line wins (same user, same database, same auth methods)
+{
+	my $node = PostgreSQL::Test::Cluster->new('first_match_wins');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust test_hba_guc.string_var=first_line\n";
+	print $hba_fh "local all all trust test_hba_guc.string_var=second_line\n";
+	print $hba_fh "local all all trust test_hba_guc.string_var=third_line\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my $result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'first_line',
+		'First matching HBA line wins when multiple lines match');
+
+	$node->stop;
+}
+
+# Test 2: Database-specific GUC values
+{
+	my $node = PostgreSQL::Test::Cluster->new('database_specific');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	# Create test databases
+	$node->start;
+	$node->safe_psql('postgres', 'CREATE DATABASE testdb1;');
+	$node->safe_psql('postgres', 'CREATE DATABASE testdb2;');
+	$node->stop;
+
+	# Create pg_hba.conf with database-specific values
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local testdb1 all trust test_hba_guc.string_var=from_testdb1\n";
+	print $hba_fh "local testdb2 all trust test_hba_guc.string_var=from_testdb2\n";
+	print $hba_fh "local all all trust test_hba_guc.string_var=from_wildcard\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my $result = $node->safe_psql('testdb1',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'from_testdb1',
+		'Database-specific GUC value applied for testdb1');
+
+	$result = $node->safe_psql('testdb2',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'from_testdb2',
+		'Database-specific GUC value applied for testdb2');
+
+	$result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'from_wildcard',
+		'Wildcard GUC value applied for postgres database');
+
+	$node->stop;
+}
+
+# Test 3: Empty/no GUC options on first match, GUC options on later line
+# The first matching line wins even if it has no GUC options
+{
+	my $node = PostgreSQL::Test::Cluster->new('no_gucs_first');
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"session_preload_libraries = 'test_hba_guc'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust\n";
+	print $hba_fh "local all all trust test_hba_guc.string_var=not_used\n";
+	close $hba_fh;
+
+	$node->start;
+
+	# Should get default value since first matching line has no GUC options
+	my $result = $node->safe_psql('postgres',
+		"SELECT current_setting('test_hba_guc.string_var');");
+	is($result, 'default_value',
+		'Default GUC value when first matching line has no GUC options');
+
+	$node->stop;
+}
+
+done_testing();
diff --git a/src/test/modules/test_hba_guc/test_hba_guc--1.0.sql b/src/test/modules/test_hba_guc/test_hba_guc--1.0.sql
new file mode 100644
index 00000000000..eb29d174f52
--- /dev/null
+++ b/src/test/modules/test_hba_guc/test_hba_guc--1.0.sql
@@ -0,0 +1,22 @@
+/* src/test/modules/test_hba_guc/test_hba_guc--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_hba_guc" to load this file. \quit
+
+-- Function to get current value of test_hba_guc.string_var
+CREATE FUNCTION get_test_hba_string()
+RETURNS text
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+-- Function to get current value of test_hba_guc.int_var
+CREATE FUNCTION get_test_hba_int()
+RETURNS integer
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+-- Function to get current value of test_hba_guc.bool_var
+CREATE FUNCTION get_test_hba_bool()
+RETURNS boolean
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_hba_guc/test_hba_guc.c b/src/test/modules/test_hba_guc/test_hba_guc.c
new file mode 100644
index 00000000000..74b4a559ac5
--- /dev/null
+++ b/src/test/modules/test_hba_guc/test_hba_guc.c
@@ -0,0 +1,93 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_hba_guc.c
+ *		Test module for PGC_HBA GUC variables
+ *
+ * This module tests the PGC_HBA context level for GUC variables, which
+ * allows variables to be set in pg_hba.conf or postgresql.conf but not
+ * by client connections.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_hba_guc/test_hba_guc.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC;
+
+static char *test_hba_string = NULL;
+static int	test_hba_int = 0;
+static bool test_hba_bool = false;
+
+PG_FUNCTION_INFO_V1(get_test_hba_string);
+PG_FUNCTION_INFO_V1(get_test_hba_int);
+PG_FUNCTION_INFO_V1(get_test_hba_bool);
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_hba_guc.string_var",
+							   "Test PGC_HBA string variable",
+							   "This variable can only be set in pg_hba.conf or postgresql.conf",
+							   &test_hba_string,
+							   "default_value",
+							   PGC_HBA,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	DefineCustomIntVariable("test_hba_guc.int_var",
+							"Test PGC_HBA integer variable",
+							"This variable can only be set in pg_hba.conf or postgresql.conf",
+							&test_hba_int,
+							42,
+							0,
+							10000,
+							PGC_HBA,
+							0,
+							NULL,
+							NULL,
+							NULL);
+
+	DefineCustomBoolVariable("test_hba_guc.bool_var",
+							 "Test PGC_HBA boolean variable",
+							 "This variable can only be set in pg_hba.conf or postgresql.conf",
+							 &test_hba_bool,
+							 false,
+							 PGC_HBA,
+							 0,
+							 NULL,
+							 NULL,
+							 NULL);
+
+	MarkGUCPrefixReserved("test_hba_guc");
+}
+
+Datum
+get_test_hba_string(PG_FUNCTION_ARGS)
+{
+	if (test_hba_string == NULL)
+		PG_RETURN_NULL();
+	PG_RETURN_TEXT_P(cstring_to_text(test_hba_string));
+}
+
+Datum
+get_test_hba_int(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(test_hba_int);
+}
+
+Datum
+get_test_hba_bool(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(test_hba_bool);
+}
diff --git a/src/test/modules/test_hba_guc/test_hba_guc.conf b/src/test/modules/test_hba_guc/test_hba_guc.conf
new file mode 100644
index 00000000000..29ee434a784
--- /dev/null
+++ b/src/test/modules/test_hba_guc/test_hba_guc.conf
@@ -0,0 +1,3 @@
+# Configuration for test_hba_guc regression tests
+# Load in session_preload_libraries to test the proper validation flow
+session_preload_libraries = 'test_hba_guc'
diff --git a/src/test/modules/test_hba_guc/test_hba_guc.control b/src/test/modules/test_hba_guc/test_hba_guc.control
new file mode 100644
index 00000000000..1f4a99069ba
--- /dev/null
+++ b/src/test/modules/test_hba_guc/test_hba_guc.control
@@ -0,0 +1,5 @@
+# test_hba_guc extension
+comment = 'Test module for PGC_HBA GUC variables'
+default_version = '1.0'
+module_pathname = '$libdir/test_hba_guc'
+relocatable = true
diff --git a/src/test/modules/test_hba_guc_contexts/Makefile b/src/test/modules/test_hba_guc_contexts/Makefile
new file mode 100644
index 00000000000..7f805b0abd8
--- /dev/null
+++ b/src/test/modules/test_hba_guc_contexts/Makefile
@@ -0,0 +1,18 @@
+# src/test/modules/test_hba_guc_contexts/Makefile
+
+MODULE_big = test_hba_guc_contexts
+OBJS = \
+	$(WIN32RES) \
+	test_hba_guc_contexts.o
+PGFILEDESC = "test_hba_guc_contexts - test module for PGC_HBA context validation"
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_hba_guc_contexts
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_hba_guc_contexts/meson.build b/src/test/modules/test_hba_guc_contexts/meson.build
new file mode 100644
index 00000000000..e2c0eeed229
--- /dev/null
+++ b/src/test/modules/test_hba_guc_contexts/meson.build
@@ -0,0 +1,28 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_hba_guc_contexts_sources = files(
+  'test_hba_guc_contexts.c',
+)
+
+if host_system == 'windows'
+  test_hba_guc_contexts_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_hba_guc_contexts',
+    '--FILEDESC', 'test_hba_guc_contexts - test module for PGC_HBA context validation',])
+endif
+
+test_hba_guc_contexts = shared_module('test_hba_guc_contexts',
+  test_hba_guc_contexts_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_hba_guc_contexts
+
+tests += {
+  'name': 'test_hba_guc_contexts',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_context_validation.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_hba_guc_contexts/t/001_context_validation.pl b/src/test/modules/test_hba_guc_contexts/t/001_context_validation.pl
new file mode 100644
index 00000000000..35d540c04e8
--- /dev/null
+++ b/src/test/modules/test_hba_guc_contexts/t/001_context_validation.pl
@@ -0,0 +1,85 @@
+# Test that only variables with context PGC_HBA or below can be set from pg_hba.conf
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+sub test_hba_rejected
+{
+	my ($node_name, $var_name, $test_desc) = @_;
+
+	my $node = PostgreSQL::Test::Cluster->new($node_name);
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"shared_preload_libraries = 'test_hba_guc_contexts'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust test_hba_guc_contexts.$var_name=from_hba\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my ($ret, $stdout, $stderr) = $node->psql('postgres', 'SELECT 1;');
+	isnt($ret, 0, $test_desc);
+	like($stderr, qr/cannot be changed|cannot be set/,
+		'Error message indicates variable cannot be set from pg_hba.conf');
+
+	$node->stop;
+	return;
+}
+
+sub test_hba_accepted
+{
+	my ($node_name, $var_name, $test_desc) = @_;
+
+	my $node = PostgreSQL::Test::Cluster->new($node_name);
+	$node->init;
+
+	$node->append_conf('postgresql.conf',
+		"shared_preload_libraries = 'test_hba_guc_contexts'");
+
+	my $hba_conf = $node->data_dir . '/pg_hba.conf';
+	open my $hba_fh, '>', $hba_conf or die "Could not open $hba_conf: $!";
+	print $hba_fh "local all all trust test_hba_guc_contexts.$var_name=from_hba\n";
+	close $hba_fh;
+
+	$node->start;
+
+	my ($ret, $stdout, $stderr) = $node->psql('postgres',
+		"SHOW test_hba_guc_contexts.$var_name;");
+	is($ret, 0, $test_desc);
+	like($stdout, qr/from_hba/, 'Variable was set to value from pg_hba.conf');
+
+	$node->stop;
+	return;
+}
+
+# Test 1: PGC_POSTMASTER variable should NOT be settable from pg_hba.conf
+test_hba_rejected('postmaster_rejected', 'postmaster_var',
+	'Connection rejected when trying to set PGC_POSTMASTER from pg_hba.conf');
+
+# Test 2: PGC_SIGHUP variable should NOT be settable from pg_hba.conf
+test_hba_rejected('sighup_rejected', 'sighup_var',
+	'Connection rejected when trying to set PGC_SIGHUP from pg_hba.conf');
+
+# Test 3: PGC_SU_BACKEND variable SHOULD be settable from pg_hba.conf
+test_hba_accepted('su_backend_accepted', 'su_backend_var',
+	'Connection accepted when setting PGC_SU_BACKEND from pg_hba.conf');
+
+# Test 4: PGC_BACKEND variable SHOULD be settable from pg_hba.conf
+test_hba_accepted('backend_accepted', 'backend_var',
+	'Connection accepted when setting PGC_BACKEND from pg_hba.conf');
+
+# Test 5: PGC_SUSET variable SHOULD be settable from pg_hba.conf
+test_hba_accepted('suset_accepted', 'suset_var',
+	'Connection accepted when setting PGC_SUSET from pg_hba.conf');
+
+# Test 6: PGC_USERSET variable SHOULD be settable from pg_hba.conf
+test_hba_accepted('userset_accepted', 'userset_var',
+	'Connection accepted when setting PGC_USERSET from pg_hba.conf');
+
+done_testing();
diff --git a/src/test/modules/test_hba_guc_contexts/test_hba_guc_contexts.c b/src/test/modules/test_hba_guc_contexts/test_hba_guc_contexts.c
new file mode 100644
index 00000000000..0859f70174a
--- /dev/null
+++ b/src/test/modules/test_hba_guc_contexts/test_hba_guc_contexts.c
@@ -0,0 +1,89 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_hba_guc_contexts.c
+ *		Test module for validating PGC_HBA context restrictions
+ *
+ * This module defines GUC variables with different context levels to test
+ * that only variables with context PGC_HBA or below can be set from pg_hba.conf.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_hba_guc_contexts/test_hba_guc_contexts.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC;
+
+static char *test_postmaster_var = NULL;
+static char *test_sighup_var = NULL;
+static char *test_su_backend_var = NULL;
+static char *test_backend_var = NULL;
+static char *test_suset_var = NULL;
+static char *test_userset_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_hba_guc_contexts.postmaster_var",
+								"Test PGC_POSTMASTER variable",
+								"This should NOT be settable from pg_hba.conf",
+								&test_postmaster_var,
+								"postmaster_default",
+								PGC_POSTMASTER,
+								0,
+								NULL, NULL, NULL);
+
+	DefineCustomStringVariable("test_hba_guc_contexts.sighup_var",
+								"Test PGC_SIGHUP variable",
+								"This should NOT be settable from pg_hba.conf",
+								&test_sighup_var,
+								"sighup_default",
+								PGC_SIGHUP,
+								0,
+								NULL, NULL, NULL);
+
+	DefineCustomStringVariable("test_hba_guc_contexts.su_backend_var",
+								"Test PGC_SU_BACKEND variable",
+								"This SHOULD be settable from pg_hba.conf",
+								&test_su_backend_var,
+								"su_backend_default",
+								PGC_SU_BACKEND,
+								0,
+								NULL, NULL, NULL);
+
+	DefineCustomStringVariable("test_hba_guc_contexts.backend_var",
+								"Test PGC_BACKEND variable",
+								"This SHOULD be settable from pg_hba.conf",
+								&test_backend_var,
+								"backend_default",
+								PGC_BACKEND,
+								0,
+								NULL, NULL, NULL);
+
+	DefineCustomStringVariable("test_hba_guc_contexts.suset_var",
+								"Test PGC_SUSET variable",
+								"This SHOULD be settable from pg_hba.conf",
+								&test_suset_var,
+								"suset_default",
+								PGC_SUSET,
+								0,
+								NULL, NULL, NULL);
+
+	DefineCustomStringVariable("test_hba_guc_contexts.userset_var",
+								"Test PGC_USERSET variable",
+								"This SHOULD be settable from pg_hba.conf",
+								&test_userset_var,
+								"userset_default",
+								PGC_USERSET,
+								0,
+								NULL, NULL, NULL);
+
+	MarkGUCPrefixReserved("test_hba_guc_contexts");
+}
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 7+ messages in thread


end of thread, other threads:[~2026-01-28 16:04 UTC | newest]

Thread overview: 7+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-19 20:30 Re: Custom oauth validator options Zsolt Parragi <[email protected]>
2026-01-20 18:02 ` Jacob Champion <[email protected]>
2026-01-20 20:31   ` Zsolt Parragi <[email protected]>
2026-01-24 00:04     ` Jacob Champion <[email protected]>
2026-01-26 09:51       ` Zsolt Parragi <[email protected]>
2026-01-27 17:40         ` Jacob Champion <[email protected]>
2026-01-28 16:04           ` Zsolt Parragi <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox