Feat: Allow certs for PostgreSQL connections#416
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens PostgreSQL connectivity for the DocumentDB Kubernetes Operator by tightening default pg_hba rules and introducing an API surface (spec.tls.postgres) to optionally supply CloudNative-PG PostgreSQL server/replication certificate Secrets—supporting mTLS for cross-cluster replication while keeping existing gateway loopback behavior.
Changes:
- Replace permissive PostgreSQL
pg_hbaentries with least-privilege defaults and replication rules (streaming_replica+ cert/TLS-aware behavior). - Introduce
spec.tls.postgresbacked by CloudNative-PGCertificatesConfiguration, with CRD CEL validation to enforce secret pairing invariants. - Propagate certificate configuration into CNPG Cluster spec (including patch/diff support) and update multi-region replication connection parameters to use TLS client certs when provided; update docs/playground and tests accordingly.
Reviewed changes
Copilot reviewed 22 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| operator/src/internal/controller/physical_replication.go | Builds external cluster connection parameters with optional TLS client cert/root CA and updates replication patch logic. |
| operator/src/internal/controller/physical_replication_test.go | Adds tests covering external cluster detail patching and Postgres cert-related replication behavior. |
| operator/src/internal/controller/certificate_controller_test.go | Extends cert controller tests to ensure Postgres cert resources are not managed/created unexpectedly. |
| operator/src/internal/controller/backup_controller_test.go | Test formatting/indentation update (no behavioral change). |
| operator/src/internal/cnpg/cnpg_sync.go | Adds CNPG Cluster patching support for /spec/certificates changes. |
| operator/src/internal/cnpg/cnpg_patch.go | Adds JSON patch path constants for certificates and pg_hba. |
| operator/src/internal/cnpg/cnpg_cluster.go | Sets CNPG spec.certificates from spec.tls.postgres and hardens default pg_hba. |
| operator/src/internal/cnpg/cnpg_cluster_test.go | Updates/extends tests for Postgres certificate config presence and pg_hba expectations. |
| operator/src/config/crd/bases/documentdb.io_dbs.yaml | Updates generated CRD schema for spec.tls.postgres and adds CEL validation rule. |
| operator/src/api/preview/zz_generated.deepcopy.go | Regenerates deep-copies to support CNPG CertificatesConfiguration on TLS spec. |
| operator/src/api/preview/documentdb_types.go | Replaces placeholder PostgresTLS with CNPG CertificatesConfiguration + XValidation. |
| operator/documentdb-helm-chart/crds/documentdb.io_dbs.yaml | Mirrors CRD schema changes for Helm-managed CRDs. |
| documentdb-playground/aks-fleet-deployment/documentdb-resource-crp.yaml | Adds cert-manager resources and fleet Secret propagation for cross-region replication TLS. |
| documentdb-playground/aks-fleet-deployment/documentdb-operator-crp.yaml | Adjusts fleet placement affinity for operator resources. |
| documentdb-playground/aks-fleet-deployment/deploy-multi-region.sh | Adds SAN computation + more robust apply logic with webhook readiness/retries. |
| documentdb-playground/aks-fleet-deployment/deploy-fleet-bicep.sh | Hardens shell settings/quoting and pins external repos to tags; adds RBAC bindings and hub CA handling. |
| docs/operator-public-documentation/preview/multi-region-deployment/setup.md | Documents replication TLS options and SAN requirements; updates backup link. |
| docs/operator-public-documentation/preview/multi-region-deployment/overview.md | Updates replication traffic guidance to reference spec.tls.postgres behavior. |
| docs/operator-public-documentation/preview/getting-started/deploy-on-aks.md | Fixes/updates doc links for storage and TLS configuration. |
| docs/operator-public-documentation/preview/configuration/tls.md | Adds PostgreSQL certificate configuration documentation and examples. |
| docs/operator-public-documentation/preview/architecture/overview.md | Updates backup doc link location. |
| docs/operator-public-documentation/preview/api-reference.md | Updates API reference to reflect CertificatesConfiguration and removes placeholder PostgresTLS. |
| CHANGELOG.md | Adds security and feature entries describing pg_hba hardening and new Postgres TLS support. |
Files not reviewed (1)
- operator/src/api/preview/zz_generated.deepcopy.go: Generated file
|
🤖 Auto-triaged by documentdb-triage-tool. Applied: Reasoningcomponent from path globs (docs, controllers, playground, test, api, manifests); P0 keyword "security" in body; effort from diff stats (1190+172 LOC, 24 files) If a label is wrong, remove it manually and ping |
7fb40af to
2cb5c35
Compare
|
Thanks for tackling replication TLS — the hardened 1. Is
|
WentingWu666666
left a comment
There was a problem hiding this comment.
Marking this as request-changes — not because I think any specific answer is required, but because these four points concern the public API surface (hard to change once released) and the security posture of cross-region replication, so I'd like us to work through them deliberately before merging rather than after.
I've laid out the details and rationale in my earlier comment. The four things I'd ask you to think carefully about:
- Naming — does
spec.tls.postgresdescribe the real scope (replication only), or could it imply app→DB coverage it doesn't provide? - CNPG type leakage — is exposing
cnpgv1.CertificatesConfigurationdirectly in our CRD a coupling we want to commit to long-term? verify-fullguarantee — is thesslmode=requirepartial-cert path (MITM-exposed) something we should ship and call "mTLS," or should the two secrets be required as a pair?- Silent plaintext default — is the cert-less
trustfallback (and its Istio-mesh assumption) safe as an implicit default, or should it be an explicit opt-in?
No rush to agree with my leanings — I'd just like your reasoning on each before we lock the API in. Happy to pair or prototype if that helps move it forward.
Duplicate — superseded by the request-changes review directly below.
|
To address your questions from the comment above:
|
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
Signed-off-by: Alexander Laye <alaye@microsoft.com>
c1eac3b to
ec2c173
Compare
Signed-off-by: Alexander Laye <alaye@microsoft.com>
ec2c173 to
5602e23
Compare
Signed-off-by: Alexander Laye <alaye@microsoft.com>
WentingWu666666
left a comment
There was a problem hiding this comment.
Approving this — the functionality is correct and the security items from my earlier review (the mTLS wording and the explicit disableTLS opt-in) are addressed. Nice work. I have two non-blocking things I'd love the team to keep in mind.
1. Let's rethink the tls.postgres shape while it's still easy to change.
You mentioned tls.postgres was already there as a placeholder, so renaming feels out of scope. I'd gently flip that around: because it was only an empty placeholder before this PR, this PR is the first time it gets a real shape and real fields. Nobody depends on it yet, so right now is the cheapest moment to get the name and structure right — changing it later, once people use it, is a breaking change.
Two things I'd love you to consider:
-
It doesn't match our own conventions. Most of our API is DocumentDB-native — we rarely expose CNPG types directly.
tls.postgresis an exception: it passes CNPG'sCertificatesConfigurationstraight through. It's also different from its siblingtls.gateway, which already uses a clean, native shape. And DocumentDB isn't CNPG — users reach us through the gateway (Mongo protocol) and cross-cluster replication, not as generic Postgres clients, so the surface we expose should reflect that. -
A simpler shape. Since a
kubernetes.io/tlssecret already bundles cert + key +ca.crt, we could collapse the four CNPG fields into two of our own:spec: tls: replication: serverCertSecret: <secret> # -> serverTLSSecret + serverCASecret clientCertSecret: <secret> # -> replicationTLSSecret + clientCASecret
The operator maps these onto CNPG under the hood. (One open question worth a quick think: keep the CA bundled inside each secret like above, or expose a shared
caSecretseparately — since the CA is really the one shared piece across clusters.)
Since we're in preview and don't have many customers yet, it's totally fine to ship this as-is for now. I just think it's worth revisiting before GA. Could we open a tracking issue so it isn't forgotten?
2. Add an e2e test for the cert path.
The unit tests are solid, but they only check that the operator writes the right CNPG spec — they don't prove replication actually connects over TLS. The e2e replication path currently sets disableTLS: true, so the cert paths this PR adds aren't exercised end-to-end. It would be good to add an e2e test (even single-cluster) covering the cert path — ideally a positive case (certs provided -> replication healthy) and a negative case (bad cert -> replication fails).
Thanks again for the work here — happy to help with either follow-up.
xgerman
left a comment
There was a problem hiding this comment.
Reviewed and tested locally on this branch: go build, go test ./... (all packages), go vet, gofmt -l, and make manifests generate (no diff) all pass. The change correctly closes the trust-everywhere pg_hba gap for new clusters, the CEL pairing invariants are sound, and splitting externalClusters/pg_hba/synchronous into independent add/replace/remove ops is a real improvement over the old name-only trigger.
One functional gap worth addressing before merge (Major 1 below), plus a couple of lower-severity notes. Details inline.
| }) | ||
| } | ||
|
|
||
| if !reflect.DeepEqual(current.Spec.Certificates, desired.Spec.Certificates) { |
There was a problem hiding this comment.
🟠 Major — the hardened pg_hba never reaches existing non-replication clusters on upgrade. SyncCnpgCluster diffs PostgresConfiguration.Parameters (just above) and now Certificates, but not PostgresConfiguration.PgHBA. The only place that patches pg_hba is getReplicasChangePatchOps (physical_replication.go:541), reachable solely via syncReplicationChanges, which early-returns when ReplicaCluster == nil (physical_replication.go:378-380).
Net effect: a single-instance cluster created before this upgrade keeps host all all 0.0.0.0/0 trust indefinitely — the headline vulnerability stays open for existing installs. New clusters are fine (rules are set at bootstrap in buildPostgresConfiguration).
Suggest adding a PostgresConfiguration.PgHBA diff/patch here (mirroring the Parameters block, using PatchPathPostgresPgHBA) so all clusters converge regardless of the replication path.
| if postgresClientCertificateProvided { | ||
| connectionParameters["sslmode"] = "require" | ||
| if postgresServerCAProvided { | ||
| connectionParameters["sslmode"] = "verify-full" |
There was a problem hiding this comment.
🟡 Minor — verify-full is auto-selected whenever a server CA is present. verify-full validates the server hostname against the certificate SAN, but the connection host is the internal -rw.<ns>.svc (or fleet) DNS name. User-provided server certs whose SANs don't cover that exact host will make replication silently fail with no operator-side signal. The deploy scripts generate correct SANs, but user-supplied serverCASecret/serverTLSSecret won't necessarily. Consider making sslmode configurable (or documenting the exact SAN requirement prominently next to the API field).
| } | ||
| delete(nameSet, cluster.Name) | ||
| func getReplicasChangePatchOps(patchOps *[]cnpg.JSONPatch, current, desired *cnpgv1.Cluster, replicationContext *util.ReplicationContext) { | ||
| externalClusterSpecChanged := !reflect.DeepEqual(current.Spec.ExternalClusters, desired.Spec.ExternalClusters) |
There was a problem hiding this comment.
🟡 Minor — this reflect.DeepEqual on the full ExternalClusters slice replaces the old order-insensitive name-set compare. Ordering is deterministic here (slice-backed OtherCNPGClusterNames), so no churn today. The residual risk is server-side defaulting: if CNPG ever echoes back a normalized/defaulted ExternalCluster form (e.g. an added connection param), current vs freshly-built desired would differ every reconcile → perpetual replace patches. Low risk given current CNPG behavior, but a field-scoped compare (name + connection params + SSL selectors) would be more robust.
| "host all all 0.0.0.0/0 trust", | ||
| "host all all ::0/0 trust", | ||
| "host replication all all trust", | ||
| "host all all localhost trust", |
There was a problem hiding this comment.
🟢 Nitpick — localhost here is matched by Postgres as a hostname (reverse-DNS lookup on the client IP), not a literal address. It works inside the pod, but host all all 127.0.0.1/32 trust + host all all ::1/128 trust is the more explicit/robust form and avoids depending on reverse-DNS resolution.
Removes the permissive pg_hba rules that allowed passwordless PostgreSQL access from any address and replaces them with least-privilege rules plus optional certificate-based mutual TLS for replication. Adds a new spec.tls.postgres field so operators can supply their own CA/certificate Secrets for PostgreSQL server and replication connections.
Motivation
Previously the operator emitted:
Anyone with pod-network reachability could connect to PostgreSQL unauthenticated. This PR closes that gap while keeping the gateway sidecar (loopback) and cross-region replication working.
Changes
PostgreSQL access hardening (cnpg_cluster.go)
Base pg_hba is now host all all localhost trust (gateway sidecar over pod loopback) + hostssl replication streaming_replica all cert.
New spec.tls.postgres API (documentdb_types.go)
spec.tls.postgres is now backed by CNPG's CertificatesConfiguration (replaces the empty PostgresTLS placeholder). Supports serverTLSSecret/serverCASecret and replicationTLSSecret/clientCASecret.
A CEL XValidation rule enforces pairing invariants: server and replication secrets must each be provided together, and serverTLSSecret requires replicationTLSSecret.
Certificates are propagated to the CNPG Cluster and diffed/patched in cnpg_sync.go (add/replace/remove).
Replication TLS (physical_replication.go)
When replication client certs are provided, cross-region externalClusters connect with sslmode=require (or verify-full when a server CA is present) and present the streaming_replica client certificate.
When spec.tls.postgres is unset in multi-region deployments, replication falls back to host replication streaming_replica all trust, relying on network-layer security (e.g. an Istio/service-mesh mTLS boundary).
Reworked replication patch logic to diff externalClusters, pg_hba, and synchronous independently (proper add/replace/remove ops) instead of only reacting to cluster-name changes.
Docs & playground
New "PostgreSQL certificates" section in the TLS configuration guide; multi-region setup docs updated.
deploy-multi-region.sh / deploy-fleet-bicep.sh generate replication certs with cross-region SANs; new documentdb-resource-crp.yaml propagates cert Secrets across the fleet.
CHANGELOG entries under Security and Major Features.
Tests
New physical_replication_test.go and certificate_controller_test.go coverage; updated CNPG cluster and backup controller tests.