docs: rework security/hardening batch with diagrams and callouts

Mermaid diagrams up front, GFM alert callouts for key decisions and
gotchas, tables instead of bullet walls - replaces plain prose with
the visual template established for security-monitoring.md.
This commit is contained in:
thesabear committed 2026-08-21 23:17:10 +03:00
1 parent a4c334b3d8
commit 41a6f569b4
6 files changed
+247 -114

No files matched your search

+41 -16
View File
@@ -1,34 +1,59 @@
# Firewall & Network Segmentation
# 🧱 Firewall & Network Segmentation
## No blanket LAN-to-LAN allow
> [!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.
The most important rule in the whole model is a negative one: there is no default "anything on the internal network can talk to anything else on it" rule. Every real service-to-service flow — DNS lookups, secrets/PKI traffic, reverse-proxy access, metrics/log shipping, security-agent reporting, NTP sync, admin SSH, file sharing, and a handful of specific app-to-app API integrations — gets its own explicit allow rule. Anything not explicitly listed hits a default-deny-and-log catch-all at the bottom of the ruleset. This catch-all is the fleet's actual lateral-movement boundary, not any single host's own firewall (see below).
```mermaid
flowchart TD
Internet(["Internet"]) --> Edge
subgraph Edge["Edge Firewall"]
Bogon["Bogon block +<br/>IP-reputation blacklist"]
NonstdSSH["SSH — nonstandard port only"]
PubProxy["Public reverse-proxy layer"]
IDS["Inline IDS/IPS"]
end
Edge -->|explicit rules only| LAN
A few of the explicit rules are narrower than they look at first glance, 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.
subgraph LAN["Internal Network"]
direction TB
Rule1["DNS · Secrets/PKI · Reverse proxy<br/>Metrics/logs · Security agents<br/>NTP · Admin SSH · File sharing"]
Catchall["Everything else:<br/>default-deny + log"]
end
```
## Firewall objects are named groups, not raw addresses
## The one rule that matters most
Every rule references named groups instead of hardcoded addresses — adding a host to a group is a single membership edit rather than hunting down and updating every rule that needs to reach it. Groups nest: a "the whole managed fleet" group is composed of several more specific per-role groups, so a rule written against the broad group automatically covers new members added to any of the narrower ones. One group is deliberately scoped *narrower* than its name might suggest — it only covers the fleet's actively-managed tier, and anything outside that tier has to be added on purpose if it needs the same access.
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
On top of the network-level segmentation above, each host's own local firewall opens only the specific ports that host actually needs — not a blanket "allow everything, block nothing" default. One host takes this further: instead of one open port for its most sensitive service, individual source IPs get their own explicit rule, so a compromised unrelated service on the same network still can't reach it.
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.
**A real, documented gap, not glossed over:** on Docker hosts, container-published ports bypass the host's own firewall rules entirely — the container runtime's own network rules are evaluated first, so the host firewall only actually protects the small number of *native* (non-containerized) services running there, not the dozens of containerized apps. The network-level segmentation above (and the edge firewall further out) still applies regardless — this is a defense-in-depth gap, not an open-to-the-world one.
> [!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
## Edge firewall: three jobs, one device
The edge firewall is the fleet's outermost boundary and does three separate jobs:
```mermaid
flowchart LR
EdgeFW["Edge Firewall"] --> J1["Public ingress<br/>bogon block, IP-reputation feed,<br/>nonstandard SSH port"]
EdgeFW --> J2["Second reverse-proxy layer<br/>for genuinely public apps only,<br/>automated wildcard certs"]
EdgeFW --> J3["Security tooling<br/>inline IDS/IPS, cert renewal"]
```
- **Public ingress** — a bogon-address block, an IP-reputation blacklist feed, SSH exposed only on a nonstandard port, and HTTPS/HTTP forwarded to the fleet's public-facing reverse-proxy layer. Nothing else reaches the edge firewall's public interface at all.
- **A second, independent reverse-proxy layer** — separate from the internal two-zone model covered in [DNS Naming Model](dns-naming.md), the edge firewall runs its own reverse-proxy package specifically for the small number of apps that are genuinely meant to be public. Each public app gets its own backend rule mapping its public hostname to the correct internal service, with automated wildcard certificate issuance.
- **Security tooling** — an inline intrusion detection/prevention engine, the same IP-reputation service driving the WAN blacklist, and automated certificate renewal all run at this layer.
**Failure impact, thought through in advance:** if the edge firewall goes down, public access and outbound internet access 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."
> [!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. It's left with an empty ruleset on purpose — the edge firewall and each host's own local firewall already cover the two layers that actually matter, and a third overlapping layer would add complexity without meaningfully improving the security posture. Recognizing when *not* to add a redundant control is as much a design decision as adding one.
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/)