From ee76ec199b139a43fa45bf3c89e705981bc394db Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 23 May 2026 23:12:52 +1000 Subject: [PATCH] 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'