Thoughts on a "global" client configuration?

Started by Jacob Champion3 months ago27 messages
#1Jacob Champion
jacob.champion@enterprisedb.com

Hi all,

I want to move towards a world where we have sslmode=verify-full by
default. (And maybe gssencmode=disabled, for that matter.) But
changing defaults is risky for established users.

I'm exploring the idea of a global configuration for libpq --
/etc/libpqrc, if you will -- that contains all of our connection
options and lets people override our decisions. So new users and
established users don't have to agree on what's best for their use
cases, and we can make improvements without fearing that we've locked
some subset of users into their "last version" of Postgres because
they can't upgrade.

I started on a proof of concept and very quickly hit a fork. Do I
1) introduce a completely new config file, or
2) adapt pg_service.conf to this use case?

If you're interested in that proof of concept, I'd like to know which
option you'd like to see first. Some thoughts on each are in the
appendix below, if you've got time, but a quick straw-poll response is
helpful too.

Thanks!
--Jacob

= Appendix: Design Thoughts =

I wanted my PoC to show the following:
- a two-tier approach, so that administrators can set system-wide
defaults in /etc and users can set user-wide overrides for those
defaults in their home directory
- backwards and forwards compatibility (we don't ever break old
libpqs, but new libpqs can add new options safely)

That last part is why I initially preferred option (1). I didn't want
to have to figure out the cross-compatibility implications of adapting
pg_service.conf. I thought we could use installation-specific
/etc/postgresql/<version>/libpqrc files and have them be completely
separate from the longstanding service concept.

Unfortunately that has at least one design mistake, which is that the
user-tier config file can't have a version-specific prefix. So I
either put the version into the name (gross), or else I have to solve
cross-compatibility anyway.

If I adapt pg_service.conf, I get the tier system for free. But I
would still have to invent some sort of forwards compatibility
mechanism, and my ideas so far involve adding non-INI syntax to the
beginning of the file, where it would be ignored by existing versions.
It puts us closer to ssh_config territory. Not sure how well that
would go over; there are other projects parsing this.

#2Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#1)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 6, 2025 at 2:06 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I want to move towards a world where we have sslmode=verify-full by
default. (And maybe gssencmode=disabled, for that matter.) But
changing defaults is risky for established users.

I'm exploring the idea of a global configuration for libpq --
/etc/libpqrc, if you will -- that contains all of our connection
options and lets people override our decisions. So new users and
established users don't have to agree on what's best for their use
cases, and we can make improvements without fearing that we've locked
some subset of users into their "last version" of Postgres because
they can't upgrade.

I think the down side of this kind of system is that it makes life
harder for people who want to write code that works everywhere. You
have to take into account the possibility that the configuration file
could be overriding the compiled-in defaults and messing up your
extension/tool/utility. This is why we tend to want to avoid
behavior-changing GUCs on the server side -- anybody who writes an
extension now has to be prepared for every possible value of each of
those settings, and it's not a lot of fun.

Now, all that said, I'm not sure how serious that class of problems is
in this case. In the case of a client application, even if it's
intended as a general tool that might be used by many people on many
systems, in most cases, the author of that tool probably shouldn't
have strong opinions about what details ought to be in the connection
string vs. what details ought to be taken from a configuration file
somewhere. But there are counterexamples, such as our regression
tests. It seems like those tests are never likely to be stable under
every possible choice of settings that you might put in that file, and
eventually somebody who is running a buildfarm machine will end up
with a file installed that breaks something, and that will be
annoying. Maybe that can be avoided with an opt-out, like
ignoresystemdefaults=1 or something, but it's worth noting that our
current service-file system avoids this problem by being strictly
opt-in, and I kind of wonder if we ought to stick with that.

Because the alternative to what you're describing here is to just
change the defaults in some new major release, and let the chips fall
where they may. People will have to adjust, and one way they might do
that is by using a service file to recover the old defaults. Whether
that's better or worse than trying to ease the migration with some
mechanism like the one you propose here is a judgement call, but I
don't think anybody thinks that sslmode=prefer is actually a good
default any more. I can imagine votes for disable, verify-full, and
maybe require, but prefer seems obviously silly.

