138 lines
5.5 KiB
Bash
138 lines
5.5 KiB
Bash
#!/usr/bin/env bash
|
|
# Is this estate coherent? Reports and instructs; never fixes.
|
|
#
|
|
# Takes no subcommand — there is one question to ask.
|
|
# Every check corresponds to something wrong in the estate today.
|
|
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
source ./lib/config.sh
|
|
source ./lib/estate.sh
|
|
load_config
|
|
|
|
WORST=0
|
|
note() { echo " $*"; }
|
|
warn() { echo " WARN $*"; [ "$WORST" -lt 1 ] && WORST=1; return 0; }
|
|
bad() { echo " FAIL $*"; WORST=2; return 0; }
|
|
|
|
echo "estate: $ESTATE target: $TARGET domain: $DOMAIN"
|
|
echo
|
|
|
|
# 1. cert coverage: a wildcard matches exactly one label.
|
|
echo "certs — does the cert cover every name the services serve?"
|
|
issued=$(estate_get "certs.issued" | python3 -c 'import json,sys
|
|
try: print("\n".join(json.load(sys.stdin)))
|
|
except Exception: pass')
|
|
if [ -z "$issued" ]; then
|
|
warn "no certs.issued in the estate file — cannot check coverage."
|
|
else
|
|
while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do
|
|
[ -z "$host" ] && continue
|
|
fqdn="${host}.${DOMAIN}"
|
|
# A '*' host stands for "any single label here" — check the deepest
|
|
# name it can produce, which is the one that fails.
|
|
probe="$fqdn"
|
|
case "$host" in \*.*) probe="anyroom.${host#\*.}.${DOMAIN}" ;; esac
|
|
covered=""
|
|
while IFS= read -r san; do
|
|
[ -z "$san" ] && continue
|
|
if san_covers "$probe" "$san"; then covered=1; break; fi
|
|
done <<< "$issued"
|
|
if [ -z "$covered" ]; then
|
|
bad "$name: '$probe' is covered by NO issued SAN"
|
|
note " issued: $(echo "$issued" | tr '\n' ' ')"
|
|
note " a wildcard matches exactly ONE label — reissue with"
|
|
note " -d '*.${host#\*.}.${DOMAIN}' or move the name one level up"
|
|
fi
|
|
done < <(estate_services "$TARGET")
|
|
[ "$WORST" -lt 2 ] && note "every service name is covered."
|
|
fi
|
|
echo
|
|
|
|
# 2. unmatched names: DNS and the cert are wildcard, nginx is exact, so
|
|
# without a :443 default_server the fallback is whichever vhost loads first.
|
|
echo "gateway — is there a deliberate answer for unmatched names?"
|
|
DEFAULT_CONF="${PPL_DIR:-$HOME/wdir/semester/ppl}/gateway/nginx/conf.d/default.conf"
|
|
if [ ! -f "$DEFAULT_CONF" ]; then
|
|
note "ppl not on this machine at $DEFAULT_CONF — skipped."
|
|
elif grep -qE '^\s*listen\s+443.*default_server' "$DEFAULT_CONF"; then
|
|
note "default.conf has a :443 default_server."
|
|
else
|
|
bad "default.conf has NO :443 default_server."
|
|
note " Unmatched names fall through to the first-loaded vhost."
|
|
note " This is a PREREQUISITE for generating any config: adding a"
|
|
note " generated include changes load order, and load order is what"
|
|
note " currently decides the fallback."
|
|
fi
|
|
echo
|
|
|
|
# 3. a rule allowing a port nothing listens on is dead config; a service no
|
|
# compose file declares is undocumented state. Needs both halves to see.
|
|
echo "firewall — rules against listeners"
|
|
estate_get "firewall" | python3 -c '
|
|
import json,sys
|
|
try: fw = json.load(sys.stdin)
|
|
except Exception: fw = []
|
|
for r in fw:
|
|
n = r.get("note")
|
|
print(" %-6s %-5s %s" % (r["port"], r.get("proto","tcp"), r.get("desc","")))
|
|
if n: print(" UNRESOLVED: " + n)
|
|
'
|
|
note "listener side: unknown until captured (ss -ltnp over ssh $HOST)."
|
|
wg=$(estate_get "vpn._status")
|
|
[ -n "$wg" ] && warn "overlay: not fully captured — see 'make vpn check'" && \
|
|
note " 10.8.0.1 carries the registry and woodpecker gRPC;" && \
|
|
note " 10.8.0.2 backs langfuse. No 51820/udp rule, nothing creates" && \
|
|
note " the interface — a fresh box cannot start the gateway compose."
|
|
note "overlay detail: make vpn show estate"
|
|
echo
|
|
|
|
# 4. ssh aliases, never hostnames: a bare hostname offers every agent key and
|
|
# trips MaxAuthTries before reaching the right one.
|
|
echo "ssh — aliases, never hostnames"
|
|
for var in HOST HOST_ADMIN; do
|
|
val="${!var:-}"
|
|
if [ -z "$val" ]; then
|
|
warn "$var is unset."
|
|
elif [[ "$val" == *.* ]]; then
|
|
bad "$var='$val' looks like a hostname, not a ~/.ssh/config alias."
|
|
note " A bare hostname trips MaxAuthTries before reaching the key."
|
|
elif [ -f "$HOME/.ssh/config" ] && grep -qiE "^\s*Host\s+.*\b${val}\b" "$HOME/.ssh/config"; then
|
|
note "$var=$val — Host block present."
|
|
else
|
|
warn "$var='$val' has no matching Host block in ~/.ssh/config."
|
|
fi
|
|
done
|
|
for f in "$HOME/wdir/semester/ppl/ctrl/.env"; do
|
|
[ -f "$f" ] || continue
|
|
if grep -qE '^SERVER=.*\.' "$f"; then
|
|
warn "$f sets SERVER to a hostname, not an alias — every ppl script inherits it."
|
|
fi
|
|
done
|
|
echo
|
|
|
|
# ── 5. toolchain ───────────────────────────────────────────────────────────
|
|
echo "toolchain"
|
|
for t in python3 "$TOFU_BIN" aws gcloud ssh rsync wg; do
|
|
if command -v "$t" >/dev/null 2>&1; then
|
|
note "$(printf '%-8s' "$t") present"
|
|
else
|
|
note "$(printf '%-8s' "$t") MISSING — $(case $t in
|
|
tofu) echo 'blocks: estate plan/apply; terraform works identically' ;;
|
|
wg) echo 'blocks: vpn keygen and the overlay checks' ;;
|
|
aws) echo 'blocks: the control-plane inventory, dns on route53' ;;
|
|
gcloud) echo 'blocks: the gcp estate' ;;
|
|
*) echo 'blocks: most things' ;;
|
|
esac)"
|
|
fi
|
|
done
|
|
echo
|
|
|
|
case "$WORST" in
|
|
0) echo "OK" ;;
|
|
1) echo "OK, with warnings" ;;
|
|
2) echo "PROBLEMS FOUND — see FAIL lines above" ;;
|
|
esac
|
|
exit 0
|