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.
This commit is contained in:
1 parent
7ba1acdbc5
commit
3cc99d81cc
17 files changed
+634
-137
No files matched your search
@@ -0,0 +1,173 @@
|
||||
#!/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; }
|
||||
}
|
||||
@@ -103,7 +103,7 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
|
||||
}
|
||||
|
||||
@test "the bare-IP catch-all redirects to the HTTPS homepage" {
|
||||
grep -q 'redir https://dankclaw.franksdank.cloud/' "$CADDYFILE"
|
||||
grep -q 'redir https://franksdank.cloud/' "$CADDYFILE"
|
||||
}
|
||||
|
||||
@test "the caddy service uses the locally built porkbun image" {
|
||||
|
||||
+17
-26
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: the homepage site block in dankclaw/Caddyfile. It must carry a root
|
||||
# basicauth hash and a distinct /admin/-scoped admin hash (Q18/Q19), and serve
|
||||
# /srv/homepage. caddy is not on the dev box, so the tests assert the
|
||||
# structure statically; the deploy renders the two real hashes from the .env.
|
||||
# Seam: the homepage site block in dankclaw/Caddyfile. It serves /srv/homepage
|
||||
# from the naked domain (franksdank.cloud) and www. Authentication is via
|
||||
# Caddy forward_auth (Authentik SSO) — the basicauth hashes were retired in
|
||||
# favor of the unified Authentik login. caddy is not on the dev box, so the
|
||||
# tests assert the structure statically.
|
||||
|
||||
load helpers
|
||||
|
||||
@@ -10,38 +11,28 @@ setup() {
|
||||
CADDY="$REPO_ROOT/dankclaw/Caddyfile"
|
||||
}
|
||||
|
||||
@test "the homepage has its own hash and a distinct admin hash" {
|
||||
grep -q "REPLACE_WITH_HOMEPAGE_HASH" "$CADDY"
|
||||
grep -q "REPLACE_WITH_ADMIN_HASH" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the admin basicauth is scoped to /admin/ (Q19)" {
|
||||
# The admin page is a 'handle /admin*' block with the operator (frank)
|
||||
# credential. Caddy sorts a top-level 'rewrite' before 'basicauth', which
|
||||
# would change the path and break the path-based auth matcher; 'handle'
|
||||
# preserves order, so the auth is checked on the original path.
|
||||
grep -qE "handle[[:space:]]+/admin\*" "$CADDY"
|
||||
grep -qE "frank[[:space:]]+REPLACE_WITH_ADMIN_HASH" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the homepage serves /srv/homepage" {
|
||||
grep -q "root \* /srv/homepage" "$CADDY"
|
||||
grep -q "file_server" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the /admin/ page serves admin.html and / serves index.html" {
|
||||
# Each page is served by its own 'handle' block via 'try_files' (which
|
||||
# serves the named file without a top-level 'rewrite' that Caddy would
|
||||
# sort before the basicauth).
|
||||
# Each page is served by its own 'handle' block via 'try_files'.
|
||||
grep -qE "handle[[:space:]]+/admin\*" "$CADDY"
|
||||
grep -qE "try_files[[:space:]]+/admin.html" "$CADDY"
|
||||
grep -qE "try_files[[:space:]]+/index.html" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the shared data + assets accept both the family and the operator" {
|
||||
# The admin page (frank) and the user page (dank) both fetch the shared
|
||||
# data (services.json, status.json) and assets. The browser reuses the
|
||||
# page's credential for these, so the block must accept both users.
|
||||
@test "the shared data + assets are served to any authenticated user" {
|
||||
# The data (services.json, status.json) and assets are fetched by both the
|
||||
# user page and the admin page. With forward_auth, any authenticated user
|
||||
# can access them (the browser's Authentik session covers the fetch).
|
||||
grep -qE "@data[[:space:]]+path[[:space:]]+/services\.json[[:space:]]+/status\.json" "$CADDY"
|
||||
grep -qE "handle[[:space:]]+@data" "$CADDY"
|
||||
grep -qE "dankuser[[:space:]]+REPLACE_WITH_HOMEPAGE_HASH" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the homepage vhost uses forward_auth (Authentik SSO)" {
|
||||
local block
|
||||
block=$(awk '/^franksdank\.cloud[[:space:]]/,/^}/' "$CADDY")
|
||||
echo "$block" | grep -q 'forward_auth'
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: scripts/deploy.sh deploys the homepage, and the checker runs as a
|
||||
# systemd timer. The deploy is git-driven (master = the deployed state, Q33).
|
||||
# The tests assert the deploy references the homepage steps and renders BOTH
|
||||
# hashes, and that the checker timer + service exist and point at the checker.
|
||||
# The tests assert the deploy references the homepage steps and renders the
|
||||
# basicauth hash, and that the checker timer + service exist and point at the
|
||||
# checker. (The homepage's per-user hashes were retired: it now uses Authentik
|
||||
# forward_auth, so only the shared family hash is rendered.)
|
||||
|
||||
load helpers
|
||||
|
||||
@test "deploy.sh ships the homepage (assets, timer, both hashes, deployed.js)" {
|
||||
@test "deploy.sh ships the homepage (assets, timer, basicauth hash, deployed.js)" {
|
||||
local d="$REPO_ROOT/scripts/deploy.sh"
|
||||
grep -q "/srv/homepage" "$d"
|
||||
grep -q "homepage-status" "$d"
|
||||
grep -q "CADDY_BASICAUTH_HASH" "$d"
|
||||
grep -q "CADDY_ADMIN_HASH" "$d"
|
||||
grep -q "deployed.js" "$d"
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -87,17 +87,21 @@ HOSTS="$PIHOLE_DIR/addn-hosts"
|
||||
case "$line" in \#*) continue ;; esac
|
||||
ip="$(echo "$line" | awk '{print $1}')"
|
||||
for n in $(echo "$line" | cut -d' ' -f2-); do
|
||||
echo "$n" | grep -qE '\.(franksdank\.(cloud|casa))$' \
|
||||
echo "$n" | grep -qE '(^|\.)franksdank\.(cloud|casa)$' \
|
||||
|| { echo "UNEXPECTED TLD on $ip: $n"; bad=1; }
|
||||
done
|
||||
done < "$HOSTS"
|
||||
[ "$bad" -eq 0 ]
|
||||
}
|
||||
|
||||
# vhost_names <caddyfile> — print site-block names under the flat scheme
|
||||
# vhost_names <caddyfile> — print site-block names under the flat scheme.
|
||||
# Handles both single-address blocks ("name {") and multi-address blocks
|
||||
# ("name1 name2 {"), one name per line.
|
||||
vhost_names() {
|
||||
grep -oE '^(http://)?[a-z0-9.-]+\.franksdank\.(cloud|casa)[[:space:]]*\{' "$1" \
|
||||
| sed -E 's#^http://##; s/[[:space:]]*\{$//'
|
||||
grep -E '^(http://)?[a-z0-9.-]+\.franksdank\.(cloud|casa)([[:space:]]+[a-z0-9.-]+\.franksdank\.(cloud|casa))*[[:space:]]*\{' "$1" \
|
||||
| sed -E 's/^[[:space:]]+//; s/[[:space:]]*\{$//' \
|
||||
| tr ' ' '\n' \
|
||||
| sed -E 's#^http://##'
|
||||
}
|
||||
|
||||
# record_ip_for <name> — print the IP a name maps to in addn-hosts
|
||||
|
||||
Reference in new issue
Block a user