Files
serverAppConfigs/tests/authentik.bats
T
Frank 3cc99d81cc Authentik SSO + homepage naked domain
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.
2026-08-24 23:48:27 -07:00

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; }
}