Named-bridge-network production Compose (not host networking, unlike dev) for rn-platform-01; egress-proxy + agents for rn-lab-host-01. Caddy reverse proxy, single public origin, Grafana restricted to management_cidr. iptables-restore firewall rulesets for all three hosts (Proxmox host, rn-platform-01, rn-lab-host-01) codifying the Platform/Lab-Host boundary: no student-to-student traffic, no direct Proxmox exposure, no direct Postgres/Provisioner exposure from the Lab Host. Mutual TLS for the Provisioner<->Lab Host Docker connection: certificate generation/verification scripts, dockerd TLS config, firewall-restricted port 2376. Application support already existed since Phase 1; this is purely the operational half. Validated: docker compose config, caddy validate, firewall structural lint, and a real openssl s_server/s_client mTLS handshake proving the cert chain and mutual-auth rejection (legitimate client verified, rogue client correctly rejected with verify code 19).
64 lines
2.5 KiB
Bash
Executable File
64 lines
2.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Local test (Phase 4 §29): renders each .rules file with dummy IPs and validates it.
|
|
#
|
|
# `iptables-restore --test` needs root (it talks to netfilter) — if this shell has passwordless
|
|
# sudo, we use it for a real semantic check. Otherwise we fall back to a structural lint
|
|
# (balanced *table/COMMIT blocks, every rule starts with a real flag, no unrendered {{ }}
|
|
# placeholders left over) — good enough to catch a typo'd rule or a missing COMMIT, NOT a
|
|
# substitute for the real `iptables-restore --test` a human/Ansible with actual root runs
|
|
# before this ever reaches a live host. HARDWARE-DEPENDENT: proving the real IPs/CIDRs are
|
|
# correct needs the actual production network, not this script.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
TMPDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
|
|
render() {
|
|
sed \
|
|
-e 's/{{ management_cidr }}/203.0.113.0\/24/g' \
|
|
-e 's/{{ platform_ip }}/10.10.10.10/g' \
|
|
-e 's/{{ lab_host_ip }}/10.10.10.20/g' \
|
|
-e 's/{{ lab_subnet_range }}/10.42.0.0\/16/g' \
|
|
-e 's/{{ proxmox_mgmt_cidr }}/203.0.113.0\/24/g' \
|
|
-e "s/{{ lab_gateway_port_range | default('30000:32767') }}/30000:32767/g" \
|
|
"$1" > "$2"
|
|
}
|
|
|
|
structural_lint() {
|
|
local f="$1"
|
|
grep -q '^\*filter' "$f" || { echo " missing *filter table declaration"; return 1; }
|
|
grep -q '^COMMIT' "$f" || { echo " missing COMMIT"; return 1; }
|
|
grep -v '^#' "$f" | grep -q '{{' && { echo " unrendered {{ }} placeholder left in a non-comment line"; return 1; }
|
|
# every -A/-I rule line should end in a real target (ACCEPT/DROP/REJECT/LOG/a custom chain)
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^-[AI]\ ]] || continue
|
|
[[ "$line" =~ -j\ [A-Za-z0-9_-]+ ]] || { echo " rule missing a -j target: $line"; return 1; }
|
|
done < "$f"
|
|
return 0
|
|
}
|
|
|
|
HAVE_SUDO=0
|
|
sudo -n true 2>/dev/null && HAVE_SUDO=1
|
|
|
|
FAIL=0
|
|
for f in proxmox-host.rules rn-platform-01.rules rn-lab-host-01.rules; do
|
|
render "$f" "$TMPDIR/$f"
|
|
echo "==> $f"
|
|
if [ "$HAVE_SUDO" = "1" ] && command -v iptables-restore >/dev/null 2>&1; then
|
|
if sudo -n iptables-restore --test < "$TMPDIR/$f"; then
|
|
echo "OK: $f passed a real iptables-restore --test"
|
|
else
|
|
echo "FAIL: $f did not pass iptables-restore --test" >&2; FAIL=1
|
|
fi
|
|
else
|
|
if structural_lint "$TMPDIR/$f"; then
|
|
echo "OK: $f passes structural lint (no root available for a real iptables-restore --test — see script header)"
|
|
else
|
|
echo "FAIL: $f failed structural lint" >&2; FAIL=1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
exit $FAIL
|