diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index ec17e4d..efaffd4 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -3,6 +3,16 @@ > [!NOTE] > Most homelabs have one trust boundary: "inside the LAN = trusted." This one has four, each calibrated to a different actual risk level rather than applying maximum security everywhere or none anywhere — that calibration is the hardest and most interesting decision in the whole architecture, and the rest of this doc builds out from it. + + + + + + + + +
11
fleet hosts
55
compose stacks
4
trust boundaries
3
routing authorities
8
traffic laws
+ ## Dependency graph ```mermaid @@ -14,7 +24,7 @@ flowchart LR LAN --> Vault["Vault"] DNS --> Vault - Vault --> Apps["App Host
~40 stacks"] + Vault --> Apps["App Host
41 stacks"] Vault --> Automation["Automation Host"] LAN --> FileServer["File Server"] DNS --> FileServer @@ -55,20 +65,41 @@ The actual answer here is **four boundaries, each doing a different job**: ```mermaid flowchart LR - WAN["WAN"] -->|public| EdgeFW["Edge Firewall
public proxy"] - Browser["LAN browsers"] -->|mTLS + SSO| UserProxy["User-facing proxy"] - Service["App-to-app"] -->|allowlist + SSO| InternalProxy["Internal proxy"] - Infra["LAN infra-to-infra"] -->|per-flow rule| HostFW["Host firewall"] + WAN["🌐 WAN
untrusted"] -->|public| EdgeFW["Edge Firewall
public proxy"] + Browser["🧑 LAN browsers
human-operated"] -->|mTLS + SSO| UserProxy["User-facing proxy"] + Service["📦 App-to-app
machine-provisioned"] -->|allowlist + SSO| InternalProxy["Internal proxy"] + Infra["⚙️ LAN infra-to-infra
non-HTTP"] -->|per-flow rule| HostFW["Host firewall"] + + classDef untrusted fill:#f8cecc,stroke:#b85450,color:#333 + classDef human fill:#dae8fc,stroke:#6c8ebf,color:#333 + classDef machine fill:#d5e8d4,stroke:#82b366,color:#333 + classDef infra fill:#fff2cc,stroke:#d6b656,color:#333 + class WAN,EdgeFW untrusted + class Browser,UserProxy human + class Service,InternalProxy machine + class Infra,HostFW infra ``` | Boundary | Who's on the other end | Control | Why this control, not another | |---|---|---|---| -| WAN → edge | Anyone on the internet | Public reverse proxy, firewall-filtered | The only boundary facing genuinely untrusted traffic — gets the strictest, most isolated treatment | -| Browsers → user-facing proxy | A human, on a device that could be compromised, phished, or simply lost | Mutual TLS (client cert required) + SSO | The highest-value target for credential theft — a stolen password alone isn't enough without the client cert too | -| App-to-app | A container that's already inside the network, already provisioned by the same pipeline as everything else | IP-allowlist + SSO fallback, no mTLS | Full mTLS here would mean managing certificate lifecycle for every internal service-to-service call — real overhead for traffic that's already constrained to known, provisioned sources. The risk profile doesn't justify the same control as human-operated browsers. | -| LAN infra-to-infra | Non-HTTP traffic — metrics, logs, NTP, agent reporting | Explicit per-flow firewall rule, no proxy at all | Can't go through a reverse proxy — it isn't HTTP. The firewall itself is the only layer that *can* enforce this one. | +| 🔴 WAN → edge | Anyone on the internet | Public reverse proxy, firewall-filtered | The only boundary facing genuinely untrusted traffic — gets the strictest, most isolated treatment | +| 🔵 Browsers → user-facing proxy | A human, on a device that could be compromised, phished, or simply lost | Mutual TLS (client cert required) + SSO | The highest-value target for credential theft — a stolen password alone isn't enough without the client cert too | +| 🟢 App-to-app | A container that's already inside the network, already provisioned by the same pipeline as everything else | IP-allowlist + SSO fallback, no mTLS | Full mTLS here would mean managing certificate lifecycle for every internal service-to-service call — real overhead for traffic that's already constrained to known, provisioned sources. The risk profile doesn't justify the same control as human-operated browsers. | +| 🟡 LAN infra-to-infra | Non-HTTP traffic — metrics, logs, NTP, agent reporting | Explicit per-flow firewall rule, no proxy at all | Can't go through a reverse proxy — it isn't HTTP. The firewall itself is the only layer that *can* enforce this one. | -**Three separate routing authorities enforce these** — the edge firewall, the user-facing proxy, and the internal proxy zone — rather than one central router handling everything. That's a deliberate rejection of the simpler single-router design: one router is one thing to hardening-focus on and one thing that takes everything down if it's compromised or misconfigured. Three means a mistake in one router's config, or a full outage of one of them, has a blast radius bounded to the traffic it alone handles — the failure-behavior tables throughout this doc all trace back to this one decision. +**Three separate routing authorities enforce these** — the edge firewall, the user-facing proxy, and the internal proxy zone — rather than one central router handling everything. That's a deliberate rejection of the simpler single-router design: one router is one thing to hardening-focus on and one thing that takes everything down if it's compromised or misconfigured. + +### What actually happens when one fails + +Not a hypothetical — this is the real, deliberate payoff of splitting routing authority three ways instead of one: + +| If this fails... | ❌ Affected | ✅ Unaffected | +|---|---|---| +| Edge firewall | All public/WAN access | Everything internal — browsers, app-to-app, infra traffic all keep working | +| User-facing proxy | Every human browser session | Public access, all app-to-app traffic, all infra traffic | +| Internal proxy zone | App-to-app service calls | Public access, human browser sessions, infra traffic (it doesn't route through here at all) | + +A single-router design fails this test completely — one outage, one misconfiguration, everything goes dark at once. Three independent authorities mean the worst case for any one failure is "a third of the traffic types stop," never all of them. > [!IMPORTANT] > **This wasn't the starting design — it was built up over two real, deliberate changes.** The public-facing reverse proxy used to run as its own dedicated VM; it was consolidated directly onto the edge firewall as a package instead, removing an entire host from the public-ingress path without losing any functionality. And the app-to-app boundary used to not exist at all — internal service traffic ran over a flat, unauthenticated mesh network with no trust boundary. It was replaced by the two-zone model now described in [`docs/dns-naming.md`](dns-naming.md). Both changes added real operational complexity in exchange for a smaller blast radius — accepted deliberately, not stumbled into, and the kind of decision that only looks obvious in hindsight.