berth init
This commit is contained in:
137
berth/ctrl/check.sh
Normal file
137
berth/ctrl/check.sh
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user