To be honest, I think part of the problem here has to do with our
choice of syntax. For HTTP, you just change the URL from http to https
and it's one extra character. Decorating every connection string with
sslmode=none (if the default is verify-full and you're running on a
trusted network) or sslmode=verify-full (if the default is none and
you're not running on a trusted network) feels bad, especially if you
have to type those connection strings by hand with any frequency. I
can't help wondering how much of the resistance in this area
(including mine, FWIW) is subtly influenced by the feeling that it's
going to be really annoying when the default isn't what you want in a
particular case.

But despite that, I still think we should ask ourselves whether it
isn't better to endure a hard compatibility break. Maybe it isn't, and
a config file as you propose is indeed a better option. But it doesn't
solve the problem of deciding what the default ought to be in the
absence of a config file, and it does potentially create some new
problems.

--
Robert Haas
EDB: http://www.enterprisedb.com

#3Andrew Dunstan
andrew@dunslane.net
In reply to: Robert Haas (#2)
Re: Thoughts on a "global" client configuration?

On 2025-10-08 We 10:39 AM, Robert Haas wrote:

On Mon, Oct 6, 2025 at 2:06 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I want to move towards a world where we have sslmode=verify-full by
default. (And maybe gssencmode=disabled, for that matter.) But
changing defaults is risky for established users.

I'm exploring the idea of a global configuration for libpq --
/etc/libpqrc, if you will -- that contains all of our connection
options and lets people override our decisions. So new users and
established users don't have to agree on what's best for their use
cases, and we can make improvements without fearing that we've locked
some subset of users into their "last version" of Postgres because
they can't upgrade.

I think the down side of this kind of system is that it makes life
harder for people who want to write code that works everywhere. You
have to take into account the possibility that the configuration file
could be overriding the compiled-in defaults and messing up your
extension/tool/utility. This is why we tend to want to avoid
behavior-changing GUCs on the server side -- anybody who writes an
extension now has to be prepared for every possible value of each of
those settings, and it's not a lot of fun.

Now, all that said, I'm not sure how serious that class of problems is
in this case. In the case of a client application, even if it's
intended as a general tool that might be used by many people on many
systems, in most cases, the author of that tool probably shouldn't
have strong opinions about what details ought to be in the connection
string vs. what details ought to be taken from a configuration file
somewhere. But there are counterexamples, such as our regression
tests. It seems like those tests are never likely to be stable under
every possible choice of settings that you might put in that file, and
eventually somebody who is running a buildfarm machine will end up
with a file installed that breaks something, and that will be
annoying. Maybe that can be avoided with an opt-out, like
ignoresystemdefaults=1 or something, but it's worth noting that our
current service-file system avoids this problem by being strictly
opt-in, and I kind of wonder if we ought to stick with that.

Because the alternative to what you're describing here is to just
change the defaults in some new major release, and let the chips fall
where they may. People will have to adjust, and one way they might do
that is by using a service file to recover the old defaults. Whether
that's better or worse than trying to ease the migration with some
mechanism like the one you propose here is a judgement call, but I
don't think anybody thinks that sslmode=prefer is actually a good
default any more. I can imagine votes for disable, verify-full, and
maybe require, but prefer seems obviously silly.

To be honest, I think part of the problem here has to do with our
choice of syntax. For HTTP, you just change the URL from http to https
and it's one extra character. Decorating every connection string with
sslmode=none (if the default is verify-full and you're running on a
trusted network) or sslmode=verify-full (if the default is none and
you're not running on a trusted network) feels bad, especially if you
have to type those connection strings by hand with any frequency. I
can't help wondering how much of the resistance in this area
(including mine, FWIW) is subtly influenced by the feeling that it's
going to be really annoying when the default isn't what you want in a
particular case.

But despite that, I still think we should ask ourselves whether it
isn't better to endure a hard compatibility break. Maybe it isn't, and
a config file as you propose is indeed a better option. But it doesn't
solve the problem of deciding what the default ought to be in the
absence of a config file, and it does potentially create some new
problems.

There's a lot to this POV, and it's made worse by the unattractiveness
of both of Jacob's options.

If we set the default at verify-full (that would be my vote), someone
can undo that for a particular installation by setting PGSSLMODE=prefer
globally on their system, without our inventing a new config file / section.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#4Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Robert Haas (#2)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 8, 2025 at 7:39 AM Robert Haas <robertmhaas@gmail.com> wrote:

Thanks for the feedback!

I think the down side of this kind of system is that it makes life
harder for people who want to write code that works everywhere. You
have to take into account the possibility that the configuration file
could be overriding the compiled-in defaults and messing up your
extension/tool/utility.

There's truth in that, I think, from a support standpoint. It's a knob
which can be used to improve or destroy, and we could probably expect
to see a transition period where some bug reports are closed with "why
did you put that in your configuration?"

I need to put more thought into the interaction with existing
postgresql:// URIs, as well.

But there are counterexamples, such as our regression
tests. It seems like those tests are never likely to be stable under
every possible choice of settings that you might put in that file, and
eventually somebody who is running a buildfarm machine will end up
with a file installed that breaks something, and that will be
annoying.

I think this is already solved. We have PGSYSCONFDIR, which allows
tests to pull configuration from the temporary installation directory
instead (and makes pg_service.conf relocatable for all of our
utilities). Both of my proposed solutions can make use of that.

Maybe that can be avoided with an opt-out, like
ignoresystemdefaults=1 or something

An opt-out would be pretty easy to add if needed, and it would align
with clients like OpenSSH (`-F none`) and OpenLDAP (`LDAPNOINIT=1`).
But I'd want to pin down what the use cases are before adding one.

Because the alternative to what you're describing here is to just
change the defaults in some new major release, and let the chips fall
where they may. People will have to adjust, and one way they might do
that is by using a service file to recover the old defaults. Whether
that's better or worse than trying to ease the migration with some
mechanism like the one you propose here is a judgement call,

My vote is "worse", because while everyone seems to agree that
`prefer` is bad, no one seems to agree on what the replacement should
be. So momentum on the list dies quickly. And it's not just sslmode
that's in the crosshairs; even if we somehow got to agreement on a
compatibility break there, the exact same discussion is going to
happen with the next one.

In my opinion, there _is_ no "best for everyone". But we can lower the
cost of a "bad" choice (for less common use cases), to make room to
adjust defaults and make them "best for new users", or "best for the
community", or etc. Distribution packagers could further adjust them
to what's best for their communities, too.

To be honest, I think part of the problem here has to do with our
choice of syntax. For HTTP, you just change the URL from http to https
and it's one extra character.

I think a syntax discussion focuses too much on sslmode in particular,
and not enough on other settings that would also be good to ratchet
forward at some point: min_protocol_version, sslnegotiation,
ssl_min_protocol_version, sslrootcert, etc.

The idea that all of these client configuration options belong inside
our resource locators, as opposed to a file on disk somewhere, is...
peculiar. It's nice that they _can_ be part of the syntax, I guess,
but they shouldn't really _have_ to be.

Thanks,
--Jacob

#5Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andrew Dunstan (#3)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 8, 2025 at 1:40 PM Andrew Dunstan <andrew@dunslane.net> wrote:

If we set the default at verify-full (that would be my vote), someone
can undo that for a particular installation by setting PGSSLMODE=prefer
globally on their system

I don't think we should ever tell users to set PGSSLMODE=prefer. It's
really sticky, and you can't know that third-party code won't defer to
it instead of overriding it when they see it defined. A quick Github
code search turns up a few people doing exactly that.

If we make the change at the default level instead, we remain in
control of the override priority, so users will be reverting to the
previous behavior instead of introducing new untested behavior.

--Jacob

#6Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#4)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 8, 2025 at 5:09 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

Because the alternative to what you're describing here is to just
change the defaults in some new major release, and let the chips fall
where they may. People will have to adjust, and one way they might do
that is by using a service file to recover the old defaults. Whether
that's better or worse than trying to ease the migration with some
mechanism like the one you propose here is a judgement call,

My vote is "worse", because while everyone seems to agree that
`prefer` is bad, no one seems to agree on what the replacement should
be. So momentum on the list dies quickly. And it's not just sslmode
that's in the crosshairs; even if we somehow got to agreement on a
compatibility break there, the exact same discussion is going to
happen with the next one.

I'm never going to be a fan of the idea of changing libpq defaults
with any frequency, no matter what configuration options we have. If
we change those defaults with any regularity, I think it will cause a
lot of problems for a lot of people. When there's not agreement on
what to change, leaving things unchanged is often the best answer,
because it at least has the virtue of not causing random breakage. I
also think that Andrew raises a good point about the use of
environment variables. That seems like it serves much the same purpose
as a global configuration file, so I'm not sure we should have both.

--
Robert Haas
EDB: http://www.enterprisedb.com

#7Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Robert Haas (#6)
Re: Thoughts on a "global" client configuration?

On Thu, Oct 9, 2025 at 5:28 AM Robert Haas <robertmhaas@gmail.com> wrote:

I'm never going to be a fan of the idea of changing libpq defaults
with any frequency, no matter what configuration options we have. If
we change those defaults with any regularity, I think it will cause a
lot of problems for a lot of people.

Agreed. (My proposal doesn't advocate for "regular" breakage.)

When there's not agreement on
what to change, leaving things unchanged is often the best answer,

I think that's how we get into situations like "everyone hates
sslmode=require but no one will change it." I'm looking to add a tool
to make agreement easier in the first place.

(Maybe there's a better tool than a configuration file? But I'd like
to see what a file looks like, because I'm not familiar with any other
network client that requires you to put every setting into the URI you
use to contact the server. If you know of one please tell me so I can
study it.)

because it at least has the virtue of not causing random breakage. I
also think that Andrew raises a good point about the use of
environment variables. That seems like it serves much the same purpose
as a global configuration file, so I'm not sure we should have both.

I prefer simplicity, too, but environment variables for libpq mean
"this is a trusted setting that you should prefer to the default, even
if it's worse". That's not a good fit for the security-related
settings I'm concerned with changing.

--Jacob

#8Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#7)
Re: Thoughts on a "global" client configuration?

On Thu, Oct 9, 2025 at 11:09 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

Agreed. (My proposal doesn't advocate for "regular" breakage.)

I understand that, but you mentioned a few different settings ...
which could hypothetically turn into changing the default for 1 of
them every year or two.

When there's not agreement on
what to change, leaving things unchanged is often the best answer,

I think that's how we get into situations like "everyone hates
sslmode=require but no one will change it." I'm looking to add a tool
to make agreement easier in the first place.

That's a fair goal, but I'm not sure that I agree that the tool you're
proposing actually has that effect.

(Maybe there's a better tool than a configuration file? But I'd like
to see what a file looks like, because I'm not familiar with any other
network client that requires you to put every setting into the URI you
use to contact the server. If you know of one please tell me so I can
study it.)

That's a fair complaint, but on the other hand, specifying the use or
non-use of TLS in the URI is completely normal. What's abnormal about
our system is that (1) we've got all of these extra levels that don't
exist for, say, HTTP and (2) our syntax is quite verbose.

I prefer simplicity, too, but environment variables for libpq mean
"this is a trusted setting that you should prefer to the default, even
if it's worse". That's not a good fit for the security-related
settings I'm concerned with changing.

Can you expand on this thought? I don't think I understand. What makes
the environment variables "trusted values that you should prefer to
the default" rather than just "values that we want to use in this
context"?

--
Robert Haas
EDB: http://www.enterprisedb.com

#9Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Robert Haas (#8)
Re: Thoughts on a "global" client configuration?

On Thu, Oct 9, 2025 at 9:02 AM Robert Haas <robertmhaas@gmail.com> wrote:

I understand that, but you mentioned a few different settings ...
which could hypothetically turn into changing the default for 1 of
them every year or two.

I think we should avoid equating "we can change it more frequently
than never" with "we will break everyone all the time". A hypothetical
configuration file does not absolve us of our need to choose defaults
wisely and maintain good interoperability for the community.

I think that's how we get into situations like "everyone hates
sslmode=require but no one will change it." I'm looking to add a tool
to make agreement easier in the first place.

That's a fair goal, but I'm not sure that I agree that the tool you're
proposing actually has that effect.

And that's fair too; I don't expect to drive consensus without a
concrete proposal in hand.

(Maybe there's a better tool than a configuration file? But I'd like
to see what a file looks like, because I'm not familiar with any other
network client that requires you to put every setting into the URI you
use to contact the server. If you know of one please tell me so I can
study it.)

That's a fair complaint, but on the other hand, specifying the use or
non-use of TLS in the URI is completely normal. What's abnormal about
our system is that (1) we've got all of these extra levels that don't
exist for, say, HTTP and (2) our syntax is quite verbose.

This again tries to collapse the problem down to sslmode. Look at
gssencmode, sslnegotiation, sslrootcert: all things which IMHO do not
belong in the query string of a URI. We've put connection settings and
application settings at the same level, and to me, that's the abnormal
thing about our system. (Similar problem to the _pq_.* protocol option
debates, actually.)

When you tell your browser to adjust the meaning of "HTTPS" -- whether
it's adding a new root cert to the trust store, disallowing TLS 1.1,
etc. -- you generally do that in a different place from the URL bar,
and the "clients" making use of HTTPS URLs do not know about it. Our
relative fragmentation (when compared to the Web) probably puts us
closer to the SSH use case than a browser use case, because we
absolutely need per-host connection settings. But SSH still lets you
ratchet up defaults across all hosts with its ssh_config.

(I'm actually tempted to double down on the SSH comparison and say
that its host-matching configuration model might be an even _better_
fit for us than our opt-in services model. Because it'd allow every
libpq client to delegate the question of "how do I connect to X" to
us, if they wanted, without having to drill a service name through the
stack. I don't know how far I want to go down that path, in the
absence of people asking for it.)

I prefer simplicity, too, but environment variables for libpq mean
"this is a trusted setting that you should prefer to the default, even
if it's worse". That's not a good fit for the security-related
settings I'm concerned with changing.

Can you expand on this thought? I don't think I understand. What makes
the environment variables "trusted values that you should prefer to
the default" rather than just "values that we want to use in this
context"?

Sure: In the context of this thread, I want the configuration file to
be able to act as a pressure release valve for admins who absolutely
cannot follow us forward into verify-full by default, by allowing them
to return to the previous behavior. But setting a new environment
variable isn't guaranteed to return to the previous behavior, because
it's reasonable for applications to defer to trusted envvars if
they're set. (Think `${PGSSLMODE:-verify-full}`.)

In the case of PGSSLMODE=prefer, I think that's especially dangerous:
it will always _look_ like things have returned to the previous state,
because everything will still be requesting SSL, but you may have
actively downgraded the security of the application.

The worst-case persona, in my mind, is a new sysadmin who's panicking
because of a libpq5 upgrade in production on Debian, say maybe through
an indirect package dependency, and something has started failing that
wasn't caught in testing. Downgrading means losing whatever package
brought in the dependency, and they're definitely not equipped to
audit all their code to make sure that PGSSLMODE=prefer isn't going to
do something horrible. I want to give that sysadmin a safe way out.

--Jacob

#10Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#9)
Re: Thoughts on a "global" client configuration?

On Thu, Oct 9, 2025 at 4:21 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

This again tries to collapse the problem down to sslmode. Look at
gssencmode, sslnegotiation, sslrootcert: all things which IMHO do not
belong in the query string of a URI. We've put connection settings and
application settings at the same level, and to me, that's the abnormal
thing about our system. (Similar problem to the _pq_.* protocol option
debates, actually.)

That's a really interesting observation. I've always found it a bit
odd that we put things like sslca and sslrootcert into the connection
string, so I think you have a point, here. Not sure I agree about
sslnegotiation or gssencmode, though -- those seem more like sslmode,
which I would argue does belong in the connection string.

Sure: In the context of this thread, I want the configuration file to
be able to act as a pressure release valve for admins who absolutely
cannot follow us forward into verify-full by default, by allowing them
to return to the previous behavior. But setting a new environment
variable isn't guaranteed to return to the previous behavior, because
it's reasonable for applications to defer to trusted envvars if
they're set. (Think `${PGSSLMODE:-verify-full}`.)

I think this is aiming at quite a narrow target.

The worst-case persona, in my mind, is a new sysadmin who's panicking
because of a libpq5 upgrade in production on Debian, say maybe through
an indirect package dependency, and something has started failing that
wasn't caught in testing. Downgrading means losing whatever package
brought in the dependency, and they're definitely not equipped to
audit all their code to make sure that PGSSLMODE=prefer isn't going to
do something horrible. I want to give that sysadmin a safe way out.

I would argue that this is a sign that calling every version libpq5 no
matter how much we've changed the behavior is completely insane. This
scenario gets a whole lot better if installing a new release of
PostgreSQL that behaves differently doesn't magically change the
behavior of any existing releases that are already installed.

At the risk of repeating myself, I also think that we need to consider
the flip side of this scenario: some system administrator who thinks
they know better throws something into a system-wide configuration
file and breaks things for, say, PostgreSQL developers running the
regression tests, or applications running on the machine that assume a
certain system-wide configuration that in reality need not prevail
everywhere. I sometimes worry too much about non-problems at times,
and this might be one of those times, so it would be good to hear from
more people, but I think we need to be convinced not only that this
proposal has enough upside to be worth pursuing but also that the
downsides won't be too painful.

--
Robert Haas
EDB: http://www.enterprisedb.com

#11Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Robert Haas (#10)
Re: Thoughts on a "global" client configuration?

On Fri, Oct 10, 2025 at 6:24 AM Robert Haas <robertmhaas@gmail.com> wrote:

That's a really interesting observation. I've always found it a bit
odd that we put things like sslca and sslrootcert into the connection
string, so I think you have a point, here. Not sure I agree about
sslnegotiation or gssencmode, though -- those seem more like sslmode,
which I would argue does belong in the connection string.

I could see gssencmode=require as a separate scheme. I still object to
its being a query string parameter; I don't think our current
acceptance of "gssencmode=require sslmode=require" is a particularly
good thing.

Putting sslnegotiation at the same level, though, feels like the
ancient distinction between "sldap" (STARTTLS) and "ldaps" (implicit
TLS), which IMO was a confusing historical mistake. And HTTP's version
of sslmode=prefer (opportunistic encryption, a dead experiment) was
still part of the `http:` scheme. Clients didn't opt into that via
URL; they configured it out of band.

Sure: In the context of this thread, I want the configuration file to
be able to act as a pressure release valve for admins who absolutely
cannot follow us forward into verify-full by default, by allowing them
to return to the previous behavior. But setting a new environment
variable isn't guaranteed to return to the previous behavior, because
it's reasonable for applications to defer to trusted envvars if
they're set. (Think `${PGSSLMODE:-verify-full}`.)

I think this is aiming at quite a narrow target.

I agree. But it does exist in the wild [1]https://github.com/search?q=%22PGSSLMODE%3A-require%22&amp;type=code, and sslmode is one of our
most important security settings. So I'm pretty worried about
unilaterally switching to a stronger default, without giving people a
tool to navigate it, if that would actually result in much weaker
security for some percentage of users.

The worst-case persona, in my mind, is a new sysadmin who's panicking
because of a libpq5 upgrade in production on Debian, say maybe through
an indirect package dependency, and something has started failing that
wasn't caught in testing. Downgrading means losing whatever package
brought in the dependency, and they're definitely not equipped to
audit all their code to make sure that PGSSLMODE=prefer isn't going to
do something horrible. I want to give that sysadmin a safe way out.

I would argue that this is a sign that calling every version libpq5 no
matter how much we've changed the behavior is completely insane.

I think SONAME is the wrong hammer to reach for. Network compatibility
is more subtle than link-time ABI. But this has come up before, so I
want to explore it a bit. My guesses:

If we were to update to libpq.so.6 because of this -- without any
actual ABI changes -- then many people downstream aren't going to know
that they're opting into the new behavior anyway. For a true opt-in,
we'd need to actively break people at build time (libpq6.so.0?). Then
those people downstream will want to ask what amazing changes they've
gotten in exchange for this ecosystem fracture, and I suspect they'd
be less than impressed if we said "it makes sslmode stronger by
default".

Our maintainers will probably be less than impressed as well, now that
they get to keep two separate side-by-side binaries that are 99.999%
identical. (And I think there are dependency diamond problems, unless
we version all our symbols, but I haven't paid attention to modern
practices there.)

Maybe the least impressed will be any middleware maintainers who get
their connection strings from a layer above them. We're asking them to
opt into the new behavior at build time, but they can't know whether
it's safe, either. For them, it's _still_ a user-level decision; all
we've done is kicked the can, and nothing has actually improved.

All this to say, I think libpq6 should happen because of an ABI change
relevant to the code that links against us, not because of a user
compatibility change.

This
scenario gets a whole lot better if installing a new release of
PostgreSQL that behaves differently doesn't magically change the
behavior of any existing releases that are already installed.

We could probably do that if we really wanted, without breaking
SONAME. Our server already has its extensions tell it what version
they were compiled against, and our client could similarly use macro
tricks. Or symbol versioning.

But, as above, I still don't think that an opt-in at build time can
fix the general problem. It only delays the pain until the next time a
dependent package is built.

At the risk of repeating myself, I also think that we need to consider
the flip side of this scenario: some system administrator who thinks
they know better throws something into a system-wide configuration
file and breaks things for, say, PostgreSQL developers running the
regression tests

(this is solved, though)

or applications running on the machine that assume a
certain system-wide configuration that in reality need not prevail
everywhere.

I agree we need to consider this scenario; I'm just finding it hard to
treat it as anything other than "don't do that then, highly-privileged
system administrator!" I assume we would tell them the same thing
today if they exported PGSERVICE globally and broke applications that
relied on environment variables.

I sometimes worry too much about non-problems at times,
and this might be one of those times, so it would be good to hear from
more people, but I think we need to be convinced not only that this
proposal has enough upside to be worth pursuing but also that the
downsides won't be too painful.

Agreed.

Thanks,
--Jacob

[1]: https://github.com/search?q=%22PGSSLMODE%3A-require%22&amp;type=code

#12Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#11)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 13, 2025 at 3:57 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

Putting sslnegotiation at the same level, though, feels like the
ancient distinction between "sldap" (STARTTLS) and "ldaps" (implicit
TLS), which IMO was a confusing historical mistake. And HTTP's version
of sslmode=prefer (opportunistic encryption, a dead experiment) was
still part of the `http:` scheme. Clients didn't opt into that via
URL; they configured it out of band.

The distinction in my mind is that sslmode, gssencmode, and
sslnegotiation define what we're doing on the wire, whereas parameters
like sslcert just define where local resources are to be found.
Granted, that influences what we're doing on the wire in a different
way, but you can move the file and update the value of sslcert and get
the same results, because the connection fundamentally doesn't care
where its local files are found, whereas flipping sslnegotiation or
sslmode is a fundamental alteration to what the connection tries to
do. How do you see it?

I think this is aiming at quite a narrow target.

I agree. But it does exist in the wild [1], and sslmode is one of our
most important security settings. So I'm pretty worried about
unilaterally switching to a stronger default, without giving people a
tool to navigate it, if that would actually result in much weaker
security for some percentage of users.

I'm unconvinced that this is worth worrying about. I am a little
surprised to see the number of hits that your search found, but I
think people will (and should) update their code.

I would argue that this is a sign that calling every version libpq5 no
matter how much we've changed the behavior is completely insane.

I think SONAME is the wrong hammer to reach for. Network compatibility
is more subtle than link-time ABI. But this has come up before, so I
want to explore it a bit. My guesses:

If we were to update to libpq.so.6 because of this -- without any
actual ABI changes -- then many people downstream aren't going to know
that they're opting into the new behavior anyway. For a true opt-in,
we'd need to actively break people at build time (libpq6.so.0?). Then
those people downstream will want to ask what amazing changes they've
gotten in exchange for this ecosystem fracture, and I suspect they'd
be less than impressed if we said "it makes sslmode stronger by
default".

My theory is that they'll be even less impressed if they try to use a
supposedly-compatible library and it breaks a bunch of stuff, but I
wonder what Christoph Berg (cc'd) thinks.

--
Robert Haas
EDB: http://www.enterprisedb.com

#13Christoph Berg
myon@debian.org
In reply to: Robert Haas (#12)
Re: Thoughts on a "global" client configuration?

Re: Robert Haas

My theory is that they'll be even less impressed if they try to use a
supposedly-compatible library and it breaks a bunch of stuff, but I
wonder what Christoph Berg (cc'd) thinks.

It would also hinder adoption of PG in more places. There are
currently thousands of software products that link to libpq in some
form, and it would take several years to have them all fixed if
ABI/API compatibility were broken. Chasing the long tail there is
hard; we get to witness that every year with upstreams that aren't
compatible with PG18 yet. For some extensions, I'm still waiting to
get my PG17 (or PG16!) patches merged.

Christoph

#14Robert Haas
robertmhaas@gmail.com
In reply to: Christoph Berg (#13)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 13, 2025 at 4:22 PM Christoph Berg <myon@debian.org> wrote:

Re: Robert Haas

My theory is that they'll be even less impressed if they try to use a
supposedly-compatible library and it breaks a bunch of stuff, but I
wonder what Christoph Berg (cc'd) thinks.

It would also hinder adoption of PG in more places. There are
currently thousands of software products that link to libpq in some
form, and it would take several years to have them all fixed if
ABI/API compatibility were broken. Chasing the long tail there is
hard; we get to witness that every year with upstreams that aren't
compatible with PG18 yet. For some extensions, I'm still waiting to
get my PG17 (or PG16!) patches merged.

So you support calling it libpq.so.5 forever, no matter how much we change?

--
Robert Haas
EDB: http://www.enterprisedb.com

#15Christoph Berg
myon@debian.org
In reply to: Robert Haas (#14)
Re: Thoughts on a "global" client configuration?

Re: Robert Haas

So you support calling it libpq.so.5 forever, no matter how much we change?

I would say SONAME/ABI/API breakages are not a tool to promote better
SSL settings. If we want to move to sslmode=verify-full by default, we
should just do that. I don't see why adding extra ABI/API pain would
make that transition any better for users.

We can move to libpq6 if there are technical reasons, but the general
pain will be long-lasting until the whole world has caught up. (Debian
etc can switch pretty easily, but there's a gazillion of third-party
things.)

Christoph

#16Robert Haas
robertmhaas@gmail.com
In reply to: Christoph Berg (#15)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 13, 2025 at 4:30 PM Christoph Berg <myon@debian.org> wrote:

Re: Robert Haas

So you support calling it libpq.so.5 forever, no matter how much we change?

I would say SONAME/ABI/API breakages are not a tool to promote better
SSL settings. If we want to move to sslmode=verify-full by default, we
should just do that. I don't see why adding extra ABI/API pain would
make that transition any better for users.

We can move to libpq6 if there are technical reasons, but the general
pain will be long-lasting until the whole world has caught up. (Debian
etc can switch pretty easily, but there's a gazillion of third-party
things.)

OK, good to know. I somehow can't get over how crazy it seems to
hadcode the version to 5 forever, but, eh, [ old man yells at cloud
emoji here ].

--
Robert Haas
EDB: http://www.enterprisedb.com

#17Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Robert Haas (#12)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 13, 2025 at 1:15 PM Robert Haas <robertmhaas@gmail.com> wrote:

The distinction in my mind is that sslmode, gssencmode, and
sslnegotiation define what we're doing on the wire, whereas parameters
like sslcert just define where local resources are to be found.
Granted, that influences what we're doing on the wire in a different
way, but you can move the file and update the value of sslcert and get
the same results, because the connection fundamentally doesn't care
where its local files are found, whereas flipping sslnegotiation or
sslmode is a fundamental alteration to what the connection tries to
do. How do you see it?

I agree with your bucketing of wire-vs-non-wire parameters, but to me,
that's not really the dividing line for what belongs in an "ideal"
connection string. It's just an indication that something is off --
that there is no dividing line right now, and that's strange.

I've been trying to frame this in terms of our URI structure, mostly
because it highlights how odd our setup is. It's true that you don't
type your client certificate path into your browser's location bar
(it's not on the wire), but it's also true that you don't put the
minimum TLS version there, or the OCSP fallback behavior (both on the
wire). The latter two are still out-of-band decisions, separate from
the name of the resource itself, even though they directly affect
whether a browser can connect at all to certain hosts.

That goes back to what I was saying earlier about ssh_config, and how
its style might have been a better match for some of our use cases.
Host matching lets the user say "oh yeah, that server is weird,
connect this way" without forcing every application that uses SSH to
understand that information.

I'm unconvinced that this is worth worrying about. I am a little
surprised to see the number of hits that your search found, but I
think people will (and should) update their code.

I think one of the fundamental issues with sslmode=prefer is that the
affected people are unlikely to notice they should update their code.
SSL will continue to be used. Nothing will break, but active attacks
will be silently enabled. And if someone affected by this comes to a
list to report and/or complain, I think they're likely to be greeted
by a chorus of "why would an informed sysadmin ever define
PGSSLMODE=prefer? so insecure, PEBKAC, skill issue".

I get that I'm coming at this from the paranoid-security-person side
of things, but I feel like the ecosystem effects of officially
recommending a bad PGSSLMODE as a compatibility hack are not being
given the same scrutiny as the ecosystem effects of adding a
configuration file.

My theory is that they'll be even less impressed if they try to use a
supposedly-compatible library and it breaks a bunch of stuff

I agree with that, but also that's why I started this thread. I think
both options (keeping sslmode=prefer by default, and unilaterally
changing to something stronger) are untenable, and I'm trying to cut
the knot.

Some library I was looking at recently (can't remember which, or I'd
link it) had an interesting approach to compatibility breaks. It
1) warned about the break in version X, if you relied on the behavior,
2) broke behavior by default in version X+1, but gave you the option
to return to the prior functionality for a single release, and then
3) removed the deprecated behavior in version X+2.

I don't think that exact plan would work for us, but if our back
branches had client config files, you might imagine a world where we
backported warnings for really important changes. "You're relying on
default behavior that is about to change in PG30, and you need to
explicitly opt into the old behavior now if you don't want to break
when you upgrade."

(Proof-of-concept on the way soon.)

Thanks,
--Jacob

#18Peter Eisentraut
peter@eisentraut.org
In reply to: Robert Haas (#2)
Re: Thoughts on a "global" client configuration?

On 08.10.25 16:39, Robert Haas wrote:

To be honest, I think part of the problem here has to do with our
choice of syntax. For HTTP, you just change the URL from http to https
and it's one extra character. Decorating every connection string with
sslmode=none (if the default is verify-full and you're running on a
trusted network) or sslmode=verify-full (if the default is none and
you're not running on a trusted network) feels bad, especially if you
have to type those connection strings by hand with any frequency.

But even a browser has a default setting for which variant to use when
you type in a domain name without a scheme. And in most cases, that
default was changed at some point during the last 20 years.

#19Peter Eisentraut
peter@eisentraut.org
In reply to: Jacob Champion (#1)
Re: Thoughts on a "global" client configuration?

On 06.10.25 20:05, Jacob Champion wrote:

I started on a proof of concept and very quickly hit a fork. Do I
1) introduce a completely new config file, or
2) adapt pg_service.conf to this use case?

I've been thinking about this kind of thing for a long time, and my
intuition has always been to have some kind of [default] section in
pg_service.conf. That would probably be relatively easy.

But:

- backwards and forwards compatibility (we don't ever break old
libpqs, but new libpqs can add new options safely)

It might be worth elaborating exactly how this would be solved. If I
look through my dotfiles history, this kind of thing has been a
perennial problem. I don't have any specific ideas right now -- other
than perhaps "ignore unknown parameters", which is surely not without
problems. Depending on what we'd settle on here, that might inform
whether it's feasible to stick this into pg_service.conf or whether it
needs to be a separate thing.

#20Isaac Morland
isaac.morland@gmail.com
In reply to: Peter Eisentraut (#19)
Re: Thoughts on a "global" client configuration?

On Wed, 15 Oct 2025 at 15:20, Peter Eisentraut <peter@eisentraut.org> wrote:

On 06.10.25 20:05, Jacob Champion wrote:

I started on a proof of concept and very quickly hit a fork. Do I
1) introduce a completely new config file, or
2) adapt pg_service.conf to this use case?

I've been thinking about this kind of thing for a long time, and my
intuition has always been to have some kind of [default] section in
pg_service.conf. That would probably be relatively easy.

Maybe have a way to specify one or more "base" configurations for each
service?

Something like (except please don't use the "extends" keyword, I've done
enough Java already; pick some punctuation instead):

[default]
sslmode=require

[dev]
host=dev.postgres.example.com

[service1] extends default
host=postgres.example.com
user=appuser

[service2] extends default
host=postgres2.example.com
user=otherappuser

[service1-dev] extends default, dev
user=appuser

I don't know how big a deal it is that this is no longer an "INI" file.

#21Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Isaac Morland (#20)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 15, 2025 at 12:35 PM Isaac Morland <isaac.morland@gmail.com> wrote:

Maybe have a way to specify one or more "base" configurations for each service?

For the use case I have in mind, my intention is that you shouldn't
have to use a service at all to get these defaults to apply.

[service1] extends default
host=postgres.example.com
user=appuser

Nested services is interesting as well. I don't think it solves my use
case, because of a very subtle difference between defaults and
services (I intend to go into more detail in the other email I'm
typing up now).

I don't know how big a deal it is that this is no longer an "INI" file.

We would almost certainly break some libpq-compatible software if we
added new syntax. I don't know if that's a feature or a bug yet, but I
do think it's avoidable, so I'm going to try to avoid it for now.
Needs more thought.

Thanks!
--Jacob

#22Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Peter Eisentraut (#19)
6 attachment(s)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 15, 2025 at 12:20 PM Peter Eisentraut <peter@eisentraut.org> wrote:

- backwards and forwards compatibility (we don't ever break old
libpqs, but new libpqs can add new options safely)

It might be worth elaborating exactly how this would be solved. If I
look through my dotfiles history, this kind of thing has been a
perennial problem. I don't have any specific ideas right now -- other
than perhaps "ignore unknown parameters", which is surely not without
problems. Depending on what we'd settle on here, that might inform
whether it's feasible to stick this into pg_service.conf or whether it
needs to be a separate thing.

Good timing. Here's a patchset that experiments with putting it all
into pg_service.conf.

= Roadmap =

Patches 0001-0003 are small tweaks to make my life easier. I can pull
them out separately if there's interest.

0004 implements a defaults section:

[my-default-section]
+=defaults
sslmode=verify-full
gssencmode=disable

[other-service]
...

0005 implements forwards compatibility by marking specific defaults as
ignorable:

[default]
+=defaults
require_auth=scram-sha-256
channel_binding=require
?incredible_scram_feature=require

0006 implements PGNODEFAULTS to allow our test suites (and anything
else) to turn off the new handling. This prevents a broken
~/.pg_service.conf from interfering with our tests. (A different way
of solving that could be to point PGSERVICE to a blank file. I kind of
liked the "turn it off" switch, though.)

= Thoughts =

I wanted to keep INI compatibility. I found a few clients that run
pg_service.conf through a generic INI parser, which seems like an
entirely reasonable thing to do. Going back to my earlier argument
against environment variables, if we make people use this tool to get
themselves out of a compatibility problem we introduce, and it then
causes other existing parts of the Postgres ecosystem to break, I
wouldn't feel great about that.

(I could see an argument that breaking those clients would make it
obvious that they can't apply the new defaults section correctly. But
our old libpqs won't be able to apply the defaults section either, and
we're presumably not going to accept breaking old libpqs.)

I wanted to avoid stomping on existing service names. I could have
gone the Windows route and generated a GUID or something, but instead
I've allowed the user to choose any name they want for this section.
They then mark it with the (maybe-too-cute?) magic string of
"+=defaults", which
1) will cause earlier libpqs to fail if they accidentally try to
select the section as a service,
2) is INI parseable (the key is "+"), and
3) kind of suggests what the section is meant to do: add these
settings to the defaults.

I've tried to avoid too much unbearable confusion by requiring that a
defaults service be at the top of the file and have its marker first
in the section.

One subtlety that I hadn't considered was how the user and system
files interact with one another. I want user defaults to override
system defaults, if they are in conflict. But user services completely
replace system services of the same name, so the code needs to keep
the two behaviors separate.

An emergent feature popped out of this while I was implementing the
tests. You can now choose a default service, and the effect is that
any settings listed there take precedence over the envvars.
"Superdefaults." This is fragile, though -- setting a different
service gets rid of those rather than merging them -- and I was idly
wondering if that was something that could/should be made into its own
first-class concept.

The ability to ignore specific options was inspired by the ability of
an ssh_config to IgnoreUnknown. Maybe you don't care if a nice-to-have
option is ignored by older libpqs, but you maybe want to fail
immediately if some security-critical option can't be honored (or if
you just made a typo), and I think we should assume the latter. This
feature would let you mark them accordingly.

I'm not sure if all this is better than an architecture where the
defaults and services are contained in different files. Since the
syntax and behavior of the two types of sections is explicitly
different, maybe combining them would be unnecessarily confusing for
users?

--Jacob

Attachments:

0001-libpq-Simplify-newline-handling-in-t-006_service.patchapplication/x-patch; name=0001-libpq-Simplify-newline-handling-in-t-006_service.patchDownload
From 49691072e220f240f392d9bc6863469f846a09b7 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Tue, 7 Oct 2025 10:54:29 -0700
Subject: [PATCH 1/6] libpq: Simplify newline handling in t/006_service

CRLF translation will already be handled by text mode; we don't need to
add our own.
---
 src/interfaces/libpq/t/006_service.pl | 13 ++++---------
 1 file changed, 4 insertions(+), 9 deletions(-)

diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 797e6232b8f..432a7290c65 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -22,18 +22,14 @@ $dummy_node->init;
 
 my $td = PostgreSQL::Test::Utils::tempdir;
 
-# Windows vs non-Windows: CRLF vs LF for the file's newline, relying on
-# the fact that libpq uses fgets() when reading the lines of a service file.
-my $newline = $windows_os ? "\r\n" : "\n";
-
 # Create the set of service files used in the tests.
 # File that includes a valid service name, and uses a decomposed connection
 # string for its contents, split on spaces.
 my $srvfile_valid = "$td/pg_service_valid.conf";
-append_to_file($srvfile_valid, "[my_srv]" . $newline);
+append_to_file($srvfile_valid, "[my_srv]\n");
 foreach my $param (split(/\s+/, $node->connstr))
 {
-	append_to_file($srvfile_valid, $param . $newline);
+	append_to_file($srvfile_valid, $param . "\n");
 }
 
 # File defined with no contents, used as default value for PGSERVICEFILE,
@@ -51,14 +47,13 @@ my $srvfile_missing = "$td/pg_service_missing.conf";
 my $srvfile_nested = "$td/pg_service_nested.conf";
 copy($srvfile_valid, $srvfile_nested)
   or die "Could not copy $srvfile_valid to $srvfile_nested: $!";
-append_to_file($srvfile_nested, 'service=invalid_srv' . $newline);
+append_to_file($srvfile_nested, "service=invalid_srv\n");
 
 # Service file with nested "servicefile" defined.
 my $srvfile_nested_2 = "$td/pg_service_nested_2.conf";
 copy($srvfile_valid, $srvfile_nested_2)
   or die "Could not copy $srvfile_valid to $srvfile_nested_2: $!";
-append_to_file($srvfile_nested_2,
-	'servicefile=' . $srvfile_default . $newline);
+append_to_file($srvfile_nested_2, "servicefile=$srvfile_default\n");
 
 # Set the fallback directory lookup of the service file to the temporary
 # directory of this test.  PGSYSCONFDIR is used if the service file
-- 
2.34.1

0002-libpq-Pin-sectionless-keyword-behavior-in-pg_service.patchapplication/x-patch; name=0002-libpq-Pin-sectionless-keyword-behavior-in-pg_service.patchDownload
From 0132a4e8fddad82b0f4876f4ea42b7de142356ba Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Tue, 7 Oct 2025 15:19:58 -0700
Subject: [PATCH 2/6] libpq: Pin sectionless-keyword behavior in
 pg_service.conf

We've always ignored lines before the first named section of
pg_service.conf. Pin that behavior in the tests so that it doesn't
regress by accident.
---
 src/interfaces/libpq/t/006_service.pl | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 432a7290c65..9c692739511 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -26,7 +26,15 @@ my $td = PostgreSQL::Test::Utils::tempdir;
 # File that includes a valid service name, and uses a decomposed connection
 # string for its contents, split on spaces.
 my $srvfile_valid = "$td/pg_service_valid.conf";
-append_to_file($srvfile_valid, "[my_srv]\n");
+append_to_file(
+	$srvfile_valid, qq{
+# Settings without a section are, historically, ignored.
+host=256.256.256.256
+port=1
+unknown-setting=1
+
+[my_srv]
+});
 foreach my $param (split(/\s+/, $node->connstr))
 {
 	append_to_file($srvfile_valid, $param . "\n");
-- 
2.34.1

0003-libpq-Improve-unknown-keyword-message-for-pg_service.patchapplication/x-patch; name=0003-libpq-Improve-unknown-keyword-message-for-pg_service.patchDownload
From 20276a07bc1b7befa8c594ea2751f6497f18b70a Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Tue, 7 Oct 2025 15:07:51 -0700
Subject: [PATCH 3/6] libpq: Improve unknown-keyword message for
 pg_service.conf

Being told there's a syntax error in a file is a bit confusing if the
real problem is that the option name wasn't recognized.
---
 src/interfaces/libpq/fe-connect.c     |  3 ++-
 src/interfaces/libpq/t/006_service.pl | 19 +++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a3d12931fff..204f15787c9 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6153,7 +6153,8 @@ parseServiceFile(const char *serviceFile,
 				if (!found_keyword)
 				{
 					libpq_append_error(errorMessage,
-									   "syntax error in service file \"%s\", line %d",
+									   "unknown keyword \"%s\" in service file \"%s\", line %d",
+									   key,
 									   serviceFile,
 									   linenr);
 					result = 3;
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 9c692739511..6c1e7ec34ec 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -57,6 +57,14 @@ copy($srvfile_valid, $srvfile_nested)
   or die "Could not copy $srvfile_valid to $srvfile_nested: $!";
 append_to_file($srvfile_nested, "service=invalid_srv\n");
 
+# File with an unknown setting.
+my $srvfile_bad_keyword = "$td/pg_service_bad_keyword.conf";
+append_to_file(
+	$srvfile_bad_keyword, qq{
+[my_srv]
+bad-unknown-setting=1
+});
+
 # Service file with nested "servicefile" defined.
 my $srvfile_nested_2 = "$td/pg_service_nested_2.conf";
 copy($srvfile_valid, $srvfile_nested_2)
@@ -182,6 +190,17 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 	);
 }
 
+# Check behavior with unknown keywords.
+{
+	local $ENV{PGSERVICEFILE} = $srvfile_bad_keyword;
+
+	$dummy_node->connect_fails(
+		'service=my_srv',
+		'connection with unknown connection option in service',
+		expected_stderr =>
+		  qr/unknown keyword "bad-unknown-setting" in service file/);
+}
+
 # Properly escape backslashes in the path, to ensure the generation of
 # correct connection strings.
 my $srvfile_win_cared = $srvfile_valid;
-- 
2.34.1

0004-WIP-pg_service-implement-defaults-section.patchapplication/x-patch; name=0004-WIP-pg_service-implement-defaults-section.patchDownload
From 7f18db5f8cf1156a760dba948efc62838ac5d19c Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Tue, 7 Oct 2025 16:15:48 -0700
Subject: [PATCH 4/6] WIP: pg_service: implement defaults section

Default connection options may now be overridden by creating a section
at the top of a pg_service.conf file with the following header:

  [any-arbitrary-section-name]
  +=defaults
  host=...

System-level defaults (from the service file residing in PGSYSCONFDIR)
and user-level defaults (from the PGSERVICEFILE) are merged, with the
user-level options taking precedence.

With this change, connection options fall back in order of decreasing
precedence:

1) Connection string
2) User service
  2b) System service, but only if no user service was found
3) Environment variable
4) User default
5) System default
6) Compile-time default

Some code complexity here is due to the fact that the service name in
use may depend on the defaults. The new tests make use of this to ensure
that Test::Cluster environment variables are overridden as needed.
---
 src/interfaces/libpq/fe-connect.c     | 251 +++++++++++++++++++++-----
 src/interfaces/libpq/t/006_service.pl |  93 ++++++++++
 2 files changed, 303 insertions(+), 41 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 204f15787c9..4fce5f393e1 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -493,7 +493,8 @@ static PQconninfoOption *conninfo_find(PQconninfoOption *connOptions,
 									   const char *keyword);
 static void defaultNoticeReceiver(void *arg, const PGresult *res);
 static void defaultNoticeProcessor(void *arg, const char *message);
-static int	parseServiceInfo(PQconninfoOption *options,
+static int	parseServiceInfo(PQconninfoOption *defaults,
+							 PQconninfoOption *options,
 							 PQExpBuffer errorMessage);
 static int	parseServiceFile(const char *serviceFile,
 							 const char *service,
@@ -5916,7 +5917,8 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #endif							/* USE_LDAP */
 
 /*
- * parseServiceInfo: if a service name has been given, look it up and absorb
+ * parseServiceInfo: Parse any dynamic defaults from the service files.
+ * Additionally, if a service name has been given, look it up and absorb
  * connection options from it into *options.
  *
  * Returns 0 on success, nonzero on failure.  On failure, if errorMessage
@@ -5926,11 +5928,15 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options,
  * a null PQExpBuffer pointer.)
  */
 static int
-parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
+parseServiceInfo(PQconninfoOption *defaults, PQconninfoOption *options,
+				 PQExpBuffer errorMessage)
 {
 	const char *service = conninfo_getval(options, "service");
 	const char *service_fname = conninfo_getval(options, "servicefile");
-	char		serviceFile[MAXPGPATH];
+	char		userServiceFile[MAXPGPATH];
+	char		systemServiceFile[MAXPGPATH];
+	bool		have_user_file = false;
+	bool		have_system_file = false;
 	char	   *env;
 	bool		group_found = false;
 	int			status;
@@ -5939,64 +5945,114 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage)
 	/*
 	 * We have to special-case the environment variable PGSERVICE here, since
 	 * this is and should be called before inserting environment defaults for
-	 * other connection options.
+	 * other connection options, and it takes precedence over any default
+	 * service defined in the files.
 	 */
 	if (service == NULL)
 		service = getenv("PGSERVICE");
 
-	/* If no service name given, nothing to do */
-	if (service == NULL)
-		return 0;
-
 	/*
 	 * First, try the "servicefile" option in connection string.  Then, try
 	 * the PGSERVICEFILE environment variable.  Finally, check
 	 * ~/.pg_service.conf (if that exists).
 	 */
 	if (service_fname != NULL)
-		strlcpy(serviceFile, service_fname, sizeof(serviceFile));
+		strlcpy(userServiceFile, service_fname, sizeof(userServiceFile));
 	else if ((env = getenv("PGSERVICEFILE")) != NULL)
-		strlcpy(serviceFile, env, sizeof(serviceFile));
+		strlcpy(userServiceFile, env, sizeof(userServiceFile));
 	else
 	{
 		char		homedir[MAXPGPATH];
 
 		if (!pqGetHomeDirectory(homedir, sizeof(homedir)))
 			goto next_file;
-		snprintf(serviceFile, MAXPGPATH, "%s/%s", homedir, ".pg_service.conf");
-		if (stat(serviceFile, &stat_buf) != 0)
+		snprintf(userServiceFile, MAXPGPATH, "%s/%s", homedir, ".pg_service.conf");
+		if (stat(userServiceFile, &stat_buf) != 0)
 			goto next_file;
 	}
 
-	status = parseServiceFile(serviceFile, service, options, errorMessage, &group_found);
-	if (group_found || status != 0)
+	/*
+	 * Pull defaults out of the user file first, if one exists. They take
+	 * precedence over any defaults in the system file.
+	 */
+	status = parseServiceFile(userServiceFile, NULL, defaults, errorMessage, &group_found);
+	if (status != 0)
 		return status;
 
+	have_user_file = true;
+
 next_file:
 
 	/*
 	 * This could be used by any application so we can't use the binary
 	 * location to find our config files.
 	 */
-	snprintf(serviceFile, MAXPGPATH, "%s/pg_service.conf",
+	snprintf(systemServiceFile, MAXPGPATH, "%s/pg_service.conf",
 			 getenv("PGSYSCONFDIR") ? getenv("PGSYSCONFDIR") : SYSCONFDIR);
-	if (stat(serviceFile, &stat_buf) != 0)
+	if (stat(systemServiceFile, &stat_buf) != 0)
 		goto last_file;
 
-	status = parseServiceFile(serviceFile, service, options, errorMessage, &group_found);
+	/* Fill in system defaults for any options not given in the user file. */
+	status = parseServiceFile(systemServiceFile, NULL, defaults, errorMessage, &group_found);
 	if (status != 0)
 		return status;
 
+	have_system_file = true;
+
 last_file:
-	if (!group_found)
+
+	/*
+	 * At this point, we've filled in any dynamic defaults. We can make one
+	 * last attempt at a service name if none was given so far.
+	 */
+	if (service == NULL)
 	{
-		libpq_append_error(errorMessage, "definition of service \"%s\" not found", service);
-		return 3;
+		service = conninfo_getval(defaults, "service");
+		if (service == NULL)
+			return 0;			/* nothing left to do */
 	}
 
-	return 0;
+	/*
+	 * We have a service name. Try to find it first in the user file, then in
+	 * the system file.
+	 *
+	 * Note the difference when compared to defaults! The default sections in
+	 * the user and system files are *merged*, with the user file taking
+	 * precedence. But if a named user service is found, any identically named
+	 * system service is *ignored*.
+	 */
+	if (have_user_file)
+	{
+		status = parseServiceFile(userServiceFile, service, options, errorMessage, &group_found);
+		if (group_found || status != 0)
+			return status;
+	}
+
+	if (have_system_file)
+	{
+		status = parseServiceFile(systemServiceFile, service, options, errorMessage, &group_found);
+		if (group_found || status != 0)
+			return status;
+	}
+
+	libpq_append_error(errorMessage, "definition of service \"%s\" not found", service);
+	return 3;
 }
 
+/*
+ * Fills in option fallback values from a service file.
+ *
+ * When service is not NULL: The file is searched for the named service section.
+ * If one is found, we make sure it's not marked as the default section (which
+ * is an error) and then parse it.
+ *
+ * When service is NULL: The default section is parsed if one exists at the
+ * start of the file. Later sections are also checked to ensure that they are
+ * not incorrectly marked as defaults.
+ *
+ * Already-set options are never overwritten. *group_found is set to true if a
+ * matching section was found, and false otherwise.
+ */
 static int
 parseServiceFile(const char *serviceFile,
 				 const char *service,
@@ -6010,6 +6066,8 @@ parseServiceFile(const char *serviceFile,
 	FILE	   *f;
 	char	   *line;
 	char		buf[1024];
+	unsigned int section_num = 0;
+	unsigned int option_num = 0;
 
 	*group_found = false;
 
@@ -6052,13 +6110,17 @@ parseServiceFile(const char *serviceFile,
 		/* Check for right groupname */
 		if (line[0] == '[')
 		{
-			if (*group_found)
+			if (service != NULL && *group_found)
 			{
 				/* end of desired group reached; return success */
 				goto exit;
 			}
 
-			if (strncmp(line + 1, service, strlen(service)) == 0 &&
+			section_num++;
+			option_num = 0;
+
+			if (service != NULL &&
+				strncmp(line + 1, service, strlen(service)) == 0 &&
 				line[strlen(service) + 1] == ']')
 				*group_found = true;
 			else
@@ -6066,6 +6128,8 @@ parseServiceFile(const char *serviceFile,
 		}
 		else
 		{
+			option_num++;
+
 			if (*group_found)
 			{
 				/*
@@ -6076,7 +6140,12 @@ parseServiceFile(const char *serviceFile,
 				bool		found_keyword;
 
 #ifdef USE_LDAP
-				if (strncmp(line, "ldap", 4) == 0)
+
+				/*
+				 * LDAP lookup is allowed only in service definitions, not the
+				 * defaults section.
+				 */
+				if (service != NULL && strncmp(line, "ldap", 4) == 0)
 				{
 					int			rc = ldapServiceLookup(line, options, errorMessage);
 
@@ -6108,7 +6177,11 @@ parseServiceFile(const char *serviceFile,
 				}
 				*val++ = '\0';
 
-				if (strcmp(key, "service") == 0)
+				/*
+				 * A default service setting may be specified, but they're not
+				 * allowed to be nested inside other services.
+				 */
+				if (service != NULL && strcmp(key, "service") == 0)
 				{
 					libpq_append_error(errorMessage,
 									   "nested \"service\" specifications not supported in service file \"%s\", line %d",
@@ -6118,6 +6191,7 @@ parseServiceFile(const char *serviceFile,
 					goto exit;
 				}
 
+				/* Nested servicefile settings are never allowed. */
 				if (strcmp(key, "servicefile") == 0)
 				{
 					libpq_append_error(errorMessage,
@@ -6151,15 +6225,64 @@ parseServiceFile(const char *serviceFile,
 				}
 
 				if (!found_keyword)
+				{
+					/*
+					 * "unknown keyword" is unhelpful, if the actual problem
+					 * is that the user has tried to select the default
+					 * section.
+					 */
+					if (service != NULL
+						&& strcmp(key, "+") == 0
+						&& strcmp(val, "defaults") == 0)
+						libpq_append_error(errorMessage,
+										   "default section [%s] may not be named as a service in file \"%s\", line %d",
+										   service,
+										   serviceFile,
+										   linenr);
+					else
+						libpq_append_error(errorMessage,
+										   "unknown keyword \"%s\" in service file \"%s\", line %d",
+										   key,
+										   serviceFile,
+										   linenr);
+					result = 3;
+					goto exit;
+				}
+			}
+			else if (section_num > 0 && option_num == 1)
+			{
+				/*
+				 * Check for the default marker. We allow only the first
+				 * section in the file to be the default section, and it must
+				 * contain the marker "+=defaults" as its first key/value
+				 * pair.
+				 *
+				 * We don't currently allow whitespace around keys and values,
+				 * so we can perform a single strcmp().
+				 */
+				if (strcmp(line, "+=defaults") != 0)
+					continue;
+
+				/*
+				 * Avoid user confusion and complain if the default marker is
+				 * anywhere other than the first section.
+				 */
+				if (section_num != 1)
 				{
 					libpq_append_error(errorMessage,
-									   "unknown keyword \"%s\" in service file \"%s\", line %d",
-									   key,
+									   "only the first section may be marked default in service file \"%s\", line %d",
 									   serviceFile,
 									   linenr);
 					result = 3;
 					goto exit;
 				}
+
+				/*
+				 * If we're not looking for a service, parse this defaults
+				 * section.
+				 */
+				if (service == NULL)
+					*group_found = true;
 			}
 		}
 	}
@@ -6171,7 +6294,7 @@ exit:
 	 * if not already set.  This matters when we use a default service file or
 	 * PGSERVICEFILE, where we want to be able track the value.
 	 */
-	if (*group_found && result == 0)
+	if (service != NULL && *group_found && result == 0)
 	{
 		for (i = 0; options[i].keyword; i++)
 		{
@@ -6666,23 +6789,40 @@ static bool
 conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 {
 	PQconninfoOption *option;
+	PQconninfoOption *defaults;
+	PQconninfoOption *defopt;
+	PQExpBufferData unused = {0};
 	PQconninfoOption *sslmode_default = NULL,
 			   *sslrootcert = NULL;
 	char	   *tmp;
+	bool		success = false;
 
 	/*
-	 * If there's a service spec, use it to obtain any not-explicitly-given
-	 * parameters.  Ignore error if no error message buffer is passed because
-	 * there is no way to pass back the failure message.
+	 * Create a parallel set of options to hold dynamic defaults.
+	 * Unfortunately this requires constructing a throwaway error buffer if
+	 * the caller didn't provide one.
 	 */
-	if (parseServiceInfo(options, errorMessage) != 0 && errorMessage)
+	if (errorMessage == NULL)
+		termPQExpBuffer(&unused);	/* make buffer operations no-ops */
+
+	defaults = conninfo_init(errorMessage ? errorMessage : &unused);
+	if (defaults == NULL)
 		return false;
 
+	/*
+	 * Fill in any dynamic defaults, and if there's a service spec, use it to
+	 * obtain any not-explicitly-given parameters. Ignore error if no error
+	 * message buffer is passed because there is no way to pass back the
+	 * failure message.
+	 */
+	if (parseServiceInfo(defaults, options, errorMessage) != 0 && errorMessage)
+		goto cleanup;
+
 	/*
 	 * Get the fallback resources for parameters not specified in the conninfo
 	 * string nor the service.
 	 */
-	for (option = options; option->keyword != NULL; option++)
+	for (option = options, defopt = defaults; option->keyword != NULL; option++, defopt++)
 	{
 		if (strcmp(option->keyword, "sslrootcert") == 0)
 			sslrootcert = option;	/* save for later */
@@ -6702,7 +6842,7 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 				{
 					if (errorMessage)
 						libpq_append_error(errorMessage, "out of memory");
-					return false;
+					goto cleanup;
 				}
 				continue;
 			}
@@ -6725,7 +6865,7 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 				{
 					if (errorMessage)
 						libpq_append_error(errorMessage, "out of memory");
-					return false;
+					goto cleanup;
 				}
 				continue;
 			}
@@ -6739,8 +6879,33 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 		}
 
 		/*
-		 * No environment variable specified or the variable isn't set - try
-		 * compiled-in default
+		 * No environment variable specified or the variable isn't set. First
+		 * try a dynamic default from pg_service.
+		 *
+		 * We iterate over the defaults array in lockstep to avoid a lookup
+		 * here, so check that the option order has remained static.
+		 */
+		if (option->keyword != defopt->keyword)
+		{
+			if (errorMessage)
+				libpq_append_error(errorMessage,
+								   "defaults array is out of order");
+			goto cleanup;
+		}
+		else if (defopt->val != NULL)
+		{
+			option->val = strdup(defopt->val);
+			if (!option->val)
+			{
+				if (errorMessage)
+					libpq_append_error(errorMessage, "out of memory");
+				goto cleanup;
+			}
+			continue;
+		}
+
+		/*
+		 * Fall back to the compiled-in default.
 		 */
 		if (option->compiled != NULL)
 		{
@@ -6749,7 +6914,7 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 			{
 				if (errorMessage)
 					libpq_append_error(errorMessage, "out of memory");
-				return false;
+				goto cleanup;
 			}
 			continue;
 		}
@@ -6784,12 +6949,16 @@ conninfo_add_defaults(PQconninfoOption *options, PQExpBuffer errorMessage)
 			{
 				if (errorMessage)
 					libpq_append_error(errorMessage, "out of memory");
-				return false;
+				goto cleanup;
 			}
 		}
 	}
 
-	return true;
+	success = true;
+
+cleanup:
+	PQconninfoFree(defaults);
+	return success;
 }
 
 /*
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 6c1e7ec34ec..0826add30fd 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -118,6 +118,99 @@ local $ENV{PGSERVICEFILE} = "$srvfile_empty";
 		  qr/definition of service "undefined-service" not found/);
 }
 
+{
+	# Check handling of the defaults section.
+	#
+	# pg_service_defaults.conf contains the same parameters as srvfile_valid,
+	# but it uses a default section to select the service automatically. (Use of
+	# a service remains necessary, to take precedence over Test::Cluster's
+	# automatic envvars.)
+	my $srvfile_defaults = "$td/pg_service_defaults.conf";
+	append_to_file(
+		$srvfile_defaults, qq{
+# Settings before the default section must be ignored.
+host=256.256.256.256
+port=1
+unknown-setting=1
+
+[default]
++=defaults
+service=my_srv
+options=-O
+
+[my_srv]
+});
+	foreach my $param (split(/\s+/, $node->connstr))
+	{
+		append_to_file($srvfile_defaults, $param . "\n");
+	}
+
+	local $ENV{PGSERVICEFILE} = $srvfile_defaults;
+	$dummy_node->connect_ok(
+		'',
+		'connection with dynamic defaults in PGSERVICEFILE',
+		sql => 'SHOW allow_system_table_mods',
+		expected_stdout => qr/on/);
+
+	# TODO is it really okay that postgres:// means whatever the environment
+	# says it means...?
+	$dummy_node->connect_ok(
+		'postgres://',
+		'connection with empty URI, dynamic defaults, and PGSERVICEFILE',
+		sql => 'SHOW allow_system_table_mods',
+		expected_stdout => qr/on/);
+
+	$dummy_node->connect_fails(
+		'service=default',
+		'default sections may not be selected via connection parameter',
+		expected_stderr =>
+		  qr/default section \[default\] may not be named as a service/);
+
+	{
+		local $ENV{PGSERVICE} = 'default';
+		$dummy_node->connect_fails(
+			'',
+			'default sections may not be selected via PGSERVICE',
+			expected_stderr =>
+			  qr/default section \[default\] may not be named as a service/);
+	}
+
+	# A file containing more than one default section is rejected.
+	my $srvfile_too_many_defaults = "$td/pg_service_too_many_defaults.conf";
+	copy($srvfile_defaults, $srvfile_too_many_defaults)
+	  or die
+	  "Could not copy $srvfile_defaults to $srvfile_too_many_defaults: $!";
+	append_to_file(
+		$srvfile_too_many_defaults, qq{
+[default]
++=defaults
+});
+
+	local $ENV{PGSERVICEFILE} = $srvfile_too_many_defaults;
+	$dummy_node->connect_fails(
+		'',
+		'service files may not contain more than one default section',
+		expected_stderr => qr/only the first section may be marked default/);
+
+	# A default section may not come after the first service section.
+	my $srvfile_defaults_after_service =
+	  "$td/pg_service_defaults_after_service.conf";
+	copy($srvfile_valid, $srvfile_defaults_after_service)
+	  or die
+	  "Could not copy $srvfile_valid to $srvfile_defaults_after_service: $!";
+	append_to_file(
+		$srvfile_defaults_after_service, qq{
+[default]
++=defaults
+});
+
+	local $ENV{PGSERVICEFILE} = $srvfile_defaults_after_service;
+	$dummy_node->connect_fails(
+		'',
+		'defaults section must be first in the file',
+		expected_stderr => qr/only the first section may be marked default/);
+}
+
 # Checks case of incorrect service file.
 {
 	local $ENV{PGSERVICEFILE} = $srvfile_missing;
-- 
2.34.1

0005-WIP-pg_service-implement-forwards-compatibility.patchapplication/x-patch; name=0005-WIP-pg_service-implement-forwards-compatibility.patchDownload
From 3b3b5e9b7ad709a03c5b704ed74b9bfcf007380c Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 10 Oct 2025 11:53:49 -0700
Subject: [PATCH 5/6] WIP: pg_service: implement forwards compatibility

To allow defaults from multiple major versions of libpq to coexist in
the PGSERVICEFILE, add the ability to ignore unknown keywords in the
defaults section by prefixing them with '?':

  [my-defaults-section]
  +=defaults
  ?amazing_pg30_feature=on
  sslrootcert=system
  sslmode=verify-full
  ...
---
 src/interfaces/libpq/fe-connect.c     | 13 ++++++++++++-
 src/interfaces/libpq/t/006_service.pl |  1 +
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4fce5f393e1..4fbaf4727ef 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -6138,6 +6138,7 @@ parseServiceFile(const char *serviceFile,
 				char	   *key,
 						   *val;
 				bool		found_keyword;
+				bool		skip_unknown = false;
 
 #ifdef USE_LDAP
 
@@ -6177,6 +6178,16 @@ parseServiceFile(const char *serviceFile,
 				}
 				*val++ = '\0';
 
+				/*
+				 * Inside a defaults section, unknown options may be marked as
+				 * skippable by the user for forwards compatibility purposes.
+				 */
+				if (service == NULL && key[0] == '?')
+				{
+					skip_unknown = true;
+					key++;
+				}
+
 				/*
 				 * A default service setting may be specified, but they're not
 				 * allowed to be nested inside other services.
@@ -6224,7 +6235,7 @@ parseServiceFile(const char *serviceFile,
 					}
 				}
 
-				if (!found_keyword)
+				if (!found_keyword && !skip_unknown)
 				{
 					/*
 					 * "unknown keyword" is unhelpful, if the actual problem
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 0826add30fd..973035aac9b 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -137,6 +137,7 @@ unknown-setting=1
 +=defaults
 service=my_srv
 options=-O
+?unknown-setting=1  # should be ignored
 
 [my_srv]
 });
-- 
2.34.1

0006-WIP-pg_service-implement-PGNODEFAULTS.patchapplication/x-patch; name=0006-WIP-pg_service-implement-PGNODEFAULTS.patchDownload
From 7ccf7d7074ce34b61835d3c74acba853c9a394d3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 10 Oct 2025 15:56:02 -0700
Subject: [PATCH 6/6] WIP: pg_service: implement PGNODEFAULTS

Tests need to ensure that they have full control of the default
connection state. While PGSYSCONFDIR was already redirected as needed,
the user's config file would still be consulted when PGSERVICEFILE was
empty. Add a PGNODEFAULTS envvar to turn off the previous feature, and
make use of it in pg_regress and Test::Cluster connections.
---
 src/interfaces/libpq/fe-connect.c      | 33 +++++++++++++++++---------
 src/interfaces/libpq/t/006_service.pl  | 10 ++++++++
 src/test/perl/PostgreSQL/Test/Utils.pm |  3 +++
 src/test/regress/pg_regress.c          |  9 +++++++
 4 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 4fbaf4727ef..000001de05d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -5935,6 +5935,7 @@ parseServiceInfo(PQconninfoOption *defaults, PQconninfoOption *options,
 	const char *service_fname = conninfo_getval(options, "servicefile");
 	char		userServiceFile[MAXPGPATH];
 	char		systemServiceFile[MAXPGPATH];
+	bool		load_defaults = true;
 	bool		have_user_file = false;
 	bool		have_system_file = false;
 	char	   *env;
@@ -5951,6 +5952,10 @@ parseServiceInfo(PQconninfoOption *defaults, PQconninfoOption *options,
 	if (service == NULL)
 		service = getenv("PGSERVICE");
 
+	/* PGNODEFAULTS=1 disables the use of defaults sections. */
+	if ((env = getenv("PGNODEFAULTS")) != NULL && strcmp(env, "1") == 0)
+		load_defaults = false;
+
 	/*
 	 * First, try the "servicefile" option in connection string.  Then, try
 	 * the PGSERVICEFILE environment variable.  Finally, check
@@ -5971,13 +5976,16 @@ parseServiceInfo(PQconninfoOption *defaults, PQconninfoOption *options,
 			goto next_file;
 	}
 
-	/*
-	 * Pull defaults out of the user file first, if one exists. They take
-	 * precedence over any defaults in the system file.
-	 */
-	status = parseServiceFile(userServiceFile, NULL, defaults, errorMessage, &group_found);
-	if (status != 0)
-		return status;
+	if (load_defaults)
+	{
+		/*
+		 * Pull defaults out of the user file first, if one exists. They take
+		 * precedence over any defaults in the system file.
+		 */
+		status = parseServiceFile(userServiceFile, NULL, defaults, errorMessage, &group_found);
+		if (status != 0)
+			return status;
+	}
 
 	have_user_file = true;
 
@@ -5992,10 +6000,13 @@ next_file:
 	if (stat(systemServiceFile, &stat_buf) != 0)
 		goto last_file;
 
-	/* Fill in system defaults for any options not given in the user file. */
-	status = parseServiceFile(systemServiceFile, NULL, defaults, errorMessage, &group_found);
-	if (status != 0)
-		return status;
+	if (load_defaults)
+	{
+		/* Fill in system defaults for any options not given in the user file. */
+		status = parseServiceFile(systemServiceFile, NULL, defaults, errorMessage, &group_found);
+		if (status != 0)
+			return status;
+	}
 
 	have_system_file = true;
 
diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl
index 973035aac9b..cbb0b2d1c46 100644
--- a/src/interfaces/libpq/t/006_service.pl
+++ b/src/interfaces/libpq/t/006_service.pl
@@ -146,6 +146,8 @@ options=-O
 		append_to_file($srvfile_defaults, $param . "\n");
 	}
 
+	# Test::Utils will have disabled dynamic defaults.
+	local $ENV{PGNODEFAULTS} = "0";
 	local $ENV{PGSERVICEFILE} = $srvfile_defaults;
 	$dummy_node->connect_ok(
 		'',
@@ -210,6 +212,14 @@ options=-O
 		'',
 		'defaults section must be first in the file',
 		expected_stderr => qr/only the first section may be marked default/);
+
+	{
+		# Re-enable PGNODEFAULTS and check that we can continue using the
+		# working service, even though the defaults section is broken.
+		local $ENV{PGNODEFAULTS} = "1";
+		$dummy_node->connect_ok('service=my_srv',
+			'connection with bad defaults section, but PGNODEFAULTS=1');
+	}
 }
 
 # Checks case of incorrect service file.
diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm
index 85d36a3171e..3f27f315a4d 100644
--- a/src/test/perl/PostgreSQL/Test/Utils.pm
+++ b/src/test/perl/PostgreSQL/Test/Utils.pm
@@ -111,6 +111,9 @@ BEGIN
 	$ENV{LC_NUMERIC} = 'C';
 	setlocale(LC_ALL, "");
 
+	# Disable any defaults coming from pg_service.conf.
+	$ENV{PGNODEFAULTS} = "1";
+
 	# This list should be kept in sync with pg_regress.c.
 	my @envkeys = qw (
 	  PGCHANNELBINDING
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index 61c035a3983..c8b424a8336 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -728,6 +728,15 @@ initialize_environment(void)
 	 */
 	setenv("PGAPPNAME", "pg_regress", 1);
 
+	/*
+	 * Disable any defaults coming from pg_service.conf, which would thwart our
+	 * unsetenv()s below.
+	 *
+	 * TODO this would need to be documented for installcheck: only environment
+	 * variables can be used to point to the system under test
+	 */
+	setenv("PGNODEFAULTS", "1", 1);
+
 	/*
 	 * Set variables that the test scripts may need to refer to.
 	 */
-- 
2.34.1

#23Chao Li
li.evan.chao@gmail.com
In reply to: Jacob Champion (#22)
Re: Thoughts on a "global" client configuration?

On Oct 16, 2025, at 07:19, Jacob Champion <jacob.champion@enterprisedb.com> wrote:

I wanted to avoid stomping on existing service names. I could have
gone the Windows route and generated a GUID or something, but instead
I've allowed the user to choose any name they want for this section.
They then mark it with the (maybe-too-cute?) magic string of
"+=defaults", which
1) will cause earlier libpqs to fail if they accidentally try to
select the section as a service,
2) is INI parseable (the key is "+"), and
3) kind of suggests what the section is meant to do: add these
settings to the defaults.

I don’t have a strong option on the direction, so I was watching the discussion and waiting for the patch.

Before reviewing the patch, I have a comment to the design.

I am not convinced that the “=+defaults” approach is the best design.

1) Not self-documenting. Without documentation, hard to guess what it is exactly for.
2) Actually “default = true” will also causes earlier libpq to fail, but “default = true” is easier to understand.

Something I want to clarify are:

* If there are multiple default sections present, how will them be handled?
* Do we want to support specifying a default section? For example:

```
[test_default]
default = true
X = Y

[prod_default]
default = true
X = Z

[service_a]
default = test_default

[service_b]
default = prod_default
```

Best regards,
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

#24Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Chao Li (#23)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 15, 2025 at 7:16 PM Chao Li <li.evan.chao@gmail.com> wrote:

I am not convinced that the “=+defaults” approach is the best design.

1) Not self-documenting. Without documentation, hard to guess what it is exactly for.
2) Actually “default = true” will also causes earlier libpq to fail, but “default = true” is easier to understand.

Yeah, I can talk a bit about my personal flip-flopping there.
- I started with a "default=true" marker, and decided I didn't want to
camp on something that could plausibly be a connection string option.
- I switched to "!default=true!" and decided it was ugly.
- Once I realized the subtlety of the merge vs replace behavior
compared to services, I tried "+=", to try to evoke the merge idea.

*If* we decide to support a configuration file, *and* we decide to
choose the section+marker approach, I think the choice of marker would
be a good thing to bikeshed. Something fairly self-documenting would
be ideal. But the first two "ifs" are far from decided.

Something I want to clarify are:

* If there are multiple default sections present, how will them be handled?

This patchset rejects that (if I've implemented it correctly). One
default section only.

* Do we want to support specifying a default section?

This is similar to Isaac's service nesting proposal. I'm not excited
about mixing the defaults section behavior with the service sections,
because I think it would confuse users about which options take
precedence when.

At the moment, defaults defer to envvars, which defer to services. But
a "default selection" feature doesn't "look" like it works that way,
at least not to my eyes. And I'm not sure if users would ever want the
ability to switch defaults per service (as opposed to just putting
those options into the service). Isaac's nested services seem like a
better way to go if organization and deduplication is the goal.

Thanks!
--Jacob

#25Peter Eisentraut
peter@eisentraut.org
In reply to: Jacob Champion (#22)
Re: Thoughts on a "global" client configuration?

On 16.10.25 01:19, Jacob Champion wrote:

I'm not sure if all this is better than an architecture where the
defaults and services are contained in different files. Since the
syntax and behavior of the two types of sections is explicitly
different, maybe combining them would be unnecessarily confusing for
users?

After studying this a bit more and reading your account, I'm now
coming over to the side that a libpq defaults configuration file
should be separate from the existing services file mechanism.

First, just to have a clear naming. If someone wants to know where to
set the default SSL version, an answer like
/etc/postgresql/pg_service.conf doesn't make much sense.

The two mechanisms have different audiences. Currently, a
pg_service.conf entry is necessarily made in conjunction with some
application program using it. I suspect a system-wide pg_service.conf
file doesn't actually get much use. Conversely, a libpq defaults file
is more the concern of the OS admin, and per-user configuration will
be less common.

The hierarchy of the settings from different sources becomes very
complicated, as you already alluded to. A user-configured service
will completely override a system-configured service, which makes some
sense, but libpq defaults should merge on a per-parameter level.
Also, I would expect that environment variables override libpq
defaults, but with the services, it's the other way around.
Consolidating all of that into one mechanism would be very confusing.
If we have separate mechanisms with a clear hierarchy, like 0)
compiled-in, 1) defaults file, 2) environment vars, 3) services, 4)
explicit parameters, then it's clearer.

