Support user-supplied pg_hba rules via configSecret key user_hba.conf - #68
Conversation
pg_hba.conf is regenerated from scratch by these scripts on every pod
start, so users have had no way to add authentication rules -- and
because pg_hba.conf is first-match-wins, appending rules at the end
(the natural analogue of postgresql.conf's trailing include_if_exists,
which is last-wins) would leave them dead behind the generated
"host all all 0.0.0.0/0" catch-alls.
Instead, rules from /etc/config/user_hba.conf (projected by the
operator from the Postgres configSecret) are injected into every
generation branch at a fixed position: after the local/loopback and
loopback-replication rules the operator's own scripts and sidecars
depend on, and before the world-CIDR catch-alls. User rules can
therefore override the catch-alls (e.g. reject the postgres role from
outside the pod network) but cannot lock the operator out of the unix
socket or loopback.
The mechanism is version-gated, verified empirically on both paths:
* PostgreSQL >= 16 emits
include_if_exists "/etc/config/user_hba.conf"
so a secret update takes effect on pg_reload_conf() with no
restart, mirroring how user.conf already behaves for GUCs. A
missing file is tolerated at startup and reload. The path is
double-quoted deliberately: a single-quoted token is treated as a
literal filename (quotes included) and include_if_exists then
skips it silently.
* PostgreSQL <= 15 has no include support in pg_hba.conf -- a
include line is FATAL at startup ("could not load pg_hba.conf").
The file content is copied in at generation time instead, so
changes take effect on pod restart.
Only long-running server paths are changed: primary/start.sh,
standby/run.sh and standby/remote-replica.sh. warm_stanby.sh,
ha_backup_job.sh and restore.sh also generate pg_hba.conf but run
postgres only for seconds during bootstrap/restore with local access;
injecting user rules there risks breaking recovery for no security
gain.
Verified with a harness that executes the generation section of every
modified script and its master counterpart under all five
SSL/CLIENT_AUTH_MODE combinations, with and without the user file
(260 checks): without the file, <= 15 output is byte-identical to
master and >= 16 differs by exactly the include line; with the file,
the rules appear exactly once, after the essentials and before the
first catch-all, in every branch.
Note for users: rules targeting the postgres role over the pod network
can break HA -- pg-coordinator connects to peer pods as postgres with
replication=database, which matches ordinary host rules, not the
replication keyword. Restricting postgres to the pod CIDR before a
wider reject is the safe pattern; the docs guide carries the recipe.
Signed-off-by: Tamal Saha <tamal@appscode.com>
|
No changes needed here — this PR is complete as it stands, and I verified it end to end while Worth recording what the split in this PR actually buys, because the operator side now depends on
That boundary is exactly where PostgreSQL gained include support inside Verified on a k3s cluster with an image built from this branch ( Also confirmed the primary's first |
Users have had no way to add
pg_hba.confrules: the role scripts regenerate the file from scratch on every pod start, and sincepg_hba.confis first-match-wins, appending at the end (the analogue ofpostgresql.conf's trailinginclude_if_exists, which is last-wins) would leave user rules dead behind the generatedhost all all 0.0.0.0/0catch-alls.This splices
/etc/config/user_hba.conf(projected by the operator from the Postgres configSecret) into every generation branch at a fixed position: after the local/loopback and loopback-replication rules the operator's own scripts and sidecars depend on, and before the world-CIDR catch-alls. User rules can override the catch-alls — e.g. reject thepostgresrole from outside the pod network — but cannot lock the operator out.Version-gated mechanism (both paths verified empirically):
include_if_exists "/etc/config/user_hba.conf"— secret updates apply onpg_reload_conf(), no restart; missing file tolerated at startup and reload. The double quotes are load-bearing: a single-quoted token is treated as a literal filename and skipped silently.FATAL: could not load pg_hba.confat startup (verified on 15-alpine), so the file content is concatenated at generation time; changes apply on pod restart.Scope:
primary/start.sh,standby/run.sh,standby/remote-replica.sh(long-running servers).warm_stanby.sh/ha_backup_job.sh/restore.share deliberately untouched — they run postgres for seconds during bootstrap/restore, where a user rule breaking recovery costs more than a seconds-long window of default rules protects.Verification: a harness executed the generation section of all 26 modified scripts and their master counterparts under all five SSL/CLIENT_AUTH_MODE combos, with and without the user file — 260/260:
Companion PRs: kubedb/apimachinery (constant), kubedb/postgres (projects the key), kubedb/docs (guide, including the pod-CIDR-allow-before-reject pattern that keeps pg-coordinator's peer connections as
postgresworking).