deploy --diff: never diff seeded secret files, redact secret values

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
2026-09-23 16:41:31 -04:00
co-authored by Claude Sonnet 5
parent f4bb65de9c
commit 667edc8621
2 changed files with 34 additions and 3 deletions
+16 -3
View File
@@ -6,6 +6,7 @@
# ./deploy.sh --check [conf] # validate conf + render all templates, no install # ./deploy.sh --check [conf] # validate conf + render all templates, no install
# ./deploy.sh --diff [conf] # what would change in /etc # ./deploy.sh --diff [conf] # what would change in /etc
# ./deploy.sh render <file> # preview a templated file on stdout # ./deploy.sh render <file> # preview a templated file on stdout
# ./deploy.sh --redact # filter stdin, masking secret values (used by --diff; for tests)
# Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN # Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN
# port on br0) is NOT deployed here — reference copy in ap/50-van-wan.yaml, # port on br0) is NOT deployed here — reference copy in ap/50-van-wan.yaml,
# applied once manually (apply flaps uplinks). We do check it for drift below. # applied once manually (apply flaps uplinks). We do check it for drift below.
@@ -86,6 +87,12 @@ install_rendered() { # install_rendered <src> <dst> [mode]
rm -f "$tmp" rm -f "$tmp"
} }
redact_secrets() { # stdin -> stdout; mask values of secret-looking keys, keep key + diff markers
sed -E \
-e 's/((pass(word|phrase)|secret|token)[A-Za-z_]*[[:space:]]*=).*/\1<redacted>/I' \
-e 's/("[A-Za-z_]*(pass(word|phrase)|secret|token|user)[A-Za-z_]*"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"<redacted>"/I'
}
template_files() { # every repo file that carries an @TOKEN@ placeholder template_files() { # every repo file that carries an @TOKEN@ placeholder
grep -rlE "$TOKEN_RE" --exclude-dir=.git --exclude-dir=__pycache__ --exclude-dir=.superpowers \ grep -rlE "$TOKEN_RE" --exclude-dir=.git --exclude-dir=__pycache__ --exclude-dir=.superpowers \
--exclude-dir=docs --exclude-dir=tests --exclude-dir=hosts \ --exclude-dir=docs --exclude-dir=tests --exclude-dir=hosts \
@@ -114,23 +121,29 @@ case "${1:-}" in
echo "NEW $dst" echo "NEW $dst"
elif ! diff -q <(render "$src") "$dst" >/dev/null; then elif ! diff -q <(render "$src") "$dst" >/dev/null; then
echo "CHANGED $dst" echo "CHANGED $dst"
diff -u "$dst" <(render "$src") | sed 's/^/ /' || true diff -u "$dst" <(render "$src") | redact_secrets | sed 's/^/ /' || true
fi fi
done done
# Also lists plain `install` files and `rm -f` targets — these sections may be # Also lists plain `install` files and `rm -f` targets — these sections may be
# flag-gated in the real run, but --diff reports them regardless of the flag. # flag-gated in the real run, but --diff reports them regardless of the flag.
grep -E '^[[:space:]]*install -D -m[0-9]+ +[^" ]+ +/' deploy.sh | while read -r _ _ _ src dst _; do grep -E '^[[:space:]]*install -D -m[0-9]+ +[^" ]+ +/' deploy.sh | while read -r _ _ mode src dst _; do
# seed-if-absent secret files (from *.example, or 0600): never diff or print
case "$src" in *.example) continue ;; esac
[ "$mode" = -m0600 ] && continue
if [ ! -e "$dst" ]; then if [ ! -e "$dst" ]; then
echo "NEW $dst" echo "NEW $dst"
elif ! cmp -s "$src" "$dst"; then elif ! cmp -s "$src" "$dst"; then
echo "CHANGED $dst" echo "CHANGED $dst"
diff -u "$dst" "$src" | sed 's/^/ /' || true diff -u "$dst" "$src" | redact_secrets | sed 's/^/ /' || true
fi fi
done done
grep -E '^[[:space:]]*rm -f +/' deploy.sh | while read -r _ _ path _; do grep -E '^[[:space:]]*rm -f +/' deploy.sh | while read -r _ _ path _; do
[ -e "$path" ] && echo "REMOVE $path" [ -e "$path" ] && echo "REMOVE $path"
done || true done || true
exit 0 ;; exit 0 ;;
--redact) # ./deploy.sh --redact — stdin through redact_secrets (no config, no root)
redact_secrets
exit 0 ;;
render) render)
[ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; } [ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; }
load_conf "$VANLINK_CONF" load_conf "$VANLINK_CONF"
+18
View File
@@ -151,4 +151,22 @@ for c in hosts/*.conf; do
done done
done done
# 10. --diff never touches seed-if-absent secret files, and redacts secret values
out=$(./deploy.sh --diff hosts/wan.conf 2>&1) || true
leak=$(echo "$out" | grep -E '/etc/van-gps/config\.json|/etc/van-li3/config\.json|/etc/van-battery/pushover\.json') || true
if [ -z "$leak" ]; then
ok "--diff ignores seeded secret files"
else
bad "--diff mentions seeded secret files"; echo "$leak" | sed 's/^/ /'
fi
red=$(printf '%s\n' 'wpa_passphrase=hunter2' '+ "password": "hunter2",' '- "token": "abc123"' ' "user": "u123"' | ./deploy.sh --redact 2>&1) || true
if echo "$red" | grep -qE 'hunter2|abc123|u123'; then
bad "--redact leaks a secret value"; echo "$red" | sed 's/^/ /'
elif echo "$red" | grep -q '^wpa_passphrase=' && echo "$red" | grep -q '^+ "password"' \
&& echo "$red" | grep -q '^- "token"' && echo "$red" | grep -q '^ "user"'; then
ok "--redact masks values, keeps keys and diff markers"
else
bad "--redact lost keys or diff markers"; echo "$red" | sed 's/^/ /'
fi
exit "$fail" exit "$fail"