From dc42781a47d5023fe6b96c04ea09e673376eac78 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 18 Jul 2026 22:06:58 +0200 Subject: [PATCH] feat(authentik): automated SECRET_KEY rotation (openbao-ns) Rotates authentik-secrets:secret-key (Django signing key) quarterly + on-demand via the openbao-ns pattern (per-secret rotator-authentik role/policy, no netpol). Self-sync 2-sided: server + worker read the same secret; Reloader restarts both via global.podAnnotations. smtp-password in the same KV entry is preserved. externalsecret refreshInterval 1h->10m. BLAST RADIUS: invalidates active SSO sessions + in-flight signed links (re-login); downstream LDAP/proxy outposts unaffected. Requires the one-time OpenBao bootstrap. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015K7gLUVVnutvDnzV2TrMRJ --- kubernetes/apps/authentik/externalsecret.yaml | 5 +- kubernetes/apps/authentik/helmrelease.yaml | 6 ++ .../openbao/policies/rotator-authentik.hcl | 22 +++++ .../openbao/rotation/authentik.yaml | 97 +++++++++++++++++++ .../openbao/rotation/kustomization.yaml | 1 + 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 kubernetes/infrastructure/openbao/policies/rotator-authentik.hcl create mode 100644 kubernetes/infrastructure/openbao/rotation/authentik.yaml diff --git a/kubernetes/apps/authentik/externalsecret.yaml b/kubernetes/apps/authentik/externalsecret.yaml index 8a94dba1..88aff2a0 100644 --- a/kubernetes/apps/authentik/externalsecret.yaml +++ b/kubernetes/apps/authentik/externalsecret.yaml @@ -4,7 +4,10 @@ metadata: name: authentik-secrets namespace: authentik spec: - refreshInterval: 1h + # 10m (was 1h) so an automated secret-key rotation propagates within ~10m; + # Reloader then restarts server + worker. See + # kubernetes/infrastructure/openbao/rotation/authentik.yaml. + refreshInterval: 10m secretStoreRef: kind: ClusterSecretStore name: openbao diff --git a/kubernetes/apps/authentik/helmrelease.yaml b/kubernetes/apps/authentik/helmrelease.yaml index 0c23b743..9729b4a9 100644 --- a/kubernetes/apps/authentik/helmrelease.yaml +++ b/kubernetes/apps/authentik/helmrelease.yaml @@ -35,6 +35,12 @@ spec: retries: 3 values: global: + # Reloader restarts BOTH the authentik server and worker Deployments when + # authentik-secrets changes (e.g. after a SECRET_KEY rotation); global + # podAnnotations propagate to every component pod. See + # kubernetes/infrastructure/openbao/rotation/authentik.yaml. + podAnnotations: + secret.reloader.stakater.com/reload: authentik-secrets # Custom Authentik server image with the samba-bridge baked in. # The bridge synthesises sambaNTPassword (and the rest of the # Samba schema) on every User.set_password() call for any user diff --git a/kubernetes/infrastructure/openbao/policies/rotator-authentik.hcl b/kubernetes/infrastructure/openbao/policies/rotator-authentik.hcl new file mode 100644 index 00000000..d2740b16 --- /dev/null +++ b/kubernetes/infrastructure/openbao/policies/rotator-authentik.hcl @@ -0,0 +1,22 @@ +# OpenBao policy: rotator-authentik +# +# Least-privilege policy for the authentik secret-key rotation CronJob (kubernetes +# auth role `rotator-authentik`). Per-secret isolation — scoped to only the +# authentik KV path (issue #213 blast-radius, #202 per-app segregation). +# openbao-ns rotation pattern; see rotator-paperless.hcl for the template. +# +# NOT applied by Flux. Apply ONCE with an admin token: +# +# bao policy write rotator-authentik kubernetes/infrastructure/openbao/policies/rotator-authentik.hcl +# bao write auth/kubernetes/role/rotator-authentik \ +# bound_service_account_names=authentik-secret-rotator \ +# bound_service_account_namespaces=openbao \ +# policies=rotator-authentik ttl=15m +# +# Do NOT set token_no_default_policy=true (kv-v2 preflight needs the default policy). +# +# NOTE: the authentik KV entry also holds `smtp-password`; the Job patches only +# `secret-key`, and `bao kv patch` preserves smtp-password. +path "homelab/data/apps/authentik" { + capabilities = ["read", "patch"] +} diff --git a/kubernetes/infrastructure/openbao/rotation/authentik.yaml b/kubernetes/infrastructure/openbao/rotation/authentik.yaml new file mode 100644 index 00000000..ca377ff9 --- /dev/null +++ b/kubernetes/infrastructure/openbao/rotation/authentik.yaml @@ -0,0 +1,97 @@ +--- +# Automated rotation of AUTHENTIK_SECRET_KEY (Django signing key for the SSO IdP). +# openbao-ns pattern (intra-namespace OpenBao access, no NetworkPolicy). Writes +# OpenBao; ESO syncs the authentik-secrets Secret (ns authentik, 10m refresh); +# Reloader restarts BOTH authentik server + worker (via global.podAnnotations in +# the HelmRelease — both read the same secret-key, so ESO's one sync feeds both). +# +# BLAST RADIUS: rotating invalidates active authentik sessions and in-flight +# signed links (password-reset / invitation / email-verification tokens) — users +# re-login. Downstream SSO integrations (LDAP / proxy outposts) use separate +# token/TLS material, NOT SECRET_KEY, so they are unaffected. Prefer an off-hours +# window for on-demand runs. No data loss. +# +# ONE-TIME BOOTSTRAP (per rotator-authentik.hcl): apply the policy + role. +# ON-DEMAND: kubectl -n openbao create job --from=cronjob/authentik-secret-rotate authentik-secret-rotate-now +apiVersion: v1 +kind: ServiceAccount +metadata: + name: authentik-secret-rotator + namespace: openbao + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/component: secret-rotator + # OpenBao: auth/kubernetes/role/rotator-authentik binds this SA to the + # rotator-authentik policy. Talks only to OpenBao → no k8s RBAC needed. +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: authentik-secret-rotate + namespace: openbao + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/component: secret-rotator +spec: + schedule: "0 4 1 1,4,7,10 *" # quarterly, Europe/Berlin + timeZone: Europe/Berlin + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 2 + failedJobsHistoryLimit: 5 + startingDeadlineSeconds: 3600 + jobTemplate: + spec: + backoffLimit: 2 + activeDeadlineSeconds: 300 + ttlSecondsAfterFinished: 259200 # 3 d + template: + metadata: + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/component: secret-rotator + spec: + serviceAccountName: authentik-secret-rotator + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 100 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: rotate + image: quay.io/openbao/openbao:2.5.5 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + - name: HOME + value: /tmp + command: ["/bin/sh", "-ec"] + args: + - | + export BAO_ADDR=http://openbao.openbao.svc.cluster.local:8200 + BAO_TOKEN=$(bao write -field=token auth/kubernetes/login \ + role=rotator-authentik \ + jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)") + export BAO_TOKEN + # ~50-byte Django-style secret key. + NEW=$(head -c 50 /dev/urandom | base64 | tr -d '\n') + # Patch ONLY secret-key; smtp-password in the same KV entry untouched. + bao kv patch homelab/apps/authentik "secret-key=${NEW}" + echo "rotated secret-key at homelab/apps/authentik (value not logged)" + volumeMounts: + - name: tmp + mountPath: /tmp + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + memory: 64Mi + volumes: + - name: tmp + emptyDir: {} diff --git a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml index 675b19b6..c82f437a 100644 --- a/kubernetes/infrastructure/openbao/rotation/kustomization.yaml +++ b/kubernetes/infrastructure/openbao/rotation/kustomization.yaml @@ -7,3 +7,4 @@ kind: Kustomization resources: - paperless.yaml - woodpecker.yaml + - authentik.yaml -- 2.54.0