Skip to content

Commit c5a8122

Browse files
committed
fix(postgres)!: refuse plaintext sessions on external instances
CloudNativePG offers TLS but its built-in last pg_hba rule, host all all all, also accepts plaintext, so a client outside the cluster that connects with sslmode=disable gets an unencrypted session. tls.enabled only decides whether the external hostname goes into the certificate and cannot prevent this. When external is true the chart now renders spec.postgresql.pg_hba with hostssl all all all scram-sha-256 followed by host all all all reject. CloudNativePG places user rules after its fixed rules and before its default rule, and the first match wins, so only TLS sessions are accepted. The pod network cannot be told apart from an external client, so in-cluster clients are held to the same rule; libpq's default sslmode=prefer already negotiates TLS. The new tri-state tls.required follows external when omitted. Setting it to false keeps accepting plaintext on an external instance and true requires TLS on an in-cluster one. BREAKING CHANGE: external clients that connect without TLS are refused after the upgrade. Set tls.required to false to keep accepting them. Assisted-by: LLM Signed-off-by: Yasar Rahman <[email protected]>
1 parent 6144805 commit c5a8122

9 files changed

Lines changed: 144 additions & 5 deletions

File tree

‎api/apps/v1alpha1/postgresql/types.go‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎api/apps/v1alpha1/postgresql/zz_generated.deepcopy.go‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/apps/postgres/README.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ The tri-state `tls.enabled` controls whether the chart injects `serverAltDNSName
175175
- `tls.enabled: true` with `external: false` — no `serverAltDNSNames` injection is needed (there is no external hostname to add); CNPG's auto-generated cert covers internal services.
176176
- `tls.enabled: false` — the chart skips `serverAltDNSNames` injection. **Note:** CNPG keeps its built-in TLS on the wire regardless of this flag; this toggle only controls whether the external hostnames are added to the cert. To force PostgreSQL to drop TLS entirely you would need to set `postgresql.parameters.ssl = "off"` at the CNPG layer, which is out of scope for this flag.
177177

178+
**Refusing plaintext sessions.** CNPG offers TLS but its built-in last `pg_hba` rule, `host all all all`, also accepts plaintext, so a client using `sslmode=disable` gets an unencrypted session. The tri-state `tls.required` closes that:
179+
180+
- `tls.required: null` (the default) — follows `external`. With `external: true` the chart renders `spec.postgresql.pg_hba` as `hostssl all all all scram-sha-256` followed by `host all all all reject`. CNPG puts these after its fixed rules and before its default rule, and the first matching rule wins, so only TLS sessions are authenticated.
181+
- `tls.required: true` — the same rules regardless of `external`.
182+
- `tls.required: false` — no `pg_hba` rules are rendered and CNPG's default applies, which accepts plaintext.
183+
184+
The rules cover every client, in-cluster ones included, because the pod network cannot be told apart from an external client. libpq's default `sslmode=prefer` already negotiates TLS, so only clients that disable TLS explicitly are refused. Instances that were external before this setting existed refuse such clients after the upgrade; set `tls.required: false` to keep accepting them. See the CloudNativePG [`pg_hba` documentation](https://cloudnative-pg.io/documentation/current/postgresql_conf/#the-pg_hba-section).
185+
178186
**Retrieving the CA bundle** for client verification:
179187

180188
The trust anchor is published as `postgres-<name>.tenant-ca`, where `<name>` is the name of the Postgres resource: an object holding `ca.crt` and nothing else, created for every release and delivered to tenants through the `core.cozystack.io/tenantsecrets` API that the base tenant roles already grant. The `postgres-` prefix comes from the release naming this application definition applies, so a Postgres named `foo` gets `postgres-foo.tenant-ca`.
@@ -218,10 +226,11 @@ For `sslmode=verify-full` to work, the CA bundle retrieved above must be saved t
218226

219227
### TLS configuration
220228

221-
| Name | Description | Type | Value |
222-
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------ |
223-
| `tls` | TLS configuration for server connections. | `object` | `{}` |
224-
| `tls.enabled` | Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = "off"` at the CNPG layer. | `*bool` | `null` |
229+
| Name | Description | Type | Value |
230+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------ |
231+
| `tls` | TLS configuration for server connections. | `object` | `{}` |
232+
| `tls.enabled` | Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = "off"` at the CNPG layer. | `*bool` | `null` |
233+
| `tls.required` | Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if `external: true` and off otherwise. When on, the chart renders `spec.postgresql.pg_hba` with `hostssl all all all scram-sha-256` followed by `host all all all reject`, so a client that connects with `sslmode=disable` is refused instead of getting a plaintext session; in-cluster clients are held to the same rule, since the pod network cannot be told apart from an external client. Set to `false` to keep accepting plaintext sessions on an external instance, or to `true` to require TLS on an in-cluster one. | `*bool` | `null` |
225234

226235

227236
### Application-specific parameters

‎packages/apps/postgres/templates/_tls.tpl‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,16 @@ Tri-state semantics:
1515
{{- .Values.tls.enabled | toString -}}
1616
{{- end -}}
1717
{{- end -}}
18+
19+
{{/*
20+
postgres.tls.required resolves the tri-state tls.required field to "true" or
21+
"false". Unset follows external, so an externally published instance refuses
22+
plaintext sessions by default. Set explicitly, it wins in either direction.
23+
*/}}
24+
{{- define "postgres.tls.required" -}}
25+
{{- if kindIs "invalid" .Values.tls.required -}}
26+
{{- .Values.external | default false | toString -}}
27+
{{- else -}}
28+
{{- .Values.tls.required | toString -}}
29+
{{- end -}}
30+
{{- end -}}

‎packages/apps/postgres/templates/db.yaml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{{- $tlsEnabled := (include "postgres.tls.enabled" .) | eq "true" -}}
2+
{{- $tlsRequired := (include "postgres.tls.required" .) | eq "true" -}}
23
{{- $autoscaling := (include "postgres.autoscaling.active" .) | eq "true" -}}
34
---
45
apiVersion: postgresql.cnpg.io/v1
@@ -269,6 +270,19 @@ spec:
269270
{{- end }}
270271
{{- end }}
271272
postgresql:
273+
{{- if $tlsRequired }}
274+
{{- /*
275+
CloudNativePG offers TLS (ssl=on) but its built-in last rule, host all all
276+
all, also accepts plaintext, so a client choosing sslmode=disable gets an
277+
unencrypted session. User rules go after the operator's fixed rules
278+
(local peer, streaming_replica cert) and before that default rule, and the
279+
first match wins, so these two lines make remote sessions TLS-only.
280+
https://cloudnative-pg.io/documentation/current/postgresql_conf/#the-pg_hba-section
281+
*/}}
282+
pg_hba:
283+
- hostssl all all all scram-sha-256
284+
- host all all all reject
285+
{{- end }}
272286
{{- /*
273287
Preloading is exposed as a closed allowlist rather than through the blocked
274288
shared_preload_libraries parameter. The parameter is free text and accepts a
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
suite: postgres pg_hba TLS enforcement
2+
3+
# CNPG appends user pg_hba rules after its fixed rules and before its default
4+
# "host all all all" rule, and the first match wins. tls.required is a tri-state
5+
# that follows external when omitted.
6+
7+
release:
8+
name: pg-test
9+
namespace: tenant-test
10+
11+
set:
12+
_cluster:
13+
scheduling: {}
14+
cluster-domain: cozy.local
15+
_namespace:
16+
host: example.cozy.local
17+
18+
templates:
19+
- templates/db.yaml
20+
21+
tests:
22+
- it: renders no pg_hba when the instance is not external
23+
documentSelector:
24+
path: kind
25+
value: Cluster
26+
asserts:
27+
- isNull:
28+
path: spec.postgresql.pg_hba
29+
30+
- it: external instance accepts TLS sessions only
31+
set:
32+
external: true
33+
documentSelector:
34+
path: kind
35+
value: Cluster
36+
asserts:
37+
- equal:
38+
path: spec.postgresql.pg_hba
39+
value:
40+
- hostssl all all all scram-sha-256
41+
- host all all all reject
42+
43+
- it: external instance still gets the TLS rules when tls.enabled is false
44+
set:
45+
external: true
46+
tls:
47+
enabled: false
48+
documentSelector:
49+
path: kind
50+
value: Cluster
51+
asserts:
52+
- equal:
53+
path: spec.postgresql.pg_hba[0]
54+
value: hostssl all all all scram-sha-256
55+
56+
- it: tls.required false opts an external instance out
57+
set:
58+
external: true
59+
tls:
60+
required: false
61+
documentSelector:
62+
path: kind
63+
value: Cluster
64+
asserts:
65+
- isNull:
66+
path: spec.postgresql.pg_hba
67+
68+
- it: tls.required true applies to an in-cluster instance
69+
set:
70+
tls:
71+
required: true
72+
documentSelector:
73+
path: kind
74+
value: Cluster
75+
asserts:
76+
- equal:
77+
path: spec.postgresql.pg_hba
78+
value:
79+
- hostssl all all all scram-sha-256
80+
- host all all all reject
81+
82+
- it: pg_hba does not disturb postgresql.parameters
83+
set:
84+
external: true
85+
documentSelector:
86+
path: kind
87+
value: Cluster
88+
asserts:
89+
- equal:
90+
path: spec.postgresql.parameters.max_connections
91+
value: "100"

‎packages/apps/postgres/values.schema.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@
157157
"enabled": {
158158
"description": "Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = \"off\"` at the CNPG layer.",
159159
"type": "boolean"
160+
},
161+
"required": {
162+
"description": "Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if `external: true` and off otherwise. When on, the chart renders `spec.postgresql.pg_hba` with `hostssl all all all scram-sha-256` followed by `host all all all reject`, so a client that connects with `sslmode=disable` is refused instead of getting a plaintext session; in-cluster clients are held to the same rule, since the pod network cannot be told apart from an external client. Set to `false` to keep accepting plaintext sessions on an external instance, or to `true` to require TLS on an in-cluster one.",
163+
"type": "boolean"
160164
}
161165
}
162166
},