And then you could also have different policies in the two kinds of
files, like ignoring unknown settings in the defaults file, if that
kind of thing were desired.

We could still have the defaults file and the services file use the
same format and parser, if that helps.

On the question of ignoring unknown settings, or related compatibility
questions. The core use case is altering the compiled-in defaults and
giving users a way to effectively revert that. So ideally in most
cases it won't get used at all. And normally you won't change the
defaults of settings that were only recently introduced. So you might
want to change the default of sslmode, but all versions in the field
support that. Hypothetically, you might want to change the default of
sslnegotiation someday, but if not all libpq versions in the field
support that setting, then it's probably also too soon to change the
default. So perhaps this problem regulates itself. Also, generally,
you will only have one libpq version per system, so supporting many
versions concurrently might not be needed.

To that end, it would be important not to get this facility confused
with a user preferences facility, or a per-remote-host configuration
(like ssh_config). Maybe people want that too, but it should be
designed differently.

In fact, do we even need a per-user version of this? Maybe take the
OpenSSL approach: There is only a global config file, and you can
supply a different one with an environment variable. That should
satisfy those who want different defaults for their local psql use.
But for most other uses, the relevant user is some service account,
for which dot files are always awkward anyway.

#26Bruce Momjian
bruce@momjian.us
In reply to: Christoph Berg (#13)
Re: Thoughts on a "global" client configuration?

On Mon, Oct 13, 2025 at 10:22:53PM +0200, Christoph Berg wrote:

Re: Robert Haas

My theory is that they'll be even less impressed if they try to use a
supposedly-compatible library and it breaks a bunch of stuff, but I
wonder what Christoph Berg (cc'd) thinks.

It would also hinder adoption of PG in more places. There are
currently thousands of software products that link to libpq in some
form, and it would take several years to have them all fixed if
ABI/API compatibility were broken. Chasing the long tail there is
hard; we get to witness that every year with upstreams that aren't
compatible with PG18 yet. For some extensions, I'm still waiting to
get my PG17 (or PG16!) patches merged.

The fact is is called libpq --- Post-QUEL, and not libpg, supports your
analysis. ;-)

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Do not let urgent matters crowd out time for investment in the future.

#27Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Peter Eisentraut (#25)
Re: Thoughts on a "global" client configuration?

On Wed, Oct 29, 2025 at 7:20 AM Peter Eisentraut <peter@eisentraut.org> wrote:

After studying this a bit more and reading your account, I'm now
coming over to the side that a libpq defaults configuration file
should be separate from the existing services file mechanism.

I think I agree, for all the reasons you cited. I'll work on a second draft.

Conversely, a libpq defaults file
is more the concern of the OS admin, and per-user configuration will
be less common.

I'm not sure I agree with this. Unlike with services, an admin might
have good reason to pull the defaults up system-wide, _and_ a user
might want to further override them for clients under their control,
rather than messing with services or asking for root privileges. I
view it mostly as a matter of scope.

We could still have the defaults file and the services file use the
same format and parser, if that helps.

I'd actually like to strengthen it a bit, if we can afford to. The
service parser is lax in a bunch of ways, and stricter than necessary
in others (no spaces allowed for formatting?).

On the question of ignoring unknown settings, or related compatibility
questions. The core use case is altering the compiled-in defaults and
giving users a way to effectively revert that. So ideally in most
cases it won't get used at all.

Sure, but see my earlier response to Robert on ecosystem effects. If
this feature is a break-glass solution for a minority of users, we had
better make sure it gets them out of the emergency situation without
breaking more things.

Hypothetically, you might want to change the default of
sslnegotiation someday, but if not all libpq versions in the field
support that setting, then it's probably also too soon to change the
default. So perhaps this problem regulates itself.

Maybe... but if the community were to reach a consensus that a default
change is needed sooner (for any setting), I would hate for there to
be a mechanical reason that we couldn't. Let that be a matter of
maintainer policy rather than parser compatibility.

Also, generally,
you will only have one libpq version per system, so supporting many
versions concurrently might not be needed.

For Deb-alikes, yes, but I don't think that's true for our RPMs.

In fact, do we even need a per-user version of this? Maybe take the
OpenSSL approach: There is only a global config file, and you can
supply a different one with an environment variable. That should
satisfy those who want different defaults for their local psql use.

I didn't like this idea at first due to the lack of merge capability,
but I'm slowly coming around to it. Maybe no one really needs a
two-tier solution.

Thanks,
--Jacob