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.
145 lines
5.2 KiB
Bash
145 lines
5.2 KiB
Bash
#!/usr/bin/env bats
|
|
# Seam: LAN DNS (Pi-hole) on dankclaw.
|
|
#
|
|
# The Pi-hole quadlet is the LAN's authoritative DNS for the flat
|
|
# franksdank.cloud / franksdank.casa name scheme. It retired the dnsmasq
|
|
# quadlet (2026-08-24; Pi-hole FTL is a dnsmasq fork, so the addn-hosts
|
|
# record file carries over unchanged). Every Caddy vhost name must have an
|
|
# A record pointing at the host that runs it, records must stay inside the
|
|
# LAN, and the quadlet must be boot-persistent.
|
|
|
|
load helpers
|
|
|
|
PIHOLE_DIR="$REPO_ROOT/dankclaw/pihole"
|
|
QUADLET="$PIHOLE_DIR/quadlet/pihole.container"
|
|
ENV_EXAMPLE="$PIHOLE_DIR/env.example"
|
|
HOSTS="$PIHOLE_DIR/addn-hosts"
|
|
|
|
@test "the pihole quadlet container file exists" {
|
|
[ -f "$QUADLET" ]
|
|
}
|
|
|
|
@test "the pihole quadlet is installed for boot" {
|
|
grep -q 'WantedBy=default.target' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole quadlet publishes port 53 tcp and udp" {
|
|
grep -qE '^PublishPort=0\.0\.0\.0:53:53/tcp$' "$QUADLET"
|
|
grep -qE '^PublishPort=0\.0\.0\.0:53:53/udp$' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole quadlet publishes the web UI on 8083" {
|
|
grep -qE '^PublishPort=0\.0\.0\.0:8083:80/tcp$' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole quadlet restarts on failure" {
|
|
grep -q '^Restart=always' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole quadlet reads its env file" {
|
|
grep -q '^EnvironmentFile=/home/dank/Containers/pihole/env$' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole quadlet mounts the addn-hosts record file into the FTL hostsdir" {
|
|
# FTL (a dnsmasq fork) does not read /etc/dnsmasq.d/ (the generated
|
|
# dnsmasq.conf has no conf-dir). Its only file-based local-record
|
|
# mechanism is hostsdir=/etc/pihole/hosts (each file parsed as an
|
|
# /etc/hosts file). Mount the record file there, read-only.
|
|
grep -qE '^Volume=/home/dank/Containers/pihole/addn-hosts:/etc/pihole/hosts/addn-hosts:ro$' "$QUADLET"
|
|
}
|
|
|
|
@test "the pihole env example has placeholder values only" {
|
|
[ -f "$ENV_EXAMPLE" ]
|
|
grep -q '^WEBPASSWORD=changeme$' "$ENV_EXAMPLE"
|
|
grep -q '^ServerIP=192\.168\.0\.143$' "$ENV_EXAMPLE"
|
|
grep -q '^DNS1=1\.1\.1\.1$' "$ENV_EXAMPLE"
|
|
}
|
|
|
|
@test "the retired dnsmasq quadlet files are out of the repo" {
|
|
[ ! -f "$REPO_ROOT/dankclaw/dnsmasq/dnsmasq.container" ]
|
|
[ ! -f "$REPO_ROOT/dankclaw/dnsmasq/dnsmasq.conf" ]
|
|
[ ! -f "$REPO_ROOT/dankclaw/dnsmasq/Containerfile" ]
|
|
}
|
|
|
|
@test "every addn-hosts line is a valid IPv4 address plus hostnames" {
|
|
local bad=0 line
|
|
while IFS= read -r line; do
|
|
[ -z "$line" ] && continue
|
|
case "$line" in \#*) continue ;; esac
|
|
if ! echo "$line" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}[[:space:]]+[a-z0-9.-]+([[:space:]]+[a-z0-9.-]+)*$'; then
|
|
echo "BAD LINE: $line"
|
|
bad=1
|
|
fi
|
|
done < "$HOSTS"
|
|
[ "$bad" -eq 0 ]
|
|
}
|
|
|
|
@test "every addn-hosts IP is inside the homelab LAN" {
|
|
! awk '!/^[[:space:]]*#/ && NF {print $1}' "$HOSTS" \
|
|
| grep -vE '^(192\.168\.0\.[0-9]{1,3})$' \
|
|
| grep -q .
|
|
}
|
|
|
|
@test "every addn-hosts name is under franksdank.cloud or franksdank.casa" {
|
|
local bad=0 line ip n
|
|
while IFS= read -r line; do
|
|
[ -z "$line" ] && continue
|
|
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 "UNEXPECTED TLD on $ip: $n"; bad=1; }
|
|
done
|
|
done < "$HOSTS"
|
|
[ "$bad" -eq 0 ]
|
|
}
|
|
|
|
# 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 -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
|
|
record_ip_for() {
|
|
awk -v n="$1" '{ip=$1; for (i=2; i<=NF; i++) if ($i==n) print ip}' "$HOSTS" | head -1
|
|
}
|
|
|
|
# Caddy is consolidated on dankclaw (192.168.0.143): it fronts every service
|
|
# on both hosts, so every vhost name resolves to that one IP.
|
|
|
|
@test "every Caddyfile vhost has an A record on 192.168.0.143" {
|
|
local names n ip
|
|
names="$(vhost_names "$REPO_ROOT/dankclaw/Caddyfile")"
|
|
[ -n "$names" ] || { echo "no .franksdank vhosts found in dankclaw/Caddyfile"; return 1; }
|
|
while IFS= read -r n; do
|
|
[ -z "$n" ] && continue
|
|
ip="$(record_ip_for "$n")"
|
|
[ "$ip" = "192.168.0.143" ] || { echo "$n -> '$ip' (want 192.168.0.143)"; return 1; }
|
|
done <<< "$names"
|
|
}
|
|
|
|
@test "the pihole admin name has an A record on 192.168.0.143" {
|
|
[ "$(record_ip_for pihole.franksdank.cloud)" = "192.168.0.143" ]
|
|
}
|
|
|
|
@test "no addn-hosts records point at the old per-host split (192.168.0.235)" {
|
|
! awk '!/^[[:space:]]*#/ && NF {print $1}' "$HOSTS" | grep -q '^192\.168\.0\.235$'
|
|
}
|
|
|
|
@test "the retired llms name has no A record" {
|
|
! grep -qE '\billms\.franksdank\.cloud\b' "$HOSTS"
|
|
}
|
|
|
|
@test "no secret material in the pihole files" {
|
|
local f
|
|
while IFS= read -r f; do
|
|
has_no_leaked_secret "$f"
|
|
has_no_leaked_sudo_pw "$f"
|
|
done < <(find "$PIHOLE_DIR" -type f | sort)
|
|
}
|