From ee76ec199b139a43fa45bf3c89e705981bc394db Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 23 May 2026 23:12:52 +1000 Subject: [PATCH 1/4] feat(kanidm): deploy Kanidm 1.10.3 with 2-replica replication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Deploys Kanidm 1.10.3 (ghcr.io/kanidm/server:1.10.3) as a 2-replica StatefulSet with built-in replication enabled - Domain: auth.unkin.net (primary WebAuthn origin); au.auth.unkin.net is an additional hostname for this au-syd1 instance - TLS: cert-manager Certificate (vault-issuer) covering auth.unkin.net, au.auth.unkin.net, kanidm.k8s.syd1.au.unkin.net, and internal headless pod DNS names — Kanidm terminates TLS itself (passthrough) - Gateway: TLS passthrough on port 443 via TLSRoute; HTTP on port 80 redirects to HTTPS; external-dns creates kanidm.k8s.syd1.au.unkin.net (not used in server.toml; canonical origin is auth.unkin.net only) - Replication: init container generates per-pod server.toml with the correct repl:// origin (kanidm-{N}.kanidm-headless.kanidm.svc); populate kanidm-repl-peers ConfigMap post-deployment after running `kanidmd show-replication-certificate` on each pod - Storage: 10Gi cephrbd-fast-delete PVC per pod via volumeClaimTemplates - Security: runAsUser/runAsGroup 1000, runAsNonRoot, no privilege escalation, allowPrivilegeEscalation=false - ArgoCD: platform ApplicationSet and Project updated for kanidm namespace ## Requires - PR benvin/kanidm-artifactapi (add ^kanidm/ to ghcr immutable patterns) to be merged first so artifactapi can cache ghcr.io/kanidm/server ## Post-deployment steps 1. Wait for both pods to reach Running state 2. Exchange replication certificates between pods: kubectl exec -n kanidm kanidm-0 -- kanidmd show-replication-certificate kubectl exec -n kanidm kanidm-1 -- kanidmd show-replication-certificate 3. Edit kanidm-repl-peers ConfigMap with both nodes' certs (see template in configmap.yaml comments) 4. kubectl rollout restart statefulset/kanidm -n kanidm ## Test plan - [x] Sandbox tested in sandbox-kanidm: all 11 resources server dry-run OK - [ ] After merge: ArgoCD syncs kanidm namespace - [ ] Verify auth.unkin.net and au.auth.unkin.net reachable via Gateway - [ ] Verify kanidm.k8s.syd1.au.unkin.net DNS record created by external-dns - [ ] Complete replication cert exchange and verify replication active --- apps/base/kanidm/certificate.yaml | 25 ++++ apps/base/kanidm/configmap.yaml | 57 +++++++++ apps/base/kanidm/gateway.yaml | 30 +++++ apps/base/kanidm/httproute.yaml | 27 ++++ apps/base/kanidm/kustomization.yaml | 14 +++ apps/base/kanidm/namespace.yaml | 5 + apps/base/kanidm/service.yaml | 43 +++++++ apps/base/kanidm/serviceaccount.yaml | 9 ++ apps/base/kanidm/statefulset.yaml | 117 ++++++++++++++++++ apps/base/kanidm/tlsroute.yaml | 21 ++++ .../au-syd1/kanidm/kustomization.yaml | 6 + argocd/applicationsets/platform.yaml | 1 + argocd/projects/platform.yaml | 2 + 13 files changed, 357 insertions(+) create mode 100644 apps/base/kanidm/certificate.yaml create mode 100644 apps/base/kanidm/configmap.yaml create mode 100644 apps/base/kanidm/gateway.yaml create mode 100644 apps/base/kanidm/httproute.yaml create mode 100644 apps/base/kanidm/kustomization.yaml create mode 100644 apps/base/kanidm/namespace.yaml create mode 100644 apps/base/kanidm/service.yaml create mode 100644 apps/base/kanidm/serviceaccount.yaml create mode 100644 apps/base/kanidm/statefulset.yaml create mode 100644 apps/base/kanidm/tlsroute.yaml create mode 100644 apps/overlays/au-syd1/kanidm/kustomization.yaml diff --git a/apps/base/kanidm/certificate.yaml b/apps/base/kanidm/certificate.yaml new file mode 100644 index 0000000..686e300 --- /dev/null +++ b/apps/base/kanidm/certificate.yaml @@ -0,0 +1,25 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: kanidm-tls + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + secretName: kanidm-tls + issuerRef: + kind: ClusterIssuer + name: vault-issuer + commonName: auth.unkin.net + dnsNames: + - auth.unkin.net + - au.auth.unkin.net + - kanidm.k8s.syd1.au.unkin.net + - kanidm.kanidm.svc.cluster.local + - kanidm-0.kanidm-headless.kanidm.svc.cluster.local + - kanidm-1.kanidm-headless.kanidm.svc.cluster.local + privateKey: + algorithm: RSA + size: 4096 diff --git a/apps/base/kanidm/configmap.yaml b/apps/base/kanidm/configmap.yaml new file mode 100644 index 0000000..b00aea9 --- /dev/null +++ b/apps/base/kanidm/configmap.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kanidm-config + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +data: + server.toml: | + version = "2" + + domain = "auth.unkin.net" + origin = "https://auth.unkin.net" + bindaddress = "[::]:8443" + db_path = "/data/kanidm.db" + db_arc_size = 2048 + tls_chain = "/data/tls/tls.crt" + tls_key = "/data/tls/tls.key" + log_level = "info" + + [online_backup] + path = "/data/backups/" + schedule = "0 22 * * *" + versions = 7 + + [replication] + 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 +# +# Then populate peers.toml with both 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 = "" +apiVersion: v1 +kind: ConfigMap +metadata: + name: kanidm-repl-peers + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +data: + peers.toml: "" diff --git a/apps/base/kanidm/gateway.yaml b/apps/base/kanidm/gateway.yaml new file mode 100644 index 0000000..cf3479a --- /dev/null +++ b/apps/base/kanidm/gateway.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm + traefik.io/instance: internal + annotations: + external-dns.alpha.kubernetes.io/hostname: kanidm.k8s.syd1.au.unkin.net + external-dns.alpha.kubernetes.io/target: 198.18.200.4 +spec: + gatewayClassName: traefik-internal + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: https-passthrough + port: 443 + protocol: TLS + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: Same diff --git a/apps/base/kanidm/httproute.yaml b/apps/base/kanidm/httproute.yaml new file mode 100644 index 0000000..5c25e08 --- /dev/null +++ b/apps/base/kanidm/httproute.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: kanidm-http-redirect + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + hostnames: + - kanidm.k8s.syd1.au.unkin.net + - auth.unkin.net + - au.auth.unkin.net + parentRefs: + - name: kanidm + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/kanidm/kustomization.yaml b/apps/base/kanidm/kustomization.yaml new file mode 100644 index 0000000..52499e1 --- /dev/null +++ b/apps/base/kanidm/kustomization.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - serviceaccount.yaml + - certificate.yaml + - configmap.yaml + - service.yaml + - statefulset.yaml + - gateway.yaml + - httproute.yaml + - tlsroute.yaml diff --git a/apps/base/kanidm/namespace.yaml b/apps/base/kanidm/namespace.yaml new file mode 100644 index 0000000..bde2072 --- /dev/null +++ b/apps/base/kanidm/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kanidm diff --git a/apps/base/kanidm/service.yaml b/apps/base/kanidm/service.yaml new file mode 100644 index 0000000..8de5884 --- /dev/null +++ b/apps/base/kanidm/service.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + type: ClusterIP + ports: + - name: https + port: 8443 + targetPort: https + protocol: TCP + selector: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +--- +apiVersion: v1 +kind: Service +metadata: + name: kanidm-headless + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + type: ClusterIP + clusterIP: None + ports: + - name: https + port: 8443 + targetPort: https + protocol: TCP + - name: replication + port: 8444 + targetPort: replication + protocol: TCP + selector: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm diff --git a/apps/base/kanidm/serviceaccount.yaml b/apps/base/kanidm/serviceaccount.yaml new file mode 100644 index 0000000..66e0834 --- /dev/null +++ b/apps/base/kanidm/serviceaccount.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm diff --git a/apps/base/kanidm/statefulset.yaml b/apps/base/kanidm/statefulset.yaml new file mode 100644 index 0000000..cc9bbc3 --- /dev/null +++ b/apps/base/kanidm/statefulset.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + serviceName: kanidm-headless + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm + template: + metadata: + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm + spec: + serviceAccountName: kanidm + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + fsGroup: 1000 + initContainers: + - name: config-init + image: ghcr.io/kanidm/server:1.10.3 + command: ["/bin/sh", "-c"] + args: + - | + 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 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + volumeMounts: + - name: config-template + mountPath: /config-template + - name: config + mountPath: /config + - name: repl-peers + mountPath: /repl-peers + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + containers: + - name: kanidm + image: ghcr.io/kanidm/server:1.10.3 + command: ["/sbin/kanidmd"] + args: ["server", "-c", "/config/server.toml"] + ports: + - name: https + containerPort: 8443 + protocol: TCP + - name: replication + containerPort: 8444 + protocol: TCP + volumeMounts: + - name: data + mountPath: /data + - name: config + mountPath: /config + readOnly: true + - name: tls + mountPath: /data/tls + readOnly: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + readinessProbe: + tcpSocket: + port: 8443 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8443 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: config-template + configMap: + name: kanidm-config + - name: config + emptyDir: {} + - name: repl-peers + configMap: + name: kanidm-repl-peers + - name: tls + secret: + secretName: kanidm-tls + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ReadWriteOnce] + storageClassName: cephrbd-fast-delete + resources: + requests: + storage: 10Gi diff --git a/apps/base/kanidm/tlsroute.yaml b/apps/base/kanidm/tlsroute.yaml new file mode 100644 index 0000000..fb14f65 --- /dev/null +++ b/apps/base/kanidm/tlsroute.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: TLSRoute +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + hostnames: + - kanidm.k8s.syd1.au.unkin.net + - auth.unkin.net + - au.auth.unkin.net + parentRefs: + - name: kanidm + sectionName: https-passthrough + rules: + - backendRefs: + - name: kanidm + port: 8443 diff --git a/apps/overlays/au-syd1/kanidm/kustomization.yaml b/apps/overlays/au-syd1/kanidm/kustomization.yaml new file mode 100644 index 0000000..05ef408 --- /dev/null +++ b/apps/overlays/au-syd1/kanidm/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/kanidm diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index ac5afac..8974f77 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -20,6 +20,7 @@ spec: - path: apps/overlays/*/externaldns - path: apps/overlays/*/inteldeviceplugins-system - path: apps/overlays/*/jfrog + - path: apps/overlays/*/kanidm - path: apps/overlays/*/node-feature-discovery - path: apps/overlays/*/puppet - path: apps/overlays/*/purelb diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index 53e4e28..67d125c 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -27,6 +27,8 @@ spec: server: https://kubernetes.default.svc - namespace: 'jfrog' server: https://kubernetes.default.svc + - namespace: 'kanidm' + server: https://kubernetes.default.svc - namespace: 'node-feature-discovery' server: https://kubernetes.default.svc - namespace: 'purelb' -- 2.47.3 From e91fe554ebf3dea553c8ee5efea4d871659ab41b Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 23 May 2026 23:49:30 +1000 Subject: [PATCH 2/4] feat(kanidm): 3 replicas, PDB maxUnavailable=1, host anti-affinity - Increase replicas from 2 to 3 - Add kanidm-2 headless DNS SAN to TLS certificate - Add PodDisruptionBudget (maxUnavailable: 1) to maintain quorum during node drains - Add requiredDuringSchedulingIgnoredDuringExecution pod anti-affinity on kubernetes.io/hostname to spread replicas across distinct hosts - Update replication peers comment to include kanidm-2 cert exchange step --- apps/base/kanidm/certificate.yaml | 1 + apps/base/kanidm/configmap.yaml | 7 ++++++- apps/base/kanidm/kustomization.yaml | 1 + apps/base/kanidm/poddisruptionbudget.yaml | 15 +++++++++++++++ apps/base/kanidm/statefulset.yaml | 10 +++++++++- 5 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 apps/base/kanidm/poddisruptionbudget.yaml diff --git a/apps/base/kanidm/certificate.yaml b/apps/base/kanidm/certificate.yaml index 686e300..01a3000 100644 --- a/apps/base/kanidm/certificate.yaml +++ b/apps/base/kanidm/certificate.yaml @@ -20,6 +20,7 @@ spec: - kanidm.kanidm.svc.cluster.local - kanidm-0.kanidm-headless.kanidm.svc.cluster.local - kanidm-1.kanidm-headless.kanidm.svc.cluster.local + - kanidm-2.kanidm-headless.kanidm.svc.cluster.local privateKey: algorithm: RSA size: 4096 diff --git a/apps/base/kanidm/configmap.yaml b/apps/base/kanidm/configmap.yaml index b00aea9..0cf9194 100644 --- a/apps/base/kanidm/configmap.yaml +++ b/apps/base/kanidm/configmap.yaml @@ -34,8 +34,9 @@ data: # 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 both nodes' certs and restart pods. +# 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"] @@ -45,6 +46,10 @@ data: # [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: diff --git a/apps/base/kanidm/kustomization.yaml b/apps/base/kanidm/kustomization.yaml index 52499e1..750eb3c 100644 --- a/apps/base/kanidm/kustomization.yaml +++ b/apps/base/kanidm/kustomization.yaml @@ -9,6 +9,7 @@ resources: - configmap.yaml - service.yaml - statefulset.yaml + - poddisruptionbudget.yaml - gateway.yaml - httproute.yaml - tlsroute.yaml diff --git a/apps/base/kanidm/poddisruptionbudget.yaml b/apps/base/kanidm/poddisruptionbudget.yaml new file mode 100644 index 0000000..9db7ad1 --- /dev/null +++ b/apps/base/kanidm/poddisruptionbudget.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: kanidm + namespace: kanidm + labels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm diff --git a/apps/base/kanidm/statefulset.yaml b/apps/base/kanidm/statefulset.yaml index cc9bbc3..39d8ada 100644 --- a/apps/base/kanidm/statefulset.yaml +++ b/apps/base/kanidm/statefulset.yaml @@ -9,7 +9,7 @@ metadata: app.kubernetes.io/instance: kanidm spec: serviceName: kanidm-headless - replicas: 2 + replicas: 3 selector: matchLabels: app.kubernetes.io/name: kanidm @@ -21,6 +21,14 @@ spec: app.kubernetes.io/instance: kanidm spec: serviceAccountName: kanidm + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: kanidm + app.kubernetes.io/instance: kanidm + topologyKey: kubernetes.io/hostname securityContext: runAsUser: 1000 runAsGroup: 1000 -- 2.47.3 From 11286a1f89804f78526f30cafeb209683e5a6063 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 24 May 2026 00:02:40 +1000 Subject: [PATCH 3/4] 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 -- 2.47.3 From 7d2e0dfa0fbd7a822fdb63e14e359960b8d37791 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 24 May 2026 00:07:02 +1000 Subject: [PATCH 4/4] fix(kanidm): prevent ArgoCD from overwriting repl-cert ConfigMap data Remove the data keys from kanidm-repl-certs in git so ArgoCD never takes SSA ownership of them. Add ignoreDifferences for /data on that ConfigMap in the ApplicationSet template so ArgoCD doesn't flag sidecar-patched cert values as out-of-sync. --- apps/base/kanidm/configmap.yaml | 5 +---- argocd/applicationsets/platform.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/base/kanidm/configmap.yaml b/apps/base/kanidm/configmap.yaml index bd68434..d74ed18 100644 --- a/apps/base/kanidm/configmap.yaml +++ b/apps/base/kanidm/configmap.yaml @@ -37,7 +37,4 @@ metadata: labels: app.kubernetes.io/name: kanidm app.kubernetes.io/instance: kanidm -data: - kanidm-0: "" - kanidm-1: "" - kanidm-2: "" +data: {} diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index 8974f77..9b538c9 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -44,6 +44,12 @@ spec: destination: server: https://kubernetes.default.svc namespace: '{{path[3]}}' # Use directory name as namespace + ignoreDifferences: + - group: "" + kind: ConfigMap + name: kanidm-repl-certs + jsonPointers: + - /data syncPolicy: automated: prune: true -- 2.47.3