#!/usr/bin/env bats # Seam: Authentik (SSO / OIDC provider) — the homelab's auth layer. # # Authentik is a 3-service stack (PostgreSQL + server + worker) on dankclaw. # It provides OIDC, SAML, and Caddy forward_auth for the vhosts that do not # have their own application-level authentication (SearXNG, Hermes, # Prometheus, RomM, Copyparty, the homepage). # # The Caddyfile carries an `auth.franksdank.cloud` vhost that proxies to the # Authentik server. Vhosts without their own auth are wrapped in a `route` # block that (1) proxies the outpost path to Authentik, (2) runs forward_auth, # (3) serves the app. # # See ../docs/Infrastructure/Authentik.md. load helpers COMPOSE="$REPO_ROOT/dankclaw/compose.yaml" CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile" ENV_EXAMPLE="$REPO_ROOT/dankclaw/.env.example" # --- Compose: the 3-service stack --- @test "compose defines authentik-postgres" { grep -qE '^ authentik-postgres:' "$COMPOSE" } @test "compose defines authentik-server" { grep -qE '^ authentik-server:' "$COMPOSE" } @test "compose defines authentik-worker" { grep -qE '^ authentik-worker:' "$COMPOSE" } @test "authentik-postgres uses the official postgres image" { grep -q 'image: docker.io/library/postgres:' "$COMPOSE" } @test "authentik-server uses the official goauthentik image" { grep -q 'image: ghcr.io/goauthentik/server:' "$COMPOSE" } @test "authentik-server runs the 'server' command" { # The same image runs both server and worker; the command selects the role. grep -qE '^\s+command:\s*server' "$COMPOSE" } @test "authentik-worker runs the 'worker' command" { grep -qE '^\s+command:\s*worker' "$COMPOSE" } @test "authentik-server depends on a healthy authentik-postgres" { local block block=$(awk -v s="^ authentik-server:\$" ' $0 ~ s { inblk = 1; next } inblk && /^ [a-z]/ { inblk = 0 } inblk' "$COMPOSE") echo "$block" | grep -q 'condition: service_healthy' } @test "authentik-worker depends on a healthy authentik-postgres" { local block block=$(awk -v s="^ authentik-worker:\$" ' $0 ~ s { inblk = 1; next } inblk && /^ [a-z]/ { inblk = 0 } inblk' "$COMPOSE") echo "$block" | grep -q 'condition: service_healthy' } @test "authentik-server exposes port 9000" { local block block=$(awk -v s="^ authentik-server:\$" ' $0 ~ s { inblk = 1; next } inblk && /^ [a-z]/ { inblk = 0 } inblk' "$COMPOSE") echo "$block" | grep -qE '"9000:9000"' } @test "authentik-postgres healthcheck uses pg_isready" { local block block=$(awk -v s="^ authentik-postgres:\$" ' $0 ~ s { inblk = 1; next } inblk && /^ [a-z]/ { inblk = 0 } inblk' "$COMPOSE") echo "$block" | grep -q 'pg_isready' } # --- Compose: environment (secrets from .env) --- @test "authentik services consume AUTHENTIK_PG_PASS from .env" { grep -q 'AUTHENTIK_PG_PASS' "$COMPOSE" } @test "authentik services consume AUTHENTIK_SECRET_KEY from .env" { grep -q 'AUTHENTIK_SECRET_KEY' "$COMPOSE" } @test "no real authentik secret is committed in compose" { # The compose must reference the secret via ${VAR}, never a literal. ! grep -qE 'AUTHENTIK_SECRET_KEY:\s*["\x27]?\w{16,}' "$COMPOSE" } # --- Caddyfile: the auth vhost --- @test "the Caddyfile has an auth.franksdank.cloud vhost" { grep -qE '^auth\.franksdank\.cloud[[:space:]]*\{' "$CADDYFILE" } @test "the auth vhost proxies to authentik-server:9000" { local block block=$(awk '/^auth\.franksdank\.cloud/,/^}/' "$CADDYFILE") echo "$block" | grep -q 'reverse_proxy authentik-server:9000' } # --- Caddyfile: forward_auth on vhosts without their own auth --- @test "the Caddyfile uses forward_auth" { grep -q 'forward_auth' "$CADDYFILE" } @test "forward_auth targets the authentik outpost caddy endpoint" { grep -q '/outpost.goauthentik.io/auth/caddy' "$CADDYFILE" } @test "forward_auth copies the X-Authentik-Username header" { grep -q 'X-Authentik-Username' "$CADDYFILE" } @test "the outpost path is proxied to authentik in forward_auth vhosts" { grep -q '/outpost.goauthentik.io/\*' "$CADDYFILE" } # --- Homepage: moved to naked domain + www --- @test "the homepage vhost is the naked domain + www" { grep -qE '^franksdank\.cloud[[:space:]]+www\.franksdank\.cloud[[:space:]]*\{' "$CADDYFILE" } @test "dankclaw.franksdank.cloud redirects to the naked domain" { local block block=$(awk '/^dankclaw\.franksdank\.cloud/,/^}/' "$CADDYFILE") echo "$block" | grep -q 'redir https://franksdank.cloud/' } @test "dankserver.franksdank.cloud redirects to the naked domain" { local block block=$(awk '/^dankserver\.franksdank\.cloud/,/^}/' "$CADDYFILE") echo "$block" | grep -q 'redir https://franksdank.cloud/' } @test "the bare-IP catch-all redirects to the naked domain" { grep -q 'redir https://franksdank.cloud/' "$CADDYFILE" } # --- .env.example: authentik placeholders --- @test ".env.example carries AUTHENTIK_PG_PASS" { grep -q 'AUTHENTIK_PG_PASS=' "$ENV_EXAMPLE" } @test ".env.example carries AUTHENTIK_SECRET_KEY" { grep -q 'AUTHENTIK_SECRET_KEY=' "$ENV_EXAMPLE" } @test ".env.example authentik placeholders are not real secrets" { # Placeholders must be obvious (REPLACE_WITH or empty), not 64-char strings. local pg sk pg=$(grep '^AUTHENTIK_PG_PASS=' "$ENV_EXAMPLE" | cut -d= -f2-) sk=$(grep '^AUTHENTIK_SECRET_KEY=' "$ENV_EXAMPLE" | cut -d= -f2-) [ ${#pg} -lt 32 ] || { echo "AUTHENTIK_PG_PASS looks like a real secret (len=${#pg})"; return 1; } [ ${#sk} -lt 32 ] || { echo "AUTHENTIK_SECRET_KEY looks like a real secret (len=${#sk})"; return 1; } }