From 11286a1f89804f78526f30cafeb209683e5a6063 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 24 May 2026 00:02:40 +1000 Subject: [PATCH] feat(kanidm): automate replication cert exchange via native sidecar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a native sidecar (bitnami/kubectl, restartPolicy: Always) that runs kanidmd renew-replication-certificate on each pod and patches the result into the kanidm-repl-certs ConfigMap (certs are public keys, not secrets). The config-init init container reads peer certs from the ConfigMap at startup, building the replication stanza automatically — no manual cert exchange required after first deploy. Add RBAC (Role + RoleBinding) granting the kanidm service account pods/exec and configmap patch permissions scoped to the kanidm namespace. --- apps/base/kanidm/configmap.yaml | 27 +++------------- apps/base/kanidm/kustomization.yaml | 1 + apps/base/kanidm/rbac.yaml | 37 +++++++++++++++++++++ apps/base/kanidm/statefulset.yaml | 50 +++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 apps/base/kanidm/rbac.yaml diff --git a/apps/base/kanidm/configmap.yaml b/apps/base/kanidm/configmap.yaml index 0cf9194..bd68434 100644 --- a/apps/base/kanidm/configmap.yaml +++ b/apps/base/kanidm/configmap.yaml @@ -29,34 +29,15 @@ data: origin = "__REPL_ORIGIN__" bindaddress = "[::]:8444" --- -# kanidm-repl-peers is initially empty. -# -# After first deployment, exchange replication certificates: -# kubectl exec -n kanidm kanidm-0 -- kanidmd show-replication-certificate -# kubectl exec -n kanidm kanidm-1 -- kanidmd show-replication-certificate -# kubectl exec -n kanidm kanidm-2 -- kanidmd show-replication-certificate -# -# Then populate peers.toml with all nodes' certs and restart pods. -# Example peers.toml content: -# -# [replication."repl://kanidm-0.kanidm-headless.kanidm.svc.cluster.local:8444"] -# type = "mutual-pull" -# partner_cert = "" -# -# [replication."repl://kanidm-1.kanidm-headless.kanidm.svc.cluster.local:8444"] -# type = "mutual-pull" -# partner_cert = "" -# -# [replication."repl://kanidm-2.kanidm-headless.kanidm.svc.cluster.local:8444"] -# type = "mutual-pull" -# partner_cert = "" apiVersion: v1 kind: ConfigMap metadata: - name: kanidm-repl-peers + name: kanidm-repl-certs namespace: kanidm labels: app.kubernetes.io/name: kanidm app.kubernetes.io/instance: kanidm data: - peers.toml: "" + kanidm-0: "" + kanidm-1: "" + kanidm-2: "" diff --git a/apps/base/kanidm/kustomization.yaml b/apps/base/kanidm/kustomization.yaml index 750eb3c..26430b9 100644 --- a/apps/base/kanidm/kustomization.yaml +++ b/apps/base/kanidm/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - namespace.yaml - serviceaccount.yaml + - rbac.yaml - certificate.yaml - configmap.yaml - service.yaml diff --git a/apps/base/kanidm/rbac.yaml b/apps/base/kanidm/rbac.yaml new file mode 100644 index 0000000..73d27a0 --- /dev/null +++ b/apps/base/kanidm/rbac.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kanidm-repl + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] + - apiGroups: [""] + resources: ["configmaps"] + resourceNames: ["kanidm-repl-certs"] + verbs: ["get", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kanidm-repl + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +subjects: + - kind: ServiceAccount + name: kanidm + namespace: kanidm +roleRef: + kind: Role + name: kanidm-repl + apiGroup: rbac.authorization.k8s.io diff --git a/apps/base/kanidm/statefulset.yaml b/apps/base/kanidm/statefulset.yaml index 39d8ada..d68ce44 100644 --- a/apps/base/kanidm/statefulset.yaml +++ b/apps/base/kanidm/statefulset.yaml @@ -43,9 +43,17 @@ spec: set -e REPL_ORIGIN="repl://${POD_NAME}.kanidm-headless.kanidm.svc.cluster.local:8444" sed "s|__REPL_ORIGIN__|${REPL_ORIGIN}|g" /config-template/server.toml > /config/server.toml - if [ -s /repl-peers/peers.toml ]; then - cat /repl-peers/peers.toml >> /config/server.toml - fi + for peer in kanidm-0 kanidm-1 kanidm-2; do + if [ "${peer}" = "${POD_NAME}" ]; then + continue + fi + cert_file="/repl-certs/${peer}" + if [ -s "${cert_file}" ]; then + fqdn="${peer}.kanidm-headless.kanidm.svc.cluster.local" + printf '\n[replication."repl://%s:8444"]\ntype = "mutual-pull"\npartner_cert = "%s"\n' \ + "${fqdn}" "$(cat ${cert_file})" >> /config/server.toml + fi + done env: - name: POD_NAME valueFrom: @@ -56,11 +64,39 @@ spec: mountPath: /config-template - name: config mountPath: /config - - name: repl-peers - mountPath: /repl-peers + - name: repl-certs + mountPath: /repl-certs + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true + - name: repl-cert-publisher + image: bitnami/kubectl:1.33 + restartPolicy: Always + command: ["/bin/sh", "-c"] + args: + - | + until kubectl exec "${POD_NAME}" -c kanidm -- /sbin/kanidmd renew-replication-certificate 2>/dev/null | grep -q '^# certificate:'; do + sleep 30 + done + while true; do + cert=$(kubectl exec "${POD_NAME}" -c kanidm -- /sbin/kanidmd renew-replication-certificate 2>/dev/null \ + | grep '^# certificate:' | sed 's/^# certificate: "\(.*\)"$/\1/') + if [ -n "${cert}" ]; then + kubectl patch configmap kanidm-repl-certs \ + --type=merge \ + -p "{\"data\":{\"${POD_NAME}\":\"${cert}\"}}" + fi + sleep 3600 + done + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false containers: - name: kanidm image: ghcr.io/kanidm/server:1.10.3 @@ -108,9 +144,9 @@ spec: name: kanidm-config - name: config emptyDir: {} - - name: repl-peers + - name: repl-certs configMap: - name: kanidm-repl-peers + name: kanidm-repl-certs - name: tls secret: secretName: kanidm-tls