Files
rootnode-academy/infra/firewall/rn-platform-01.rules
T
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

59 lines
3.4 KiB
Plaintext

# iptables-restore ruleset for rn-platform-01 (Platform VM). Deployed by
# infra/ansible/roles/firewall/. Variables ({{ }}) are Ansible Jinja2, rendered before this
# file reaches the host — see infra/ansible/group_vars/production.yml.
#
# Policy summary (full rationale: docs/PRODUCTION_NETWORK_SECURITY.md §6):
# - only the reverse proxy's 443/80 are reachable from the internet
# - SSH only from the management CIDR (your home/office/VPN, never 0.0.0.0/0)
# - Docker TLS (2376) is a rn-lab-host-01 concern, not this host's — nothing to open here
# - Postgres, Provisioner's internal API, and the Platform API/Gateway's raw ports are never
# exposed on this VM's external interface at all — the reverse proxy is the only public
# listener; everything else binds to the docker-compose internal network only, so there is
# no host-level rule needed to "hide" them, only to make sure nothing ever publishes them
# by accident (this ruleset's default-deny INPUT catches that class of mistake too)
# - rn-lab-host-01 may reach exactly one port here: Loki (3100), for its own log shipper —
# the Provisioner otherwise calls OUT to rn-lab-host-01, never the other way around — and
# may NEVER reach Postgres or the Provisioner's internal API
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
# Loopback and established/related — standard baseline
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
# ICMP — keep basic reachability diagnostics working
-A INPUT -p icmp --icmp-type echo-request -m conntrack --ctstate NEW -j ACCEPT
# SSH — management CIDR only, never open to the internet
-A INPUT -p tcp --dport 22 -s {{ management_cidr }} -m conntrack --ctstate NEW -j ACCEPT
# Public HTTPS/HTTP — the reverse proxy (Caddy), the only public listener on this host
-A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
# Loki log ingestion — rn-lab-host-01's promtail only (that host's own log shipper, pushing its
# container/journald logs here for centralized viewing in Grafana). Same "one specific source
# IP, one specific port" pattern as Docker TLS on the other host.
-A INPUT -p tcp --dport 3100 -s {{ lab_host_ip }} -m conntrack --ctstate NEW -j ACCEPT
# Explicit deny + log for anything from rn-lab-host-01 hitting Postgres or the Provisioner's
# internal port — defense in depth on top of "these ports were never published," and the one
# rule in this file that most directly encodes the Lab->Platform boundary the whole platform
# depends on. Logged (not silently dropped) so an attempted breach attempt is visible in
# infra/monitoring/ alerts, not just absorbed by the default DROP.
-A INPUT -s {{ lab_host_ip }} -p tcp --dport 5432 -j LOG --log-prefix "ROOTNODE-DENY-LABHOST-PG: " --log-level 4
-A INPUT -s {{ lab_host_ip }} -p tcp --dport 5432 -j DROP
-A INPUT -s {{ lab_host_ip }} -p tcp --dport 4100 -j LOG --log-prefix "ROOTNODE-DENY-LABHOST-PROVISIONER: " --log-level 4
-A INPUT -s {{ lab_host_ip }} -p tcp --dport 4100 -j DROP
# Default: everything else already falls to the chain policy (DROP), nothing further needed —
# but an explicit final logged catch-all makes "what got denied and why" visible in logs rather
# than silent.
-A INPUT -j LOG --log-prefix "ROOTNODE-DENY-PLATFORM-DEFAULT: " --log-level 4
COMMIT