Files
HiveGrid-Wireframes/hivegrid-cloud/docs/improvement-plan/09-authentik-admin-sso/authentik-setup.md
T

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 existieren
  • 02-observability/sentry-wiring-api-worker.md (#30) — Sentry-SDK-Config-Patterns sind etabliert, damit Authentik nicht das erste Service ist, das Sentry-Logs schickt
  • 01-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 + Groups
  • 11-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

  1. 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
  2. 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 (Network authentik-net, absolute Volume-Pfade für Coolify, Resource-Limits mem_limit: 2g auf server+worker, Healthchecks via wget -qO- http://localhost:9000/-/health/live/ für server, ak healthcheck für worker)
    • Commit: 09-authentik/setup: add docker-compose with hardened override
  3. Env-Defaults + Secrets — .env.example mit allen Keys, dokumentiert.

    • Files: infra/authentik/.env.example
    • Inhalt mindestens:
      • AUTHENTIK_TAG=2026.x.y (zum Zeitpunkt der Implementation aktuellen Stable durch Explore-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 aus bootstrap.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=true
      • AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
      • AUTHENTIK_EMAIL__HOST= / __PORT= / __USERNAME= / __PASSWORD= / __FROM= (für Recovery-Mails)
      • AUTHENTIK_LOG_LEVEL=info
      • AUTHENTIK_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
  4. Reverse-Proxy + TLS — Traefik-Labels-Snippet für auth.<staging-domain>.

    • Files: infra/authentik/traefik.labels.yml (Labels-Snippet zum Mergen in docker-compose.override.yml)
    • Inhalt:
      • traefik.enable=true
      • traefik.http.routers.authentik.rule=Host(\auth.`)`
      • traefik.http.routers.authentik.entrypoints=websecure
      • traefik.http.routers.authentik.tls.certresolver=letsencrypt
      • traefik.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
  5. Bootstrap-Script — bootstrap.sh der 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 via curl + 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, attribute role: super_admin), toolctrl-staff (eingeschränkte Rechte, attribute role: staff)
      • infra/authentik/bootstrap/blueprints/20-policies.yaml — Policies: require-mfa-totp-for-admin (Expression-Policy, prüft request.user.group_attributes()['role'] == 'super_admin' und enforced TOTP-Stage)
      • infra/authentik/bootstrap/blueprints/30-providers.yaml — zwei OIDC-Provider-Definitions:
        • toolctrl-admin-panel mit:
          • client_type: confidential
          • client_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/callback
          • scopes: openid profile email groups
          • signing_key: <default>
          • subject_mode: hashed_user_id
          • include_claims_in_id_token: true
        • toolctrl-tenant-sso (Platzhalter, deaktiviert via enabled: false):
          • gleiche Struktur, redirect-uri-template https://{tenant}.<domain>/auth/callback
          • Wird in Backlog #66 aktiviert
      • infra/authentik/bootstrap/blueprints/40-applications.yaml — Application: Toolctrl Admin Panel (verknüpft mit toolctrl-admin-panel Provider; access-policy: Mitglied von toolctrl-admin ODER toolctrl-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
  6. 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), worker nicht 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-enrollment deaktivieren (kein Self-Registration), nur Admin-erzeugte Accounts
    • Commit: 09-authentik/setup: harden CSP, token TTLs, outpost limits, brute-force policy
  7. Backup-Container — täglicher Cron-Container für Postgres-Dump + Secrets-Backup.

    • Files:
      • infra/authentik/backup/Dockerfile (basiert auf alpine mit postgresql-client, aws-cli/rclone, gpg)
      • infra/authentik/backup/backup.sh:
        • pg_dump --format=custom --no-owner --no-acl der 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.txt mit den Env-Var-Namen (Werte stehen in Coolify-Vault!), als Reminder dass AUTHENTIK_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 s3 Lifecycle-Policy oder lokal via find -mtime)
      • infra/authentik/backup/crontab — täglich 03:30 UTC
    • Commit: 09-authentik/setup: add daily backup container with postgres dump and secrets manifest
  8. Health-Checks + Monitoring-Hooks — Docker-Healthchecks + Prometheus-Metrics-Endpoint freigeben.

    • Files: infra/authentik/docker-compose.override.yml (Erweiterung)
    • Inhalt:
      • server healthcheck: wget -qO- http://localhost:9000/-/health/live/ || exit 1, interval 30s, retries 3
      • worker healthcheck: ak healthcheck, interval 30s
      • postgresql healthcheck: pg_isready -U authentik
      • redis healthcheck: redis-cli ping
      • Prometheus-Endpoint /-/metrics/ auf server freischalten — wird in Phase 02 metrics-grafana als Scrape-Target aufgenommen (Hinweis im Doku-File)
    • Commit: 09-authentik/setup: add healthchecks and metrics endpoint
  9. Smoke-Test-Script — infra/authentik/scripts/smoke.sh der automatisiert prüft.

    • Files: infra/authentik/scripts/smoke.sh (bash mit set -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-configuration liefert valides JSON mit authorization_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)
    • Commit: 09-authentik/setup: add smoke test script
  10. Optionaler Audit-Log-Webhook — Authentik-Notifications an Webhook-Endpoint, der App-seitig AuditEvent schreibt.

    • Files: infra/authentik/bootstrap/blueprints/80-notifications.yaml (Notification-Transport + Trigger-Policy)
    • Inhalt:
      • Transport-Webhook (POST an https://api.<domain>/webhooks/authentik/audit, signed via X-Authentik-Signature HMAC 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
    • Commit: 09-authentik/setup: add optional audit log webhook (disabled by default)
  11. Doku — docs/operations/authentik-setup.md mit 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 .env bumpen, docker compose pull && up -d via 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
  12. Coolify-Deployment + Smoke-Test — Stack auf Staging deployen, manuell + via Script verifizieren.

    • Files: keine Code-Änderung (Operations-Schritt, wird in ## Progress Log festgehalten)
    • Commit: keiner (Deployment, kein Code)

Test Requirements

  • Unit-Tests: keine direkten — Infra. Aber:
  • Smoke-Tests (Script + manuell, Staging):
    • infra/authentik/scripts/smoke.sh exit-code-sauber
    • Admin-Login mit AUTHENTIK_BOOTSTRAP_EMAIL + AUTHENTIK_BOOTSTRAP_PASSWORD funktioniert; nach erstem Login wird Passwortwechsel erzwungen
    • MFA-Enrollment-Flow durchspielbar (TOTP-QR-Code scannen, Code verifizieren)
    • Test-User in Group toolctrl-admin und in Group toolctrl-staff anlegen, 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 gegen jwks_uri verifizieren → Group-Claim enthält toolctrl-admin
  • 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
  • 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-panel aktiv, OIDC-Discovery-Endpoint liefert valides JSON
  • OIDC-Provider toolctrl-tenant-sso als deaktivierter Platzhalter existiert
  • Groups toolctrl-admin (Attribut role: super_admin) und toolctrl-staff (Attribut role: 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.md enthält alle Mindest-Sections (siehe Step 11)
  • .env.example enthält alle Keys aus Step 3
  • Smoke-Script grün
  • pnpm lint + pnpm typecheck grü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 — siehe docs/operations/admin-sso.md Fallback-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 .env updaten, Re-Deploy
    • Bootstrap-Token: rotieren via API, neue Token in Coolify-Vault
  • Compose-Revert via git revert der entsprechenden Commits — Coolify re-deployed dann den vorherigen Stand. Hetzner-Disk-Quota beachten (alte Volumes ggf. docker volume rm).
  • Komplettes Disaster (Server-Verlust):
    1. Neuen Hetzner-Server mit Coolify aufsetzen
    2. Compose + Env-Vars (aus Coolify-Vault-Backup) deployen
    3. Postgres-Dump aus S3 restoren (vor App-Start)
    4. AUTHENTIK_SECRET_KEY MUSS der originale sein (sonst sind encrypted Fields unentschlüsselbar)
    5. Smoke-Test, dann OIDC-Provider-Client-Secrets in App-.env re-syncen

Security Notes

  • AUTHENTIK_SECRET_KEY ist 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-net erreichbar, NICHT als published port. Im Override explizit prüfen.
  • Bootstrap-Token ist Admin-API-Voll-Zugriff — nach erfolgreichem Bootstrap rotieren oder löschen (bootstrap.sh macht 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: "Lege infra/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-reviewer nach Task-Completion — Fokus auf Compose-Security (Network-Isolation, Volume-Permissions, Secret-Handling).

Staging-Deployment-Steps

  1. Branch agent/phase-09-authentik-setup erstellen + pushen
  2. 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.example mit Werten aus Coolify-Vault:
      • AUTHENTIK_SECRET_KEY — generiert via openssl rand -base64 60
      • AUTHENTIK_POSTGRESQL__PASSWORD — generiert via openssl rand -base64 32
      • AUTHENTIK_BOOTSTRAP_PASSWORD — generiert + zwingend zu ändern beim ersten Login
      • AUTHENTIK_BOOTSTRAP_TOKEN — generiert via openssl rand -hex 32
      • AUTHENTIK_BOOTSTRAP_EMAIL — Admin-Email
      • AUTHENTIK_SENTRY__DSN — Sentry-DSN aus #29
  3. Erstes Deployment triggert:
    • Postgres-Init + Authentik-Migrations (dauert ~2-3 Minuten)
    • bootstrap.sh wartet auf Health-Endpoint, dann Blueprints anwenden
    • Initial-Admin angelegt
  4. 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
    
  5. Manuell im Browser:
    • https://auth.<staging-domain>/ → Login mit Bootstrap-Credentials
    • Passwortwechsel erzwingen
    • In Directory → Users Test-User anlegen, in Group toolctrl-admin packen
    • Als Test-User einloggen → MFA-Enrollment-Flow erscheint → TOTP-QR scannen
    • Erneuter Login mit MFA → Success
  6. 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)
    
  7. Backup-Verifikation nach 24h:
    aws s3 ls s3://${AUTHENTIK_S3_BACKUP_BUCKET}/$(date +%Y-%m-%d)/
    # Erwartung: authentik-postgres.dump(.gpg) sichtbar
    
  8. 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
    
  9. 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 (no docker-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.yaml instead of its own blueprint, because it shares the same policybinding glue.
    • 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-tenants brand config + CSP) moved into docs/operations/authentik-setup.md Section 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 per gh 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 (id 501cea041c1f3e22bff3281d624a247b, proxied=false). Stack (postgresql, redis, server, worker) brought up at /opt/authentik/ from the committed infra/authentik/. Secrets generated on-host via openssl rand and 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 via docker-compose.override.yml (Host rule + Let's-Encrypt cert resolver on the coolify network). All four containers report healthy. Smoke green: curl -fSsI https://auth.boiler.saas-ctrl.com/ → HTTP/2 302 to /flows/-/default/authentication/?next=/. Blueprints: 01-groups.yaml applied successful; 02-policies.yaml + 03-application-toolctrl-admin.yaml apply with status error even 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.)