Auth layer (Authentik 2026.8.0): - compose: 3-service stack (authentik-postgres, authentik-server, authentik-worker), postgres health-gated, bind mounts under /home/dank/authentik - Caddyfile: auth.franksdank.cloud vhost; forward_auth on the no-auth vhosts (homepage, SearXNG, Hermes, Prometheus, RomM, Copyparty); outpost path proxied first in a route block - .env.example: AUTHENTIK_PG_PASS + AUTHENTIK_SECRET_KEY - deploy.sh: render only CADDY_BASICAUTH_HASH (the homepage per-user hashes are retired; grafana + pihole keep basicauth) - tests/authentik.bats (new) + updated homepage/caddyfile/pihole seams - docs: Authentik.md + per-service pages; Caddy.md + Homepage.md updated Homepage (naked domain): - Caddyfile: dankclaw.franksdank.cloud -> franksdank.cloud www.franksdank.cloud; dankclaw + dankserver redirect to it; bare-IP catch-all updated - addn-hosts: A records for the naked domain, www, and auth (all -> 143) - tests/pihole.bats: vhost_names handles multi-address blocks; TLD check allows the naked domain All 146 tests pass.
174 lines
5.6 KiB
Bash
174 lines
5.6 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"
|
|
|
|
# --- 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; }
|
|
}
|