The forward_auth target /outpost.goauthentik.io/auth/caddy does not exist in authentik 2026.8 (verified live: 404 for every header combination; the URL tree has no outpost auth prefix at all — the outpost worker dials back to core over ws/outpost/<uuid>/ instead). Unauthenticated subrequests got 404, Caddy treated >=400 as 'not authenticated' and redirected browsers to the dead path — users saw authentik's own Not-Found page. Fix: guarded vhosts now proxy the whole request through authentik-server:9000 with X-authentik-meta-outpost/-provider/-app headers (the exact header set this build's Traefik controller emits, adapted for Caddy). Unauthenticated clients get the login flow served in place; once a session exists, authentik proxies on to the provider's internal_host — now http://caddy:8017, a new post-auth internal listener that routes straight to the app. The chain terminates; nothing dials back into authentik. Live DB changes applied alongside (postgres is the source of truth): provider pk=1 authentication_flow=default-authentication-flow (was NULL — the actual cause of the 404s) and internal_host=http://caddy:8017 (was self-referential http://authentik-server:9000).
154 lines
6.0 KiB
Bash
154 lines
6.0 KiB
Bash
#!/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"
|
|
|
|
# --- Quadlet units: the 3-service stack ------------------------------------
|
|
# The trio left compose: they are boot-persistent Quadlet units installed to
|
|
# ~/.config/containers/systemd/ by deploy-remote.sh (deploy_authentik).
|
|
|
|
UNIT_DIR="$REPO_ROOT/dankclaw/authentik/quadlet"
|
|
|
|
@test "the three authentik quadlet units exist" {
|
|
for unit in authentik-postgres authentik-server authentik-worker; do
|
|
[ -f "$UNIT_DIR/$unit.container" ]
|
|
done
|
|
}
|
|
|
|
@test "authentik-postgres uses the official postgres image" {
|
|
grep -q 'Image=docker.io/library/postgres:16-alpine' "$UNIT_DIR/authentik-postgres.container"
|
|
}
|
|
|
|
@test "server and worker use the official goauthentik image" {
|
|
grep -q 'Image=ghcr.io/goauthentik/server:' "$UNIT_DIR/authentik-server.container"
|
|
grep -q 'Image=ghcr.io/goauthentik/server:' "$UNIT_DIR/authentik-worker.container"
|
|
}
|
|
|
|
@test "the same image runs both roles; the Exec command selects the role" {
|
|
grep -qE '^Exec=server$' "$UNIT_DIR/authentik-server.container"
|
|
grep -qE '^Exec=worker$' "$UNIT_DIR/authentik-worker.container"
|
|
}
|
|
|
|
@test "startup order is postgres -> server -> worker via unit ordering" {
|
|
grep -q 'After=network.target authentik-postgres.service' "$UNIT_DIR/authentik-server.container"
|
|
grep -q 'Wants=authentik-postgres.service' "$UNIT_DIR/authentik-server.container"
|
|
grep -q 'Wants=authentik-server.service' "$UNIT_DIR/authentik-worker.container"
|
|
}
|
|
|
|
@test "the server publishes 9000 on loopback only; LAN reaches it through Caddy" {
|
|
grep -q 'PublishPort=127.0.0.1:9000:9000' "$UNIT_DIR/authentik-server.container"
|
|
}
|
|
|
|
@test "authentik-postgres healthcheck uses pg_isready" {
|
|
grep -q 'HealthCmd=.*pg_isready' "$UNIT_DIR/authentik-postgres.container"
|
|
}
|
|
|
|
# --- Units: environment (secrets stay in the host .env, expanded at start) --
|
|
|
|
@test "the units consume AUTHENTIK_PG_PASS via systemd expansion, not literals" {
|
|
grep -q 'POSTGRES_PASSWORD=${AUTHENTIK_PG_PASS}' "$UNIT_DIR/authentik-postgres.container"
|
|
grep -q 'PASSWORD=${AUTHENTIK_PG_PASS}' "$UNIT_DIR/authentik-server.container"
|
|
grep -q 'PASSWORD=${AUTHENTIK_PG_PASS}' "$UNIT_DIR/authentik-worker.container"
|
|
}
|
|
|
|
@test "the units consume AUTHENTIK_SECRET_KEY via systemd expansion, not literals" {
|
|
for unit in authentik-server authentik-worker; do
|
|
grep -q 'AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}' "$UNIT_DIR/$unit.container"
|
|
done
|
|
}
|
|
|
|
@test "no secret material is committed in the quadlet units" {
|
|
! grep -rqE 'SECRET_KEY=[^$]' "$UNIT_DIR"
|
|
}
|
|
|
|
@test "deploy-remote.sh routes the authentik trio to the quadlet handler, not compose up" {
|
|
grep -q 'authentik-postgres|authentik-server|authentik-worker)' "$REPO_ROOT/scripts/deploy-remote.sh"
|
|
grep -q 'deploy_authentik()' "$REPO_ROOT/scripts/deploy-remote.sh"
|
|
! grep -qE '^[[:space:]]+comfyui\|.*authentik' "$REPO_ROOT/scripts/deploy-remote.sh"
|
|
}
|
|
|
|
# --- 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: guarded vhosts proxy the whole request through Authentik ----
|
|
|
|
@test "guarded vhosts proxy the whole request through authentik on the internal listener" {
|
|
awk '/^http:\/\/:8016/,/^\}/' "$CADDYFILE" | grep -q 'reverse_proxy authentik-server:9000'
|
|
}
|
|
|
|
@test "the auth decision carries the outpost meta headers" {
|
|
grep -q 'X-authentik-meta-outpost' "$CADDYFILE"
|
|
grep -q 'X-authentik-meta-provider' "$CADDYFILE"
|
|
grep -q 'X-authentik-meta-app' "$CADDYFILE"
|
|
}
|
|
|
|
@test "authenticated traffic returns to Caddy on the post-auth listener" {
|
|
grep -qE '^http://:8017 \{' "$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; }
|
|
}
|