F-10: automated Authelia repo<->host drift check

New services/authelia-diff.sh compares the access_control: section of the
repo baseline against the live host configuration.yml. OIDC clients,
identity providers, and secret values stay out of scope by design.
Exit codes: 0 ok, 1 drift, 2 file missing, 3 section missing, 4 tool missing.

posture-check.sh gains check_authelia_config_drift, which calls the diff
script and reports drift as warning (not critical). SKIP_AUTHELIA_DRIFT=1
opts out; AUTHELIA_DIFF_SCRIPT overrides the path.

WORKFLOW.md gets a dedicated "Ausnahme: Authelia configuration.yml" section
analogous to the Traefik dynamic-config exception, with the mandatory
repo->host merge workflow and the env-variable contract.

Smoke-tested locally: identical files rc=0, ACL change rc=1 with proper
unified diff, non-ACL change (session.default_redirection_url) correctly
ignored.

Operator follow-up: set up a read-only repo mirror at
/mnt/user/services/homelab-infra/ so the check finds a current baseline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 09:52:16 +02:00
parent 3bd35434d6
commit 8095ab8b5d
7 changed files with 213 additions and 2 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Vergleicht die Repo-Baseline der Authelia-Konfiguration gegen die produktive
# Host-Datei. Bewusst nur fuer Sektionen, die laut Repo-Konvention auf Host
# und Repo identisch sein muessen (Default: access_control). OIDC-Clients,
# identity_providers und Secret-Werte bleiben hostseitig und werden nicht
# verglichen.
#
# Aufruf-Defaults siehe Variablen unten. Aufruf typischerweise:
# bash services/authelia-diff.sh
#
# Exit-Codes:
# 0 alle verglichenen Sektionen identisch
# 1 Drift festgestellt (Diff wird auf stdout ausgegeben)
# 2 Repo-Baseline oder Host-Datei fehlt
# 3 Sektion in mindestens einer Datei nicht gefunden
# 4 internes Werkzeug fehlt (awk/diff)
set -uo pipefail
AUTHELIA_REPO_BASELINE="${AUTHELIA_REPO_BASELINE:-/mnt/user/services/homelab-infra/security/authelia/configuration.yml}"
AUTHELIA_HOST_CONFIG="${AUTHELIA_HOST_CONFIG:-/mnt/user/appdata/authelia/config/configuration.yml}"
AUTHELIA_DIFF_SECTIONS="${AUTHELIA_DIFF_SECTIONS:-access_control}"
for cmd in awk diff; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "authelia-diff: missing required command '$cmd'" >&2
exit 4
fi
done
if [ ! -f "$AUTHELIA_REPO_BASELINE" ]; then
echo "authelia-diff: repo baseline not found: $AUTHELIA_REPO_BASELINE" >&2
exit 2
fi
if [ ! -f "$AUTHELIA_HOST_CONFIG" ]; then
echo "authelia-diff: host config not found: $AUTHELIA_HOST_CONFIG" >&2
exit 2
fi
# Extrahiert einen Top-Level-Block aus einer YAML-Datei.
# Block-Anfang: Zeile, die mit "<section>:" beginnt (kein Whitespace davor).
# Block-Ende: naechste Top-Level-Key-Zeile (`^[A-Za-z_][A-Za-z0-9_]*:`).
# Eingaberauschen wird entfernt: reine Kommentarzeilen, trailing whitespace,
# Leerzeilen.
extract_section() {
local file="$1"
local section="$2"
awk -v section="$section" '
BEGIN { inside = 0; found = 0 }
{
line = $0
sub(/[[:space:]]+$/, "", line)
}
# Top-Level-Key entdeckt
/^[A-Za-z_][A-Za-z0-9_]*:/ {
key = line
sub(/:.*$/, "", key)
if (key == section) {
inside = 1
found = 1
print line
next
} else if (inside == 1) {
inside = 0
}
}
inside == 1 {
# Kommentar- und Leerzeilen ignorieren
if (line ~ /^[[:space:]]*#/) next
if (line ~ /^[[:space:]]*$/) next
print line
}
END {
if (!found) exit 10
}
' "$file"
}
tmpdir="$(mktemp -d -t authelia-diff.XXXXXX)"
trap 'rm -rf "$tmpdir"' EXIT
overall_status=0
diff_output=""
missing_sections=""
IFS=',' read -r -a sections <<< "$AUTHELIA_DIFF_SECTIONS"
for section in "${sections[@]}"; do
section="${section// /}"
[ -z "$section" ] && continue
repo_file="$tmpdir/repo.$section"
host_file="$tmpdir/host.$section"
if ! extract_section "$AUTHELIA_REPO_BASELINE" "$section" > "$repo_file" 2>/dev/null; then
missing_sections="${missing_sections}${missing_sections:+, }$section (repo)"
continue
fi
if ! extract_section "$AUTHELIA_HOST_CONFIG" "$section" > "$host_file" 2>/dev/null; then
missing_sections="${missing_sections}${missing_sections:+, }$section (host)"
continue
fi
if ! diff_chunk="$(diff -u \
--label "repo:$section" "$repo_file" \
--label "host:$section" "$host_file")"; then
overall_status=1
diff_output="${diff_output}${diff_chunk}"$'\n'
fi
done
if [ -n "$missing_sections" ] && [ "$overall_status" -eq 0 ]; then
echo "authelia-diff: sections missing: $missing_sections" >&2
exit 3
fi
if [ "$overall_status" -ne 0 ]; then
printf '%s' "$diff_output"
exit 1
fi
exit 0
+38
View File
@@ -10,6 +10,8 @@ TMP_DIR="${TMP_DIR:-/tmp/kallilab-posture-check}"
ALLOW_DISK1_NTFS="${ALLOW_DISK1_NTFS:-0}"
ALERT_STATE_PATH="${ALERT_STATE_PATH:-/mnt/user/services/posture-check/last-alert.state}"
ALERT_REPEAT_SECONDS="${ALERT_REPEAT_SECONDS:-86400}"
SKIP_AUTHELIA_DRIFT="${SKIP_AUTHELIA_DRIFT:-0}"
AUTHELIA_DIFF_SCRIPT="${AUTHELIA_DIFF_SCRIPT:-/mnt/user/services/homelab-infra/services/authelia-diff.sh}"
mkdir -p "$TMP_DIR"
RESULTS_FILE="$TMP_DIR/results.$$"
@@ -219,6 +221,41 @@ check_nvme_smart() {
fi
}
check_authelia_config_drift() {
if [ "$SKIP_AUTHELIA_DRIFT" = "1" ]; then
add_result "ok" "authelia_config_drift" "Authelia drift check skipped via SKIP_AUTHELIA_DRIFT=1"
return
fi
if [ ! -x "$AUTHELIA_DIFF_SCRIPT" ] && [ ! -f "$AUTHELIA_DIFF_SCRIPT" ]; then
add_result "warning" "authelia_config_drift" "Authelia diff script missing: $AUTHELIA_DIFF_SCRIPT"
return
fi
local output
local rc
output="$(bash "$AUTHELIA_DIFF_SCRIPT" 2>&1)"
rc=$?
case "$rc" in
0)
add_result "ok" "authelia_config_drift" "Authelia repo baseline matches host config (access_control)"
;;
1)
add_result "warning" "authelia_config_drift" "Authelia repo<->host drift in access_control; run authelia-diff.sh for details"
;;
2)
add_result "warning" "authelia_config_drift" "Authelia diff aborted: $output"
;;
3)
add_result "warning" "authelia_config_drift" "Authelia diff: section missing in repo or host: $output"
;;
*)
add_result "warning" "authelia_config_drift" "Authelia diff returned unexpected rc=$rc: $output"
;;
esac
}
send_ntfy() {
local severity="$1"
local topic="$2"
@@ -388,6 +425,7 @@ main() {
done
check_nvme_smart
check_authelia_config_drift
write_json
}