Network

Addresses use RFC 5737 documentation ranges and generic role names - they show the structure, not the real allocations.

Segments

Trust is split across segments that match how exposed each is.

Segment Example range Holds Trust
Management LAN 192.0.2.0/24 Core server, workstation, laptops, and the VMs (bridged) Highest (wired, local)
VPN clients 198.51.100.0/24 Authenticated remote devices Medium (remote, authed)
Docker bridges 203.0.113.0/24 Per-tier isolated container networks on the core server Scoped per tier

Docker bridges are isolated per platform tier rather than sharing one flat network, so a container in the app tier cannot reach the Git/CI tier laterally.

NoxLab network topology: edge router and management LAN, VPN-only inbound, and a core server hosting tiered Docker plus bridged VMs, with an outbound tunnel for public services
// network topology - abstracted (example addresses, role names)

Why it's shaped this way

Nothing inbound - by rule, from day one. "Nothing inbound" was a design rule before it was ever an implementation. The usual advice is to forward a port and firewall it down; my answer is, why not just leave port 22 open to the internet while you are at it? A forwarded-and-hardened port is still a port on the public internet waiting to be wrong. The whole shape of the network falls out of refusing that - one VPN port in, and public services that dial out over a tunnel instead of opening anything.

Self-hosted DNS, on purpose. The easy path is to point everything at the router or at 1.1.1.1 and forget it. I run my own resolver for two reasons: learning (DNS is foundational, and you only understand it by operating it), and not handing my ISP a log of everywhere I go - which is what the DoH layer is for.

At a glance

network · the firewall is code, not clicks
sed -n '/SSH via LAN/,+12p' roles/ufw/tasks/main.yml
- community.general.ufw: rule: allow port: "{{ ssh_port }}" comment: SSH via LAN - community.general.ufw: rule: allow port: "{{ openvpn_port }}" comment: OpenVPN - community.general.ufw: { rule: allow, port: "443", comment: Traefik HTTPS }
# every rule is declarative, commented and version-controlled - the same ruleset rebuilds identically

Full mechanics - ingress paths, cert fan-out, egress control, DNS layers, and the gotchas →