#!/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)." # Ask the structure, not the prose: the condition is "does a peer still lack a # public key", not "is there a _status string". _status is ALWAYS non-empty — # capture rewrites it to "CAPTURED ..." — so testing it for emptiness pinned # this warning on permanently, including after the capture it asks for. uncaptured=$(estate_get "vpn.overlays.estate.peers" 2>/dev/null | python3 -c ' import json, sys try: peers = json.load(sys.stdin) except Exception: sys.exit(0) print(" ".join(n for n, p in peers.items() if not p.get("public_key"))) ' 2>/dev/null) if [ -n "$uncaptured" ]; then warn "overlay: public keys not captured for:$uncaptured — see 'make vpn check'" note " 10.8.0.1 carries the registry and woodpecker gRPC;" note " 10.8.0.2 backs langfuse. Nothing in the tree creates the" note " interface — a fresh box cannot start the gateway compose." note " capture with: sudo wg show | make vpn capture --write" else note "overlay: $(estate_get 'vpn._status')" fi 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