# 🧱 Firewall & Network Segmentation
> [!NOTE]
> No blanket "anything on the internal network can talk to anything else on it" rule, anywhere. Every real flow is explicit; everything else is logged and dropped.
```mermaid
flowchart TD
Internet(["Internet"]) --> Edge
subgraph Edge["Edge Firewall"]
Bogon["Bogon block +
IP-reputation blacklist"]
NonstdSSH["SSH — nonstandard port only"]
PubProxy["Public reverse-proxy layer"]
IDS["Inline IDS/IPS"]
end
Edge -->|explicit rules only| LAN
subgraph LAN["Internal Network"]
direction TB
Rule1["DNS · Secrets/PKI · Reverse proxy
Metrics/logs · Security agents
NTP · Admin SSH · File sharing"]
Catchall["Everything else:
default-deny + log"]
end
```
## The one rule that matters most
Every real service-to-service flow — DNS lookups, secrets/PKI traffic, reverse-proxy access, metrics/log shipping, security-agent reporting, NTP, admin SSH, file sharing, and a handful of specific app-to-app integrations — gets its own explicit allow rule. Anything not explicitly listed hits a default-deny-and-log catch-all. **This catch-all is the fleet's actual lateral-movement boundary**, not any single host's own firewall.
> [!TIP]
> A few explicit rules are narrower than they look on purpose — inbound access to the secrets platform's own admin API, for instance, is gated to only the two or three services that ever legitimately need it, not the whole fleet.
## Named groups, not raw addresses
Every rule references named groups instead of hardcoded addresses — adding a host to a group is a single membership edit, not a hunt through every rule that needs to reach it. Groups nest: a "whole managed fleet" group is composed of several narrower per-role groups, so a rule against the broad group automatically covers new members of any narrower one.
## Per-host firewalls: minimal, not blanket
Each host's own local firewall opens only the specific ports it actually needs. One host goes further: instead of one open port for its most sensitive service, individual source IPs get their own explicit rule.
> [!WARNING]
> **A real, documented gap**: on Docker hosts, container-published ports bypass the host's own firewall entirely — the container runtime's own network rules are evaluated first. Per-host firewall rules only actually protect the small number of *native*, non-containerized services on that host. The network-level segmentation above (and the edge firewall further out) still applies regardless — defense-in-depth gap, not an open-to-the-world one.
## Edge firewall: three jobs, one device
```mermaid
flowchart LR
EdgeFW["Edge Firewall"] --> J1["Public ingress
bogon block, IP-reputation feed,
nonstandard SSH port"]
EdgeFW --> J2["Second reverse-proxy layer
for genuinely public apps only,
automated wildcard certs"]
EdgeFW --> J3["Security tooling
inline IDS/IPS, cert renewal"]
```
> [!IMPORTANT]
> **Failure impact, thought through in advance**: if the edge firewall goes down, public access and outbound internet both go with it — but every internal service that doesn't depend on WAN reachability keeps running normally. The blast radius is deliberately bounded to "the edge," not "the fleet."
## The hypervisor's own firewall: deliberately unused
The virtualization platform underneath everything has its own built-in firewall capability, left with an empty ruleset on purpose. The edge firewall and each host's own local firewall already cover the two layers that matter — a third overlapping layer would add complexity without meaningfully improving posture. Recognizing when *not* to add a redundant control is as much a design decision as adding one.
---
Related stacks: [`stacks/platform/caddy/`](../stacks/platform/caddy/), [`stacks/platform/caddy-internal/`](../stacks/platform/caddy-internal/), [`stacks/platform/docker-socket-proxy/`](../stacks/platform/docker-socket-proxy/)