Skip to content

Commit 0e7beb5

Browse files
authored
refactor pooler tls support and set pooler pod security context (zalando#2255)
* bump pooler image * set pooler pod security context * use hard coded RunAsUser 100 and RunAsGroup 101 for pooler pod * unify generation of TLS secret mounts * extend documentation on tls support * add unit test for testing TLS support for pooler * add e2e test for tls support
1 parent 87b7ac0 commit 0e7beb5

16 files changed

+370
-121
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ coverage.xml
9595

9696
# e2e tests
9797
e2e/manifests
98+
e2e/tls
9899

99100
# Translations
100101
*.mo

charts/postgres-operator/crds/operatorconfigurations.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ spec:
637637
default: "pooler"
638638
connection_pooler_image:
639639
type: string
640-
default: "registry.opensource.zalan.do/acid/pgbouncer:master-26"
640+
default: "registry.opensource.zalan.do/acid/pgbouncer:master-27"
641641
connection_pooler_max_db_connections:
642642
type: integer
643643
default: 60

charts/postgres-operator/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ configConnectionPooler:
416416
# db user for pooler to use
417417
connection_pooler_user: "pooler"
418418
# docker image
419-
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-26"
419+
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-27"
420420
# max db connections the pooler should hold
421421
connection_pooler_max_db_connections: 60
422422
# default pooling mode

docs/reference/cluster_manifest.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,9 @@ for both master and replica pooler services (if `enableReplicaConnectionPooler`
543543

544544
## Custom TLS certificates
545545

546-
Those parameters are grouped under the `tls` top-level key.
546+
Those parameters are grouped under the `tls` top-level key. Note, you have to
547+
define `spiloFSGroup` in the Postgres cluster manifest or `spilo_fsgroup` in
548+
the global configuration before adding the `tls` section'.
547549

548550
* **secretName**
549551
By setting the `secretName` value, the cluster will switch to load the given

docs/user.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,14 +1197,19 @@ don't know the value, use `103` which is the GID from the default Spilo image
11971197
OpenShift allocates the users and groups dynamically (based on scc), and their
11981198
range is different in every namespace. Due to this dynamic behaviour, it's not
11991199
trivial to know at deploy time the uid/gid of the user in the cluster.
1200-
Therefore, instead of using a global `spilo_fsgroup` setting, use the
1201-
`spiloFSGroup` field per Postgres cluster.
1200+
Therefore, instead of using a global `spilo_fsgroup` setting in operator
1201+
configuration or use the `spiloFSGroup` field per Postgres cluster manifest.
1202+
1203+
For testing purposes, you can generate a self-signed certificate with openssl:
1204+
```sh
1205+
openssl req -x509 -nodes -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=acid.zalan.do"
1206+
```
12021207

12031208
Upload the cert as a kubernetes secret:
12041209
```sh
12051210
kubectl create secret tls pg-tls \
1206-
--key pg-tls.key \
1207-
--cert pg-tls.crt
1211+
--key tls.key \
1212+
--cert tls.crt
12081213
```
12091214

12101215
When doing client auth, CA can come optionally from the same secret:
@@ -1231,8 +1236,7 @@ spec:
12311236

12321237
Optionally, the CA can be provided by a different secret:
12331238
```sh
1234-
kubectl create secret generic pg-tls-ca \
1235-
--from-file=ca.crt=ca.crt
1239+
kubectl create secret generic pg-tls-ca --from-file=ca.crt=ca.crt
12361240
```
12371241

12381242
Then configure the postgres resource with the TLS secret:
@@ -1255,3 +1259,16 @@ Alternatively, it is also possible to use
12551259

12561260
Certificate rotation is handled in the Spilo image which checks every 5
12571261
minutes if the certificates have changed and reloads postgres accordingly.
1262+
1263+
### TLS certificates for connection pooler
1264+
1265+
By default, the pgBouncer image generates its own TLS certificate like Spilo.
1266+
When the `tls` section is specfied in the manifest it will be used for the
1267+
connection pooler pod(s) as well. The security context options are hard coded
1268+
to `runAsUser: 100` and `runAsGroup: 101`. The `fsGroup` will be the same
1269+
like for Spilo.
1270+
1271+
As of now, the operator does not sync the pooler deployment automatically
1272+
which means that changes in the pod template are not caught. You need to
1273+
toggle `enableConnectionPooler` to set environment variables, volumes, secret
1274+
mounts and securityContext required for TLS support in the pooler pod.

e2e/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ default: tools
2929

3030
clean:
3131
rm -rf manifests
32+
rm -rf tls
3233

3334
copy: clean
3435
mkdir manifests
3536
cp -r ../manifests .
37+
mkdir tls
3638

3739
docker: scm-source.json
3840
docker build -t "$(IMAGE):$(TAG)" .

e2e/run.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,18 @@ function set_kind_api_server_ip(){
5555
sed -i "s/server.*$/server: https:\/\/$kind_api_server/g" "${kubeconfig_path}"
5656
}
5757

58+
function generate_certificate(){
59+
openssl req -x509 -nodes -newkey rsa:2048 -keyout tls/tls.key -out tls/tls.crt -subj "/CN=acid.zalan.do"
60+
}
61+
5862
function run_tests(){
5963
echo "Running tests... image: ${e2e_test_runner_image}"
6064
# tests modify files in ./manifests, so we mount a copy of this directory done by the e2e Makefile
6165

6266
docker run --rm --network=host -e "TERM=xterm-256color" \
6367
--mount type=bind,source="$(readlink -f ${kubeconfig_path})",target=/root/.kube/config \
6468
--mount type=bind,source="$(readlink -f manifests)",target=/manifests \
69+
--mount type=bind,source="$(readlink -f tls)",target=/tls \
6570
--mount type=bind,source="$(readlink -f tests)",target=/tests \
6671
--mount type=bind,source="$(readlink -f exec.sh)",target=/exec.sh \
6772
--mount type=bind,source="$(readlink -f scripts)",target=/scripts \
@@ -82,6 +87,7 @@ function main(){
8287
[[ ! -f ${kubeconfig_path} ]] && start_kind
8388
load_operator_image
8489
set_kind_api_server_ip
90+
generate_certificate
8591

8692
shift
8793
run_tests $@

e2e/tests/k8s_api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,26 @@ def get_services():
156156
while not get_services():
157157
time.sleep(self.RETRY_TIMEOUT_SEC)
158158

159+
def count_pods_with_volume_mount(self, mount_name, labels, namespace='default'):
160+
pod_count = 0
161+
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
162+
for pod in pods:
163+
for mount in pod.spec.containers[0].volume_mounts:
164+
if mount.name == mount_name:
165+
pod_count += 1
166+
167+
return pod_count
168+
169+
def count_pods_with_env_variable(self, env_variable_key, labels, namespace='default'):
170+
pod_count = 0
171+
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
172+
for pod in pods:
173+
for env in pod.spec.containers[0].env:
174+
if env.name == env_variable_key:
175+
pod_count += 1
176+
177+
return pod_count
178+
159179
def count_pods_with_rolling_update_flag(self, labels, namespace='default'):
160180
pods = self.api.core_v1.list_namespaced_pod(namespace, label_selector=labels).items
161181
return len(list(filter(lambda x: "zalando-postgres-operator-rolling-update-required" in x.metadata.annotations, pods)))
@@ -241,6 +261,18 @@ def update_config(self, config_map_patch, step="Updating operator deployment"):
241261
def patch_pod(self, data, pod_name, namespace="default"):
242262
self.api.core_v1.patch_namespaced_pod(pod_name, namespace, data)
243263

264+
def create_tls_secret_with_kubectl(self, secret_name):
265+
return subprocess.run(
266+
["kubectl", "create", "secret", "tls", secret_name, "--key=tls/tls.key", "--cert=tls/tls.crt"],
267+
stdout=subprocess.PIPE,
268+
stderr=subprocess.PIPE)
269+
270+
def create_tls_ca_secret_with_kubectl(self, secret_name):
271+
return subprocess.run(
272+
["kubectl", "create", "secret", "generic", secret_name, "--from-file=ca.crt=tls/ca.crt"],
273+
stdout=subprocess.PIPE,
274+
stderr=subprocess.PIPE)
275+
244276
def create_with_kubectl(self, path):
245277
return subprocess.run(
246278
["kubectl", "apply", "-f", path],

e2e/tests/test_e2e.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,49 @@ def test_cross_namespace_secrets(self):
622622
self.eventuallyEqual(lambda: k8s.count_secrets_with_label("cluster-name=acid-minimal-cluster,application=spilo", self.test_namespace),
623623
1, "Secret not created for user in namespace")
624624

625+
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
626+
def test_custom_ssl_certificate(self):
627+
'''
628+
Test if spilo uses a custom SSL certificate
629+
'''
630+
631+
k8s = self.k8s
632+
cluster_label = 'application=spilo,cluster-name=acid-minimal-cluster'
633+
tls_secret = "pg-tls"
634+
635+
# get nodes of master and replica(s) (expected target of new master)
636+
_, replica_nodes = k8s.get_pg_nodes(cluster_label)
637+
self.assertNotEqual(replica_nodes, [])
638+
639+
try:
640+
# create secret containing ssl certificate
641+
result = self.k8s.create_tls_secret_with_kubectl(tls_secret)
642+
print("stdout: {}, stderr: {}".format(result.stdout, result.stderr))
643+
644+
# enable load balancer services
645+
pg_patch_tls = {
646+
"spec": {
647+
"spiloFSGroup": 103,
648+
"tls": {
649+
"secretName": tls_secret
650+
}
651+
}
652+
}
653+
k8s.api.custom_objects_api.patch_namespaced_custom_object(
654+
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_tls)
655+
656+
# wait for switched over
657+
k8s.wait_for_pod_failover(replica_nodes, 'spilo-role=master,' + cluster_label)
658+
k8s.wait_for_pod_start('spilo-role=replica,' + cluster_label)
659+
660+
self.eventuallyEqual(lambda: k8s.count_pods_with_env_variable("SSL_CERTIFICATE_FILE", cluster_label), 2, "TLS env variable SSL_CERTIFICATE_FILE missing in Spilo pods")
661+
self.eventuallyEqual(lambda: k8s.count_pods_with_env_variable("SSL_PRIVATE_KEY_FILE", cluster_label), 2, "TLS env variable SSL_PRIVATE_KEY_FILE missing in Spilo pods")
662+
self.eventuallyEqual(lambda: k8s.count_pods_with_volume_mount(tls_secret, cluster_label), 2, "TLS volume mount missing in Spilo pods")
663+
664+
except timeout_decorator.TimeoutError:
665+
print('Operator log: {}'.format(k8s.get_operator_log()))
666+
raise
667+
625668
@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
626669
def test_enable_disable_connection_pooler(self):
627670
'''
@@ -653,6 +696,11 @@ def test_enable_disable_connection_pooler(self):
653696
self.eventuallyEqual(lambda: k8s.count_services_with_label(pooler_label), 2, "No pooler service found")
654697
self.eventuallyEqual(lambda: k8s.count_secrets_with_label(pooler_label), 1, "Pooler secret not created")
655698

699+
# TLS still enabled so check existing env variables and volume mounts
700+
self.eventuallyEqual(lambda: k8s.count_pods_with_env_variable("CONNECTION_POOLER_CLIENT_TLS_CRT", pooler_label), 4, "TLS env variable CONNECTION_POOLER_CLIENT_TLS_CRT missing in pooler pods")
701+
self.eventuallyEqual(lambda: k8s.count_pods_with_env_variable("CONNECTION_POOLER_CLIENT_TLS_KEY", pooler_label), 4, "TLS env variable CONNECTION_POOLER_CLIENT_TLS_KEY missing in pooler pods")
702+
self.eventuallyEqual(lambda: k8s.count_pods_with_volume_mount("pg-tls", pooler_label), 4, "TLS volume mount missing in pooler pods")
703+
656704
k8s.api.custom_objects_api.patch_namespaced_custom_object(
657705
'acid.zalan.do', 'v1', 'default',
658706
'postgresqls', 'acid-minimal-cluster',

manifests/configmap.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ data:
1717
# connection_pooler_default_cpu_request: "500m"
1818
# connection_pooler_default_memory_limit: 100Mi
1919
# connection_pooler_default_memory_request: 100Mi
20-
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-26"
20+
connection_pooler_image: "registry.opensource.zalan.do/acid/pgbouncer:master-27"
2121
# connection_pooler_max_db_connections: 60
2222
# connection_pooler_mode: "transaction"
2323
# connection_pooler_number_of_instances: 2

0 commit comments

Comments
 (0)