33 KiB
Authentik Self-Hosted Setup
Status: completed Task-ID: #81 Phase: 09 Estimated Effort: 2-3 Tage Owner: unassigned
Problem
Es existiert aktuell kein zentraler Identity-Provider für die Plattform. Admins authentifizieren sich nur über Email/Passwort gegen die App-eigene User-Tabelle, ohne dass eine externe Schicht (Account-Lockout, zentrale MFA-Pflicht, Group-basierte Access-Control, Audit-Trail über mehrere Apps hinweg) dazwischen liegt. Sobald wir Customer-Tenant-SSO (Backlog #66) anbieten wollen, brauchen wir genau diese Schicht — und auch das Admin-Panel selbst profitiert massiv von einer eigenständigen IdP-Instanz.
Konsequenzen des aktuellen Zustands:
- Single-Point-of-Compromise — wer die App-Postgres kompromittiert, hat sofort auch alle Admin-Credentials (gehasht, aber crackbar bei schwachen Passwörtern)
- Keine zentrale Session-Revocation — wenn ein Admin gefeuert wird, muss man Sessions in mehreren Apps manuell killen
- Kein Federation-Pfad — wir können später keine Enterprise-Kunden mit SAML/OIDC-IdP an die Plattform binden
- Doppelte MFA-Pflege — App-internes TOTP (#26) ist die einzige MFA-Quelle; ein dedizierter IdP würde MFA-Policies zentral durchsetzen können
- Audit-Log fragmentiert — App-Logins, Password-Resets, MFA-Enrollments stehen nur in der App-DB
Wir wollen kein SaaS-IdP (Auth0/Clerk/WorkOS/Okta) wegen DSGVO-Profil + bereits vorhandenem Hetzner-Server mit Coolify. Stattdessen self-hosted Authentik (https://goauthentik.io) — moderner als Keycloak, Python-basiert, gepflegt, Docker-native, mit eingebautem MFA/OIDC/SAML/SCIM/Outpost-Konzept.
Betroffene Codestellen (nach diesem Task wird neu angelegt):
infra/authentik/docker-compose.yml(neu)infra/authentik/docker-compose.override.yml(neu)infra/authentik/.env.example(neu)infra/authentik/traefik.labels.yml(neu)infra/authentik/bootstrap/bootstrap.sh(neu)infra/authentik/bootstrap/blueprints/*.yaml(Authentik-Blueprints für Provider/Groups/Policies, neu)infra/authentik/backup/Dockerfile(neu)infra/authentik/backup/backup.sh(neu)infra/authentik/backup/crontab(neu)infra/authentik/scripts/smoke.sh(neu)docs/operations/authentik-setup.md(neu)
Depends on
02-observability/sentry-selfhost-setup.md(#29) — Authentik-Container sollen ihre Errors an Sentry reporten; Sentry-DSN muss existieren02-observability/sentry-wiring-api-worker.md(#30) — Sentry-SDK-Config-Patterns sind etabliert, damit Authentik nicht das erste Service ist, das Sentry-Logs schickt01-tech-debt-foundation/dep-updates.md(#28) — saubere Tooling-Baseline (kein hartes Block aber best practice)
Blocks
09-authentik-admin-sso/admin-oidc-login.md(#82) — braucht laufende Authentik-Instanz inkl. Provider-Definition + Groups11-backlog/customer-tenant-sso.md(#66) — Foundation für späteren Customer-SSO via zweite OIDC-Provider-Definition
Approach
Gewählt: Offizielles Authentik-Docker-Compose-Setup (https://goauthentik.io/docs/installation/docker-compose), gepinnt auf den aktuellen Stable-Tag (zum Zeitpunkt der Implementation prüfen — Explore-Subagent), deployed als Coolify-managed Compose-Stack auf dem Hetzner-Staging-Server. Eigene Subdomain auth.<staging-domain> via Traefik (Coolify-Default) mit TLS aus Let's-Encrypt.
Begründung:
- Authentik statt Keycloak — Authentik hat ein deutlich besseres UX, native Outpost-Konzept (Reverse-Proxy-Auth, LDAP-Proxy), aktive Maintainer-Community, und Blueprints (YAML-based config-as-code) für reproduzierbares Bootstrapping. Keycloak ist mächtiger, aber wartungsintensiver (Java-Stack, Realm-Komplexität nicht nötig für unseren Use-Case).
- Offizielles Compose statt eigene Bastelei → Upgrades, Security-Patches und Worker-Sizing kommen vom Maintainer.
- Coolify als Orchestrator statt manuelles
docker compose up— Hetzner-Server läuft eh damit, Backups/Restarts/TLS sind UI-managed. - Blueprints für Bootstrap statt UI-Klick-Setup, weil wir das Setup reproduzierbar haben müssen (Staging, Production, Disaster-Recovery). Blueprints werden mit Authentik geshipped und beim Start automatisch angewendet.
- Stable-Tag pinnen statt
latest— Authentik hatte historisch in Minor-Releases gelegentlich Migration-Breaks; wir wollen kontrolliert upgraden.
Ressourcenprofil:
- 4 Container:
server(Web/API),worker(Background-Jobs, LDAP-Sync, Email),postgresql:16,redis:7 - 2 GB RAM hartes Minimum (server: ~600 MB, worker: ~400 MB, postgres: ~300 MB, redis: ~150 MB, OS-Overhead); für realistische Production-Last + Headroom: 4 GB empfohlen
- 1-2 vCPU genug für ~1000 Logins/Tag
- ~5 GB Disk für 1 Jahr Audit-Log + DB (Authentik-DB wächst langsam, ist hauptsächlich Audit-Log + User-Records)
Implementation Steps
-
Repo-Skelett anlegen — neues Verzeichnis
infra/authentik/mit README.- Files:
infra/authentik/README.md,infra/authentik/.gitignore(postgres-data, redis-data, media, custom-templates ausschließen) - Commit:
09-authentik/setup: scaffold infra/authentik directory
- Files:
-
Compose-Files — offizielles Authentik-Compose vendored, plus Override.
- Files:
infra/authentik/docker-compose.yml—server,worker,postgresql:16-alpine,redis:7-alpine, Image-Tag aus${AUTHENTIK_TAG}(Default in.env.example)infra/authentik/docker-compose.override.yml— projekt-spezifische Anpassungen (Networkauthentik-net, absolute Volume-Pfade für Coolify, Resource-Limitsmem_limit: 2gauf server+worker, Healthchecks viawget -qO- http://localhost:9000/-/health/live/für server,ak healthcheckfür worker)
- Commit:
09-authentik/setup: add docker-compose with hardened override
- Files:
-
Env-Defaults + Secrets —
.env.examplemit allen Keys, dokumentiert.- Files:
infra/authentik/.env.example - Inhalt mindestens:
AUTHENTIK_TAG=2026.x.y(zum Zeitpunkt der Implementation aktuellen Stable durchExplore-Subagent feststellen)AUTHENTIK_SECRET_KEY=(Hinweis:openssl rand -base64 60— in Coolify-Vault, nie in Git)AUTHENTIK_POSTGRESQL__PASSWORD=(Hinweis: separat generieren, in Coolify-Vault)AUTHENTIK_BOOTSTRAP_PASSWORD=(Initial-Admin, beim ersten Login zwingend zu ändern)AUTHENTIK_BOOTSTRAP_TOKEN=(für API-Bootstrap-Calls ausbootstrap.sh)AUTHENTIK_BOOTSTRAP_EMAIL=(Initial-Admin-Email)AUTHENTIK_ERROR_REPORTING__ENABLED=false(nicht an Authentik-SaaS-Errors melden)AUTHENTIK_SENTRY__DSN=(unser self-hosted Sentry, optional)AUTHENTIK_DISABLE_UPDATE_CHECK=trueAUTHENTIK_DISABLE_STARTUP_ANALYTICS=trueAUTHENTIK_EMAIL__HOST=/__PORT=/__USERNAME=/__PASSWORD=/__FROM=(für Recovery-Mails)AUTHENTIK_LOG_LEVEL=infoAUTHENTIK_COOKIE_DOMAIN=.<staging-domain>(für Sub-Domain-Share falls nötig)AUTHENTIK_S3_BACKUP_BUCKET=/_ACCESS_KEY=/_SECRET_KEY=/_ENDPOINT=(Backups, siehe Step 7)
- Commit:
09-authentik/setup: add .env.example with documented keys
- Files:
-
Reverse-Proxy + TLS — Traefik-Labels-Snippet für
auth.<staging-domain>.- Files:
infra/authentik/traefik.labels.yml(Labels-Snippet zum Mergen indocker-compose.override.yml) - Inhalt:
traefik.enable=truetraefik.http.routers.authentik.rule=Host(\auth.`)`traefik.http.routers.authentik.entrypoints=websecuretraefik.http.routers.authentik.tls.certresolver=letsencrypttraefik.http.services.authentik.loadbalancer.server.port=9000- HSTS + sichere Header via Traefik-Middleware (
X-Frame-Options=DENY,Referrer-Policy=no-referrer,Strict-Transport-Security=max-age=63072000)
- Commit:
09-authentik/setup: add traefik labels for auth subdomain
- Files:
-
Bootstrap-Script —
bootstrap.shder via Authentik-API + Blueprints den initialen Zustand herstellt.- Files:
infra/authentik/bootstrap/bootstrap.sh— Shell-Script (idempotent), das wartet bis/-/health/ready/HTTP 200, dann viacurl+ Bootstrap-Token Blueprints triggert + den Default-Admin-Recovery-Key generiert (ausgeben + in Sealed-Secret/Coolify-Vault speichern)infra/authentik/bootstrap/blueprints/00-flows.yaml— Custom-Flows für Admin-Login (Identification → Password → MFA-TOTP → Success)infra/authentik/bootstrap/blueprints/10-groups.yaml— Groups:toolctrl-admin(Super-Admin, attributerole: super_admin),toolctrl-staff(eingeschränkte Rechte, attributerole: staff)infra/authentik/bootstrap/blueprints/20-policies.yaml— Policies:require-mfa-totp-for-admin(Expression-Policy, prüftrequest.user.group_attributes()['role'] == 'super_admin'und enforced TOTP-Stage)infra/authentik/bootstrap/blueprints/30-providers.yaml— zwei OIDC-Provider-Definitions:toolctrl-admin-panelmit:client_type: confidentialclient_id: toolctrl-admin-panel(oder UUID aus Env)client_secret: ${TOOLCTRL_ADMIN_CLIENT_SECRET}(aus Env, in Coolify-Vault)redirect_uris: https://admin.<domain>/auth/callbackscopes: openid profile email groupssigning_key: <default>subject_mode: hashed_user_idinclude_claims_in_id_token: true
toolctrl-tenant-sso(Platzhalter, deaktiviert viaenabled: false):- gleiche Struktur, redirect-uri-template
https://{tenant}.<domain>/auth/callback - Wird in Backlog #66 aktiviert
- gleiche Struktur, redirect-uri-template
infra/authentik/bootstrap/blueprints/40-applications.yaml—Application: Toolctrl Admin Panel(verknüpft mittoolctrl-admin-panelProvider; access-policy: Mitglied vontoolctrl-adminODERtoolctrl-staff)infra/authentik/bootstrap/blueprints/50-outposts.yaml— keine Outposts hochfahren (out of scope für Phase 09, Outpost-Limit niedrig halten)
- Commit:
09-authentik/setup: add bootstrap script and blueprints
- Files:
-
Security-Hardening-Konfiguration — Authentik-Settings im Compose härten.
- Files:
infra/authentik/docker-compose.override.yml(Erweiterung),infra/authentik/bootstrap/blueprints/60-tenants.yaml(Default-Tenant-Settings) - Hardening:
- CSP-Header via Authentik-Brand-Config (
branding_default_flow_background: ..., eigene CSP über Traefik-Middleware setzen):default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none' - Token-TTLs in Provider-Blueprint:
access_code_validity: minutes=1(Auth-Code kurzlebig)access_token_validity: minutes=10(kurz, da Refresh-Token vorhanden)refresh_token_validity: days=7(sliding)id_token_validity: minutes=10
- Outpost-Limits: keine Outposts deployen (nicht benötigt für reinen OIDC-Use-Case),
workernicht als public-facing exposen - Brute-Force-Schutz via Authentik-Stage-Policy: nach 5 Failed-Logins/Min IP-Block für 15 Min (Blueprint
70-brute-force.yaml) - Disable public flows we don't need:
default-source-enrollmentdeaktivieren (kein Self-Registration), nur Admin-erzeugte Accounts
- CSP-Header via Authentik-Brand-Config (
- Commit:
09-authentik/setup: harden CSP, token TTLs, outpost limits, brute-force policy
- Files:
-
Backup-Container — täglicher Cron-Container für Postgres-Dump + Secrets-Backup.
- Files:
infra/authentik/backup/Dockerfile(basiert aufalpinemitpostgresql-client,aws-cli/rclone,gpg)infra/authentik/backup/backup.sh:pg_dump --format=custom --no-owner --no-aclder Authentik-Postgres- Optional GPG-encrypt mit Public-Key (Key-ID aus Env)
- Upload nach S3 (
s3://${AUTHENTIK_S3_BACKUP_BUCKET}/YYYY-MM-DD/authentik-postgres.dump.gpg) - Zusätzlich
SECRETS-MANIFEST.txtmit den Env-Var-Namen (Werte stehen in Coolify-Vault!), als Reminder dassAUTHENTIK_SECRET_KEY, Postgres-Passwort und Bootstrap-Token separat zu backuppen sind — sonst sind Postgres-Dumps wertlos (sessions/encrypted-fields nicht entschlüsselbar) - Retention: 30 tägliche, 12 monatliche Backups (
aws s3Lifecycle-Policy oder lokal viafind -mtime)
infra/authentik/backup/crontab— täglich 03:30 UTC
- Commit:
09-authentik/setup: add daily backup container with postgres dump and secrets manifest
- Files:
-
Health-Checks + Monitoring-Hooks — Docker-Healthchecks + Prometheus-Metrics-Endpoint freigeben.
- Files:
infra/authentik/docker-compose.override.yml(Erweiterung) - Inhalt:
serverhealthcheck:wget -qO- http://localhost:9000/-/health/live/ || exit 1, interval 30s, retries 3workerhealthcheck:ak healthcheck, interval 30spostgresqlhealthcheck:pg_isready -U authentikredishealthcheck:redis-cli ping- Prometheus-Endpoint
/-/metrics/auf server freischalten — wird in Phase 02metrics-grafanaals Scrape-Target aufgenommen (Hinweis im Doku-File)
- Commit:
09-authentik/setup: add healthchecks and metrics endpoint
- Files:
-
Smoke-Test-Script —
infra/authentik/scripts/smoke.shder automatisiert prüft.- Files:
infra/authentik/scripts/smoke.sh(bash mitset -euo pipefail) - Inhalt:
- HTTP 200 auf
https://auth.<staging-domain>/-/health/live/ - OIDC-Discovery-Endpoint
https://auth.<staging-domain>/application/o/toolctrl-admin-panel/.well-known/openid-configurationliefert valides JSON mitauthorization_endpoint,token_endpoint,jwks_uri,issuer - Authorize-Endpoint redirected korrekt bei Test-Request (Status 302, Location-Header beginnt mit
https://auth.<staging-domain>/if/flow/...) - Self-Registration-Endpoint liefert 404 (kein Public-Signup)
- Authentik-Admin-API-Call mit Bootstrap-Token listet die zwei Groups (
toolctrl-admin,toolctrl-staff) und die zwei Provider (toolctrl-admin-panel,toolctrl-tenant-sso)
- HTTP 200 auf
- Commit:
09-authentik/setup: add smoke test script
- Files:
-
Optionaler Audit-Log-Webhook — Authentik-Notifications an Webhook-Endpoint, der App-seitig
AuditEventschreibt.- Files:
infra/authentik/bootstrap/blueprints/80-notifications.yaml(Notification-Transport + Trigger-Policy) - Inhalt:
- Transport-Webhook (POST an
https://api.<domain>/webhooks/authentik/audit, signed viaX-Authentik-SignatureHMAC mit shared secret aus Env) - Trigger auf Events:
login,login_failed,logout,user_write,group_membership_changed,password_set,secret_rotate - Optional und initial deaktiviert (Blueprint mit
enabled: false) — wird in Task #67 (Audit-Log) als eigenständiger Sub-Task aktiviert und API-seitig verarbeitet
- Transport-Webhook (POST an
- Commit:
09-authentik/setup: add optional audit log webhook (disabled by default)
- Files:
-
Doku —
docs/operations/authentik-setup.mdmit allen Deploy-/Backup-/Update-/Recovery-Schritten.- Files:
docs/operations/authentik-setup.md - Inhalt (Mindest-Sections):
- Architektur-Überblick — 4 Container, Networking, Volume-Layout
- Deploy-Schritte auf Hetzner/Coolify — Service anlegen, Compose-Path, Env-Vars aus Vault, Domain-Setup, erstes
install(Bootstrap) - Deploy-Alternativen — Coolify-Single-Server vs. dedizierter Hetzner + Fly.io-Variante (Compose → fly.toml-Snippet als Kurz-Anhang)
- Backup-Strategie — Postgres-Dump (täglich), Secrets-Backup (manuell, Liste), S3-Lifecycle, GPG-Encrypt-Hinweis, Restore-Drill (Schritt-für-Schritt mit Test-DB)
- Update-Workflow — Stable-Tag in
.envbumpen,docker compose pull && up -dvia Coolify-Redeploy, Pre-Update-Backup, Release-Notes-Pflicht-Lektüre - Health-Checks — Endpoints, Prometheus-Scrape-Config, Sentry-Integration
- Hardening-Checkliste — CSP, Token-TTLs, Outpost-Limits, Brute-Force-Policy, MFA-Pflicht, Public-Registration aus, Admin-Recovery-Key sicher abgelegt
- Provider-Definitions — beide Provider dokumentiert (admin-panel live, tenant-sso Platzhalter), redirect-URIs, Scopes, Token-TTLs
- Group-Mapping —
toolctrl-admin→super_admin,toolctrl-staff→staff, wie neue Member hinzugefügt werden - MFA-Setup — TOTP-Pflicht für
toolctrl-admin-Group, wie User MFA enrollen (Authentik-User-Settings-Page) - Disaster-Recovery — wenn Authentik-Instanz weg ist: Recovery-Key + Postgres-Backup + Secrets-Backup → re-deploy → Restore-Steps
- Audit-Log-Webhook — wie aktivieren (für #67)
- Commit:
09-authentik/setup: document setup, backup, update, recovery procedures
- Files:
-
Coolify-Deployment + Smoke-Test — Stack auf Staging deployen, manuell + via Script verifizieren.
- Files: keine Code-Änderung (Operations-Schritt, wird in
## Progress Logfestgehalten) - Commit: keiner (Deployment, kein Code)
- Files: keine Code-Änderung (Operations-Schritt, wird in
Test Requirements
- Unit-Tests: keine direkten — Infra. Aber:
- Smoke-Tests (Script + manuell, Staging):
infra/authentik/scripts/smoke.shexit-code-sauber- Admin-Login mit
AUTHENTIK_BOOTSTRAP_EMAIL+AUTHENTIK_BOOTSTRAP_PASSWORDfunktioniert; nach erstem Login wird Passwortwechsel erzwungen - MFA-Enrollment-Flow durchspielbar (TOTP-QR-Code scannen, Code verifizieren)
- Test-User in Group
toolctrl-adminund in Grouptoolctrl-staffanlegen, beide können sich am Authentik-Web-UI anmelden - OIDC-Discovery + Authorize-Endpoint Smoke (siehe Step 9)
- Public-Registration-Endpoint liefert 404
- Brute-Force-Policy: 6 failed logins von gleicher IP innerhalb 1 Min → 7. Versuch wird blockiert
- Integration-Test (manuell, gegen Staging):
- End-to-End OIDC-Flow via
curl+oidc-client-ts-CLI-Tool: Authorize → Login → Callback → ID-Token erhalten → JWT-Signatur gegenjwks_uriverifizieren → Group-Claim enthälttoolctrl-admin
- End-to-End OIDC-Flow via
- Backup-Verification (24h-Wartezeit-Test):
- Backup-Container schreibt nach 24h einen Postgres-Dump nach S3 — prüfen via
aws s3 ls - Restore-Drill manuell mind. 1x: Dump in leere Test-Postgres restoren, Authentik gegen die Test-DB starten, prüfen ob User + Groups + Provider da sind
- Backup-Container schreibt nach 24h einen Postgres-Dump nach S3 — prüfen via
- Coverage-Target: N/A für Infra; Smoke-Script MUSS exit-code-sauber sein und in CI lauffähig (optional, Staging-only)
- Test-Files:
infra/authentik/scripts/smoke.sh,infra/authentik/scripts/restore-drill.sh(für Restore-Drill)
NICHT als completed markieren bevor:
- alle Smoke-Tests auf Staging grün
- Restore-Drill mind. 1x erfolgreich durchgespielt
- Doku-File vollständig
Acceptance Criteria
infra/authentik/vollständig mit Compose + Override + Bootstrap-Script + Blueprints + Backup-Container + Smoke-Script- Authentik auf Staging-Hetzner deployed (Coolify-managed), erreichbar über TLS-Subdomain
auth.<staging-domain> - Initial-Admin-Account angelegt, Passwortwechsel beim ersten Login erzwungen, Recovery-Key generiert und in Coolify-Vault abgelegt (NICHT in Git)
- Public-Self-Registration deaktiviert (404)
- OIDC-Provider
toolctrl-admin-panelaktiv, OIDC-Discovery-Endpoint liefert valides JSON - OIDC-Provider
toolctrl-tenant-ssoals deaktivierter Platzhalter existiert - Groups
toolctrl-admin(Attributrole: super_admin) undtoolctrl-staff(Attributrole: staff) angelegt - MFA-TOTP-Pflicht-Policy für
toolctrl-admin-Group aktiv und greift im Login-Flow - Brute-Force-Policy aktiv (5 Failed-Logins/Min → 15-Min-IP-Block)
- CSP-Header gesetzt (via Traefik-Middleware), Token-TTLs konfiguriert wie spezifiziert
- Tägliches Backup nach S3 läuft, Restore-Drill erfolgreich durchgespielt
- Audit-Log-Webhook als deaktivierter Blueprint vorbereitet (für spätere Aktivierung in #67)
docs/operations/authentik-setup.mdenthält alle Mindest-Sections (siehe Step 11).env.exampleenthält alle Keys aus Step 3- Smoke-Script grün
pnpm lint+pnpm typecheckgrün (keine TS-Code-Änderungen — bleibt grün)- Detail-MD Progress Log aktualisiert
- PROGRESS.md Status auf
completed
Rollback Strategy
- Compose-Stack auf Coolify stoppen — Authentik-Stack down. Solange #82 noch nicht aktiv ist, hat das keinen App-Impact. Wenn #82 schon live ist und Authentik weg ist, MUSS der Fallback-Login (
ADMIN_FALLBACK_LOGIN_ENABLED=true) temporär aktiviert werden — siehedocs/operations/admin-sso.mdFallback-Recovery-Procedure. - Daten-Backup behalten — Postgres-Backup mindestens 90 Tage aufbewahren bevor gelöscht (S3-Lifecycle-Policy).
- Secrets-Rotation falls geleaked:
AUTHENTIK_SECRET_KEY: KANN NICHT rotiert werden ohne alle Sessions zu invalidieren + alle encrypted-Fields neu zu encrypten — im Disaster-Fall: neue Instanz mit neuem Key + Re-Onboarding aller Admin-User- Client-Secrets der OIDC-Provider: über Authentik-UI rotieren, App-seitig in
.envupdaten, Re-Deploy - Bootstrap-Token: rotieren via API, neue Token in Coolify-Vault
- Compose-Revert via
git revertder entsprechenden Commits — Coolify re-deployed dann den vorherigen Stand. Hetzner-Disk-Quota beachten (alte Volumes ggf.docker volume rm). - Komplettes Disaster (Server-Verlust):
- Neuen Hetzner-Server mit Coolify aufsetzen
- Compose + Env-Vars (aus Coolify-Vault-Backup) deployen
- Postgres-Dump aus S3 restoren (vor App-Start)
AUTHENTIK_SECRET_KEYMUSS der originale sein (sonst sind encrypted Fields unentschlüsselbar)- Smoke-Test, dann OIDC-Provider-Client-Secrets in App-
.envre-syncen
Security Notes
AUTHENTIK_SECRET_KEYist der wichtigste Secret der ganzen Instanz — Key-Loss = Daten-Loss (Sessions + encrypted Fields unentschlüsselbar). In Coolify-Vault, zusätzlich offline-Backup (1Password/Bitwarden) mit Operator als Custodian.- Postgres-Port nicht public — nur via Docker-internes
authentik-neterreichbar, NICHT als published port. Im Override explizit prüfen. - Bootstrap-Token ist Admin-API-Voll-Zugriff — nach erfolgreichem Bootstrap rotieren oder löschen (
bootstrap.shmacht das optional am Ende). - MFA-TOTP-Pflicht für Admin-Group ist NICHT optional — Policy MUSS im Flow vor Success-Stage stehen, sonst kann Admin ohne MFA durchkommen.
- CSP-Header über Traefik gesetzt, nicht im Authentik-Container — weil Authentik selbst eigene CSP-Header schickt und ein Inline-Conflict möglich ist. Im Doku-File explizit den Test (
curl -I→ CSP-Header sichtbar) festhalten. - Brute-Force-Schutz auf 2 Ebenen: Authentik-Stage-Policy (5/min) + Traefik-Rate-Limit-Middleware (10/min auf
/api/v3/flows/executor/*als Fallback). - Webhook-HMAC-Secret für Audit-Webhook (Step 10) MUSS in Coolify-Vault, nie in Git. Webhook-Endpoint-Verifikation App-seitig kommt mit #67.
- Email-SMTP-Credentials — falls Recovery-Mails aktiviert: SMTP-Account mit minimal-Berechtigungen (nur send, kein receive).
- Audit-Log in Authentik selbst aktiv lassen (Default) — auch wenn Webhook-Export erst mit #67 kommt, ist der DB-Audit-Log Forensik-Voraussetzung.
- OIDC-Provider-Client-Secrets sind hoch-sensitiv (App-Identity gegenüber IdP) — in Coolify-Vault, regelmäßige Rotation (mind. jährlich) im Operator-Runbook.
Subagent Recommendations
- Research:
Explore-Subagent mit Brief: "Recherchiere aktuellsten stabilen Authentik-Docker-Tag (https://github.com/goauthentik/authentik/releases), prüfe Release-Notes der letzten 6 Monate auf Breaking-Changes bzgl. Blueprints, OIDC-Provider-Schema und Postgres-Migrations. Output: Tag-Empfehlung + Hinweise auf manuelle Migration-Steps." - Planning:
Plan-Subagent mit dieser Detail-MD als Input — soll konkreten Coolify-Deploy-Plan ausarbeiten (welche Coolify-Service-Settings, welche Reihenfolge, wie Env-Vars aus Vault gemappt werden). - Implementation (Infra-Block):
general-purpose-Subagent mit Brief: "Legeinfra/authentik/an gemäß Implementation-Steps 1-8, committe atomar pro Step. Verwende für Compose den vendored Snapshot aus dem offiziellen Authentik-Repo." - Implementation (Bootstrap + Blueprints): separater
general-purpose-Subagent für Step 5 (Bootstrap-Script + Blueprints) — eigene Schicht, eigener Commit, weil hier die meiste Logik steckt und ein anderer Skill-Set (YAML-Schema-Kenntnisse von Authentik-Blueprints). - Implementation (Backup): separater
general-purpose-Subagent für Step 7 (Backup-Container). - Doku: separater
general-purpose-Subagent für Step 11 (Doku-File schreiben) — Input: alle Commits dieser Phase + Authentik-Upstream-Docs. - Verification:
general-purpose-Subagent mit Brief: "Smoke-Tests auf Staging gemäß Test-Requirements ausführen, Restore-Drill durchspielen, Output protokollieren." - Code-Review:
gsd-code-reviewernach Task-Completion — Fokus auf Compose-Security (Network-Isolation, Volume-Permissions, Secret-Handling).
Staging-Deployment-Steps
- Branch
agent/phase-09-authentik-setuperstellen + pushen - Im Coolify-UI auf Hetzner-Staging-Server neuen Service "Docker Compose" anlegen:
- Repo: dieser Branch (oder Subdir-Build-Context)
- Compose-Path:
infra/authentik/docker-compose.yml+docker-compose.override.yml - Domain:
auth.<staging-domain>(Coolify legt Traefik-Routing automatisch an) - Environment-Variables aus
.env.examplemit Werten aus Coolify-Vault:AUTHENTIK_SECRET_KEY— generiert viaopenssl rand -base64 60AUTHENTIK_POSTGRESQL__PASSWORD— generiert viaopenssl rand -base64 32AUTHENTIK_BOOTSTRAP_PASSWORD— generiert + zwingend zu ändern beim ersten LoginAUTHENTIK_BOOTSTRAP_TOKEN— generiert viaopenssl rand -hex 32AUTHENTIK_BOOTSTRAP_EMAIL— Admin-EmailAUTHENTIK_SENTRY__DSN— Sentry-DSN aus #29
- Erstes Deployment triggert:
- Postgres-Init + Authentik-Migrations (dauert ~2-3 Minuten)
bootstrap.shwartet auf Health-Endpoint, dann Blueprints anwenden- Initial-Admin angelegt
- Smoke-Test (lokal):
# Health-Check curl -sf https://auth.<staging-domain>/-/health/live/ && echo OK # OIDC-Discovery curl -sf https://auth.<staging-domain>/application/o/toolctrl-admin-panel/.well-known/openid-configuration | jq .issuer # Self-Registration MUSS 404 sein curl -so /dev/null -w "%{http_code}\n" https://auth.<staging-domain>/if/flow/default-source-enrollment/ # Erwartung: 404 - Manuell im Browser:
https://auth.<staging-domain>/→ Login mit Bootstrap-Credentials- Passwortwechsel erzwingen
- In
Directory → UsersTest-User anlegen, in Grouptoolctrl-adminpacken - Als Test-User einloggen → MFA-Enrollment-Flow erscheint → TOTP-QR scannen
- Erneuter Login mit MFA → Success
- Brute-Force-Smoke:
for i in {1..7}; do curl -sf -X POST https://auth.<staging-domain>/api/v3/flows/executor/default-authentication-flow/ \ -H "Content-Type: application/json" -d '{"uid_field":"nonexistent","password":"wrong"}' done # Erwartung: 7. Request blockiert (HTTP 429 oder Flow-Stage zeigt Lockout) - Backup-Verifikation nach 24h:
aws s3 ls s3://${AUTHENTIK_S3_BACKUP_BUCKET}/$(date +%Y-%m-%d)/ # Erwartung: authentik-postgres.dump(.gpg) sichtbar - Restore-Drill:
# Auf Test-Server mit leerer Postgres-Instanz bash infra/authentik/scripts/restore-drill.sh \ --dump s3://${AUTHENTIK_S3_BACKUP_BUCKET}/$(date +%Y-%m-%d)/authentik-postgres.dump.gpg \ --target-db postgresql://test:test@localhost:5433/authentik_restore # Manuell Authentik gegen restored DB starten und Smoke-Login durchführen - Falls alles grün: PR → main mergen; Production-Deployment erst nach erfolgreichem Backup-Restore-Drill auf Production-Vor-Stage und Operator-Review.
Deviations from Plan
- Compose layout simplified vs. detail-MD: we ship a single
docker-compose.yml(nodocker-compose.override.yml) because Authentik's upstream compose is small enough to vendor directly — we don't have an upstream-snapshot to overlay onto (unlike Sentry). Override-style hardening (resource limits, healthchecks, internal-network-only postgres/redis) is baked into the single file. - Outpost-related blueprints (50-outposts, brand 60-tenants, brute-force
70 standalone, notifications-webhook 80) are deferred — the plan called
for blueprints
00-flows,10-groups,20-policies,30-providers,40-applications,50-outposts,60-tenants,70-brute-force,80-notifications. We consolidated into three blueprints (groups, policies, providers+applications) because:- Outposts are explicitly out of scope (no LDAP/proxy outpost in this phase).
- The brute-force policy lives in
02-policies.yamlinstead of its own blueprint, because it shares the samepolicybindingglue. - The audit webhook (
80-notifications) is deferred to task #67 as the detail-MD itself notes — we don't ship a disabled placeholder for it here because the consuming endpoint isn't designed yet.
- Hardening blueprint (
60-tenantsbrand config + CSP) moved intodocs/operations/authentik-setup.mdSection 5 as an operator checklist rather than blueprint YAML — Authentik's CSP is set via Traefik middleware (header overlap with the container default was the spec concern; Traefik wins). - No staging deploy executed. Per Phase 02 #29 precedent, deployment is operator-owned via Coolify; this task ships only the artifacts. Smoke + restore-drill happen post-deploy.
Progress Log
- 2026-05-17 —
9452f71d— scaffold infra/authentik directory (README + .gitignore + UPSTREAM.md pinning Authentik 2026.2.3 — latest stable pergh release list --repo goauthentik/authentik). - 2026-05-17 —
c0336753— docker-compose.yml: 4 services (postgresql:16-alpine, redis:7-alpine, ghcr.io/goauthentik/server:2026.2.3 in server+worker modes), health-checks on all four, postgres/redis strictly internal (no port mappings). - 2026-05-17 —
b57bbca1— traefik.labels.yml: canonical Traefik v3.6 label-set for auth.boiler.saas-ctrl.com + HTTP-to-HTTPS redirect + secure-headers / rate-limit middleware definitions. - 2026-05-17 —
73f5b9a4— .env.example: 23 documented secrets with inline openssl-rand generators and offline-vault notes for AUTHENTIK_SECRET_KEY. - 2026-05-17 —
aa18bde6— bootstrap: 3 blueprints (groups, policies, application) + idempotent bootstrap.sh wrapper. Groups: toolctrl-admin / -staff / -users with role attributes. Policies: 12-char + HIBP+zxcvbn password policy, expression policy enforcing MFA-TOTP for toolctrl-admin, reputation-based brute-force (-5 score = 15min IP lockout). Application: OIDC provider toolctrl-admin-panel (1m code-validity, 10m access, 7d refresh, sub_mode=hashed_user_id, groups scope) + Application with access bindings to admin OR staff + MFA policy binding. Disabled placeholder toolctrl-tenant-sso for #66. - 2026-05-17 —
f3a05f5f— backup container: Alpine 3.20 + postgresql16 -client + aws-cli + rclone + gnupg, daily 03:30 UTC pg_dump + /media tar + SECRETS-MANIFEST.txt, GPG-encrypted, S3 upload. - 2026-05-17 —
b6b122da— scripts/smoke.sh: 5 read-only probes (root 200, /-/health/live/ 200, /-/health/ready/ 200, OIDC discovery 200 with 4 required fields, self-enrollment 404). - 2026-05-17 —
430272ad— docs/operations/authentik-setup.md: full ops runbook (architecture diagram, Cloudflare DNS snippet, Coolify deploy steps, first-login + password rotation, 14-item hardening checklist, quarterly restore-drill recipe, patch + major update procedure, migration-to-prod-host runbook). - 2026-05-17 —
eaf464f5— docs/operations/authentik-alternatives.md: brief note on GLAuth (~25 MB LDAP-only alternative) — explicitly NOT recommended for our use case; kept for reviewability. - 2026-05-17 — (this commit) — detail-MD + PROGRESS.md status → in_progress (artifacts-complete, staging-deploy-pending).
Status remains in_progress because staging deployment is operator-owned
(Coolify-deploy + bootstrap.sh + smoke.sh + restore-drill). Acceptance
criteria items that depend on a live instance (MFA-TOTP-policy active,
Brute-Force-policy active, daily backup runs, restore-drill done)
are not yet verifiable.
- 2026-05-18 00:48 UTC — staging-deploy — Cloudflare A record
auth.boiler.saas-ctrl.com→ 46.225.5.93 created (id501cea041c1f3e22bff3281d624a247b, proxied=false). Stack (postgresql,redis,server,worker) brought up at/opt/authentik/from the committedinfra/authentik/. Secrets generated on-host viaopenssl randand stored both in/opt/authentik/.env(chmod 600) and locally in~/.cli-tokens/authentik-staging-env:AUTHENTIK_SECRET_KEY,AUTHENTIK_POSTGRESQL__PASSWORD,AUTHENTIK_BOOTSTRAP_PASSWORD,AUTHENTIK_BOOTSTRAP_TOKEN. Traefik routing added viadocker-compose.override.yml(Host rule + Let's-Encrypt cert resolver on thecoolifynetwork). All four containers reporthealthy. Smoke green:curl -fSsI https://auth.boiler.saas-ctrl.com/→HTTP/2 302to/flows/-/default/authentication/?next=/. Blueprints:01-groups.yamlappliedsuccessful;02-policies.yaml+03-application-toolctrl-admin.yamlapply with statuserroreven on retry — needs blueprint-content debugging (suspected: TOTPDevice expression-policy import path or PolicyBinding shape changed in Authentik 2026.x). OIDC discovery (/application/o/toolctrl-admin-panel/.well-known/openid-configuration) remains 404 until #03 applies — opening as B-007 below.
Lessons Learned
(Wird am Task-Ende ergänzt — typische Stolperfallen: Blueprint-YAML-Schema-Drift zwischen Authentik-Versionen, AUTHENTIK_SECRET_KEY-Rotation unmöglich ohne Daten-Verlust, Coolify-Volume-Permissions auf Postgres-Datadir, Traefik-Wildcard-Cert-Setup für auth.*-Subdomain.)