feat(authentik): automated SECRET_KEY rotation (openbao-ns) #302

Merged
k.renner merged 2 commits from feat/authentik-secret-rotation into master 2026-07-18 21:24:32 +00:00
5 changed files with 130 additions and 1 deletions
@@ -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
@@ -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
@@ -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"]
}
@@ -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: {}
@@ -8,3 +8,4 @@ resources:
- paperless.yaml
- woodpecker.yaml
- shelly-manager.yaml
- authentik.yaml