‎packages/apps/postgres/values.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ flavor: postgresql
101101

102102
## @typedef {struct} TLS - TLS configuration for PostgreSQL server connections.
103103
## @field {*bool} enabled - Tri-state switch controlling whether the chart injects the external hostnames into the operator-managed CNPG cert via spec.certificates.serverAltDNSNames. When omitted, the chart injects them if `external: true` and skips them otherwise. Set explicitly to `true` to inject regardless of `external` (no-op when `external: false` since there is no external hostname to add). Set to `false` to skip injection. Note that CNPG keeps its built-in TLS on the wire regardless of this flag — this toggle only controls the chart-side SAN injection; to disable PostgreSQL TLS entirely set `postgresql.parameters.ssl = "off"` at the CNPG layer.
104+
## @field {*bool} required - Tri-state switch controlling whether the server refuses sessions that are not encrypted. When omitted, it is on if `external: true` and off otherwise. When on, the chart renders `spec.postgresql.pg_hba` with `hostssl all all all scram-sha-256` followed by `host all all all reject`, so a client that connects with `sslmode=disable` is refused instead of getting a plaintext session; in-cluster clients are held to the same rule, since the pod network cannot be told apart from an external client. Set to `false` to keep accepting plaintext sessions on an external instance, or to `true` to require TLS on an in-cluster one.
104105

105106
## @param {TLS} tls - TLS configuration for server connections.
106107
tls: {}

‎packages/system/postgres-rd/cozyrds/postgres.yaml‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)