Files
ced4568 d2280d0f0d feat(security): production Compose topology, firewall rulesets, Docker mTLS
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).
2026-08-18 17:13:09 -05:00

52 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verifies a generated Docker TLS cert bundle is internally consistent (CA signed both certs,
# key/cert pairs match, SANs are correct) BEFORE deploying it — cheap, local, no real dockerd
# needed. Once deployed, also verifies live mTLS against a real endpoint if given one.
#
# Usage: ./verify-docker-tls.sh <cert-dir> [lab-host-ip:port]
set -euo pipefail
DIR="${1:?usage: verify-docker-tls.sh <cert-dir> [lab-host-ip:port]}"
ENDPOINT="${2:-}"
fail() { echo "FAIL: $1" >&2; exit 1; }
echo "==> Checking file presence"
for f in ca.pem server-cert.pem server-key.pem provisioner-client-cert.pem provisioner-client-key.pem; do
[ -f "$DIR/$f" ] || fail "missing $f"
done
echo "OK: all expected files present"
echo "==> Checking key/cert pairs match (modulus comparison)"
for pair in "server-cert.pem server-key.pem" "provisioner-client-cert.pem provisioner-client-key.pem"; do
set -- $pair
cert_mod=$(openssl x509 -noout -modulus -in "$DIR/$1" | openssl md5)
key_mod=$(openssl rsa -noout -modulus -in "$DIR/$2" | openssl md5)
[ "$cert_mod" = "$key_mod" ] || fail "$1 / $2 do not match"
done
echo "OK: cert/key pairs match"
echo "==> Checking both certs are signed by the CA"
openssl verify -CAfile "$DIR/ca.pem" "$DIR/server-cert.pem" | grep -q "OK" || fail "server-cert.pem not CA-signed"
openssl verify -CAfile "$DIR/ca.pem" "$DIR/provisioner-client-cert.pem" | grep -q "OK" || fail "provisioner-client-cert.pem not CA-signed"
echo "OK: both certs verify against ca.pem"
echo "==> Checking private key permissions"
for f in ca-key.pem server-key.pem provisioner-client-key.pem; do
[ -f "$DIR/$f" ] || continue
perm=$(stat -c '%a' "$DIR/$f" 2>/dev/null || stat -f '%Lp' "$DIR/$f")
[ "$perm" = "400" ] || echo "WARNING: $f has permissions $perm, expected 400"
done
if [ -n "$ENDPOINT" ]; then
echo "==> Testing live mTLS handshake against $ENDPOINT"
echo | openssl s_client -connect "$ENDPOINT" \
-cert "$DIR/provisioner-client-cert.pem" -key "$DIR/provisioner-client-key.pem" \
-CAfile "$DIR/ca.pem" 2>&1 | grep -q "Verify return code: 0" \
&& echo "OK: live mTLS handshake succeeded" \
|| fail "live handshake against $ENDPOINT failed — check firewall/dockerd TLS config"
else
echo "(skipped live handshake test — no endpoint given; this only proves the cert bundle is internally consistent, not that dockerd is actually listening with it)"
fi
echo "==> All checks passed"