Files
serverAppConfigs/tests/caddyfile.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

193 lines
7.7 KiB
Bash

#!/usr/bin/env bats
# Seam: Caddyfile validity.
#
# There is exactly one Caddy in the homelab: the container on dankclaw
# (dankclaw/compose.yaml, service 'caddy'). It fronts every service on both
# hosts — local services by container name, dankserver services by published
# LAN port. Site blocks use the flat franksdank.cloud / franksdank.casa
# convention. The basicauth hash is a placeholder in git; deploy.sh renders
# the real hash from the host .env (CADDY_BASICAUTH_HASH).
load helpers
CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
@test "the Caddyfile lives on dankclaw only (Caddy is consolidated there)" {
[ -f "$CADDYFILE" ]
[ ! -f "$REPO_ROOT/dankserver/Caddyfile" ]
}
@test "the Caddyfile uses the flat franksdank.cloud hostname convention" {
grep -qE '\.franksdank\.cloud' "$CADDYFILE"
}
@test "no .local vhost names remain in the Caddyfile" {
! grep -qE '^([a-z0-9.-]+\.)?(dankserver|dankclaw)\.local[[:space:]]*\{' \
"$CADDYFILE"
}
@test "the Caddyfile has balanced braces" {
local open close
open=$(grep -o '{' "$CADDYFILE" | wc -l)
close=$(grep -o '}' "$CADDYFILE" | wc -l)
[ "$open" -eq "$close" ] || { echo "unbalanced braces ($open vs $close)"; return 1; }
}
@test "caddy validate passes when caddy is available" {
if command -v caddy >/dev/null 2>&1; then
caddy validate --config "$CADDYFILE" --adapter caddyfile
else
skip "caddy binary not installed locally"
fi
}
@test "homepage basic auth exists in the Caddyfile" {
grep -qiE 'basicauth|basic_auth' "$CADDYFILE"
}
@test "the Caddyfile carries the renderable basicauth placeholder" {
grep -q 'REPLACE_WITH_HASH' "$CADDYFILE"
}
@test "the Caddyfile does not disable the admin endpoint (hot reload needs it)" {
# admin off disables Caddy's config file watcher: rendered Caddyfile
# changes are never loaded until the container is recreated, and the
# stale config fails silently (2026-08-22: basicauth stayed broken for
# hours across deploys). The admin API binds to 127.0.0.1:2019 inside
# the container and the port is not published, so it is not reachable
# from the LAN.
if grep -qE '^\s*admin\s+off' "$CADDYFILE"; then
echo "Caddyfile: 'admin off' disables hot reload of rendered config"
return 1
fi
}
@test "the basicauth placeholder is a bare token (no bcrypt prefix in the template)" {
# Regression (2026-08-22): the template line was
# dank $2a$14$REPLACE_WITH_HASH
# and deploy.sh substitutes the FULL hash (prefix included), so the
# rendered line became an invalid double-prefixed hash and every
# basicauth vhost rejected every password with 401.
if grep -qE '\$2[abxy]\$[0-9]+\$REPLACE_WITH_HASH' "$CADDYFILE"; then
echo "Caddyfile: bcrypt prefix before REPLACE_WITH_HASH (double-prefix regression)"
return 1
fi
grep -qE 'dankuser REPLACE_WITH_HASH' "$CADDYFILE"
}
@test "compose no longer defines the quadlet-replaced services" {
# gitea, its postgres, and open-webui run as boot-persistent quadlets on
# dankclaw (see dankclaw/gitea/quadlet, dankclaw/open-webui/quadlet).
! grep -qE '^\s+(gitea|postgres|openwebui):\s*$' "$REPO_ROOT/dankclaw/compose.yaml"
}
@test "the caddy service healthcheck probes the bare-IP catch-all" {
grep -q 'curl.*-sf.*http://localhost:80/' "$REPO_ROOT/dankclaw/compose.yaml"
}
# --- Phase 2: real TLS via DNS-01 (Porkbun wildcard cert) ---
@test "all vhosts use TLS except the legacy .casa name" {
# franksdank.casa is not in the same Porkbun account, so it cannot get
# a DNS-01 Let's Encrypt cert; it stays explicitly http. Every other
# site address must carry no http:// prefix.
local offenders
offenders=$(grep -E '^http://' "$CADDYFILE" | grep -v 'plex\.franksdank\.casa' || true)
[ -z "$offenders" ] || { echo "non-.casa http:// vhosts: $offenders"; return 1; }
}
@test "the Caddyfile configures the Porkbun DNS-01 provider globally" {
grep -q 'acme_dns porkbun' "$CADDYFILE"
grep -q '{env.PORKBUN_API_KEY}' "$CADDYFILE"
grep -q '{env.PORKBUN_API_SECRET_KEY}' "$CADDYFILE"
}
@test "the bare-IP catch-all redirects to the HTTPS homepage" {
grep -q 'redir https://franksdank.cloud/' "$CADDYFILE"
}
@test "the caddy service uses the locally built porkbun image" {
grep -q 'franksdank/caddy-porkbun' "$REPO_ROOT/dankclaw/compose.yaml"
}
@test "the caddy service passes the Porkbun credentials from .env" {
grep -q 'PORKBUN_API_KEY: \${PORKBUN_API_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
grep -q 'PORKBUN_API_SECRET_KEY: \${PORKBUN_API_SECRET_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
}
@test ".env.example carries the Porkbun secret under the module's name" {
grep -q 'PORKBUN_API_SECRET_KEY=' "$REPO_ROOT/dankclaw/.env.example"
! grep -q '^PORKBUN_API_SECRET=' "$REPO_ROOT/dankclaw/.env.example"
}
@test "the porkbun caddy image build lives in the repo" {
local cf="$REPO_ROOT/dankclaw/caddy/Containerfile"
[ -f "$cf" ] || { echo "missing $cf"; return 1; }
grep -q 'xcaddy build' "$cf"
grep -q 'github.com/caddy-dns/porkbun' "$cf"
}
# --- Observability configs (deployed by deploy.sh step 3) ---
@test "no legacy .local service names remain in shared configs or compose" {
! grep -rn 'dankclaw\.local\|dankserver\.local' \
"$REPO_ROOT/shared" "$REPO_ROOT/dankclaw/compose.yaml"
}
@test "loki compactor has a delete_request_store (required when retention is on)" {
local loki="$REPO_ROOT/shared/loki/loki-config.yaml"
if grep -q 'retention_enabled: true' "$loki"; then
grep -q 'delete_request_store:' "$loki"
fi
}
@test "prometheus targets are host:port (no URL schemes)" {
! grep -nE '^\s*-\s*"?http' "$REPO_ROOT/shared/prometheus/prometheus.yml"
}
@test "observability containers run as container root (rootless = host user)" {
# In rootless podman, container root maps to the invoking user (dank),
# which owns the /opt data dirs. The images' default users (loki 10001,
# prometheus nobody, grafana 472) map to subuids that own nothing under
# /opt and cannot write the data dirs.
local compose="$REPO_ROOT/dankclaw/compose.yaml"
local svc block
for svc in prometheus loki grafana; do
block=$(awk -v s="^ $svc:\$" '
$0 ~ s { inblk = 1; next }
inblk && /^ [a-z]/ { inblk = 0 }
inblk' "$compose")
echo "$block" | grep -q 'user: "0:0"' || {
echo "service $svc does not run as user 0:0"; return 1;
}
done
}
# --- distroless healthchecks (loki/alloy carry a static busybox) ---
@test "loki and alloy run from local images carrying a static busybox" {
local compose="$REPO_ROOT/dankclaw/compose.yaml"
grep -q 'image: franksdank/loki:latest' "$compose" || { echo "loki image not local"; return 1; }
grep -q 'image: franksdank/alloy:latest' "$compose" || { echo "alloy image not local"; return 1; }
local img
for img in loki alloy; do
local cf="$REPO_ROOT/dankclaw/$img/Containerfile"
[ -f "$cf" ] || { echo "missing $cf"; return 1; }
grep -q 'busybox' "$cf" || { echo "$cf does not install busybox"; return 1; }
done
}
@test "loki and alloy healthchecks use the busybox wget" {
local compose="$REPO_ROOT/dankclaw/compose.yaml"
local n
n=$(grep -c '/usr/local/bin/busybox' "$compose")
[ "$n" -ge 2 ] || { echo "expected >=2 busybox healthchecks, found $n"; return 1; }
}
@test "alloy config declares no ui/server block (default UI listens on 127.0.0.1:12345)" {
# alloy 1.x refuses to configure the ui service from a config block
# ("service ui does not support being configured"). The default UI
# already listens where the in-container healthcheck probes.
! grep -qE '^(ui|server) \{' "$REPO_ROOT/shared/alloy/config.alloy"
}