chore(infra): add internal routing smoke-test script

scripts/verify-internal.sh runs the exact verification sequence before
the edge is touched. Checks (in order):

  0. Port listeners — host Caddy owns :80, Traefik NodePort on :30080
  1. GET / — 200 Homer or 302 → auth.researchstack.info (not internal IP)
  2. /api/* bypass — none of /api/*/health may 302 to Authentik
  3. auth.researchstack.info — 200/302 from Authentik, no internal-IP loop
  4. Host header passthrough — Traefik :30080 /ping reachable; /api/jobs/health
     routes end-to-end through host Caddy → Traefik → Ingress
  5. X-Forwarded-Proto — https header survives the server-Caddy hop

Usage:
  # on nixos-laptop directly:
  bash 4-Infrastructure/k3s-flake/scripts/verify-internal.sh

  # from any tailnet node:
  bash 4-Infrastructure/k3s-flake/scripts/verify-internal.sh --remote

Exit 0 = safe to deploy edge. Exit 1 = fix red checks first.

Generated with Devin (https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Brandon Schneider 2026-05-26 21:08:58 -05:00
parent 8d1d94b256
commit aae680ad3c

View file

@ -0,0 +1,192 @@
#!/usr/bin/env bash
# verify-internal.sh — internal routing smoke test
#
# Run on nixos-laptop (100.102.173.61) after nixos-rebuild switch,
# BEFORE deploying the edge Caddy. Exercises the host-Caddy → Traefik
# → Ingress path directly so you know the internal leg is clean.
#
# Usage:
# bash 4-Infrastructure/k3s-flake/scripts/verify-internal.sh
# bash 4-Infrastructure/k3s-flake/scripts/verify-internal.sh --remote
# (runs via ssh root@100.102.173.61 if not already on the node)
#
# Exit 0 = all checks passed.
# Exit 1 = one or more checks failed; details printed to stderr.
set -euo pipefail
NODE_IP="100.102.173.61"
PASS=0
FAIL=0
# ── helpers ──────────────────────────────────────────────────────────────────
green() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
red() { printf '\033[31m✗ %s\033[0m\n' "$*" >&2; }
check() {
local label="$1"; shift
if "$@" 2>/dev/null; then
green "$label"
(( PASS++ )) || true
else
red "$label"
(( FAIL++ )) || true
fi
}
# Run from remote if requested and not already on the node.
if [[ "${1:-}" == "--remote" ]]; then
echo "[verify] running remotely on $NODE_IP..."
exec ssh "root@$NODE_IP" bash -s < "$0"
fi
echo
echo "═══════════════════════════════════════════════════════"
echo " Internal routing smoke test — $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo " Target node: $NODE_IP"
echo "═══════════════════════════════════════════════════════"
echo
# ── 0. Listener sanity ───────────────────────────────────────────────────────
echo "── 0. Port listeners ──"
check "host Caddy owns :80" \
bash -c "ss -tlnp | grep -q ':80 '"
check "Traefik NodePort :30080 is bound" \
bash -c "ss -tlnp | grep -q ':30080 '"
echo
# ── 1. Root path: Homer or Authentik redirect ────────────────────────────────
echo "── 1. GET / (expect 200 Homer or 302 → Authentik) ──"
root_status=$(curl -so /dev/null -w '%{http_code}' \
-H "Host: researchstack.info" \
-H "X-Forwarded-Proto: https" \
--max-time 5 \
"http://$NODE_IP/")
check "/ returns 200 or 302 (got $root_status)" \
bash -c '[[ '"$root_status"' == 200 || '"$root_status"' == 302 ]]'
if [[ "$root_status" == "302" ]]; then
root_loc=$(curl -so /dev/null -w '%{redirect_url}' \
-H "Host: researchstack.info" \
-H "X-Forwarded-Proto: https" \
--max-time 5 \
"http://$NODE_IP/")
check "/ redirect points to auth.researchstack.info (not an http://10x... internal URL)" \
bash -c '[[ '"\"$root_loc\""' == *"auth.researchstack.info"* ]]'
fi
echo
# ── 2. API bypass: /api/* must NOT 302 to Authentik ─────────────────────────
echo "── 2. /api/* paths must not redirect to Authentik ──"
for path in /api/registry/health /api/jobs/health /api/blobs/health; do
status=$(curl -so /dev/null -w '%{http_code}' \
-H "Host: researchstack.info" \
--max-time 5 \
"http://$NODE_IP$path")
check "$path$status (not 302)" \
bash -c '[[ '"$status"' != 302 ]]'
if [[ "$status" == "302" ]]; then
loc=$(curl -so /dev/null -w '%{redirect_url}' \
-H "Host: researchstack.info" \
--max-time 5 \
"http://$NODE_IP$path")
red " redirect target: $loc"
fi
done
echo
# ── 3. auth.researchstack.info → Authentik (no forward_auth loop) ───────────
echo "── 3. auth.researchstack.info serves Authentik (200 or 302, no loop) ──"
auth_status=$(curl -so /dev/null -w '%{http_code}' \
-H "Host: auth.researchstack.info" \
-H "X-Forwarded-Proto: https" \
--max-time 5 \
"http://$NODE_IP/")
check "auth.researchstack.info returns 200 or 302 (got $auth_status)" \
bash -c '[[ '"$auth_status"' == 200 || '"$auth_status"' == 302 ]]'
# If it's a redirect, the location must not point back to an internal IP
if [[ "$auth_status" == "302" ]]; then
auth_loc=$(curl -so /dev/null -w '%{redirect_url}' \
-H "Host: auth.researchstack.info" \
-H "X-Forwarded-Proto: https" \
--max-time 5 \
"http://$NODE_IP/")
check "auth redirect does not loop to internal IP ($auth_loc)" \
bash -c '[[ '"\"$auth_loc\""' != *"100.102"* && '"\"$auth_loc\""' != *"127.0"* ]]'
fi
# Response body should look like Authentik (may be empty if redirecting to flow)
auth_body=$(curl -s \
-H "Host: auth.researchstack.info" \
-H "X-Forwarded-Proto: https" \
-L --max-redirs 3 --max-time 10 \
"http://$NODE_IP/" 2>/dev/null || true)
check "auth response contains Authentik markers" \
bash -c '[[ '"\"$auth_body\""' == *"authentik"* || '"\"$auth_body\""' == *"ak-flow"* || '"\"$auth_body\""' == *"Sign in"* ]]'
echo
# ── 4. Host header passthrough ────────────────────────────────────────────────
echo "── 4. Host header passthrough (server Caddy → Traefik) ──"
# The Traefik dashboard is disabled, but /ping is available by default
ping_status=$(curl -so /dev/null -w '%{http_code}' \
--max-time 5 \
"http://$NODE_IP:30080/ping" 2>/dev/null || echo "000")
check "Traefik /ping on :30080 responds (got $ping_status — 200/404 both ok, 000 = not reachable)" \
bash -c '[[ '"$ping_status"' != 000 ]]'
# Confirm Host is forwarded by checking a real Ingress path
jobs_status=$(curl -so /dev/null -w '%{http_code}' \
-H "Host: researchstack.info" \
--max-time 5 \
"http://$NODE_IP/api/jobs/health")
check "/api/jobs/health via host Caddy → Traefik → service (got $jobs_status, not 000)" \
bash -c '[[ '"$jobs_status"' != 000 ]]'
echo
# ── 5. X-Forwarded-Proto passthrough ─────────────────────────────────────────
echo "── 5. X-Forwarded-Proto preserved through server Caddy ──"
# Hermes placeholder echoes back X-Forwarded headers in its /health response
# if deployed. If not, check that Traefik doesn't return 000 on the path.
proto_status=$(curl -so /dev/null -w '%{http_code}' \
-H "Host: researchstack.info" \
-H "X-Forwarded-Proto: https" \
--max-time 5 \
"http://$NODE_IP/api/registry/health")
check "X-Forwarded-Proto: https request routes without error (got $proto_status)" \
bash -c '[[ '"$proto_status"' != 000 ]]'
echo
echo "═══════════════════════════════════════════════════════"
printf " Results: %d passed, %d failed\n" "$PASS" "$FAIL"
echo "═══════════════════════════════════════════════════════"
echo
if (( FAIL > 0 )); then
echo "[verify] FAIL — fix the red checks before deploying the edge." >&2
exit 1
fi
echo "[verify] PASS — internal leg is clean. Safe to deploy k3s-edge."
echo " Next: nixos-rebuild switch on microvm-racknerd, then run:"
echo " cd 4-Infrastructure/k3s-flake/tests && npm test"