docs(homelab): trim operational detail from network table and ADRs

Pure copy edit. Page now publishes the reasoning behind decisions, not
the operational specifics (IPs, subnets, ports, hardware fingerprints,
build pipeline mechanics). Reasoning preserved in every ADR.

- VLAN table: drop Subnet column; replace numeric VLAN IDs with tier names
- ISP gateway ADR: drop carrier and gateway model
- Caddy ADR: tighten DNS-01 framing to internal-services exposure; SSL → TLS
- WireGuard ADR: drop port, VPN subnet, throughput numbers, tier enumeration
- Pi-hole ADR: drop host IP and VLAN ID; sharpen trade-off
- N100 ADR: drop core/clock and precise throughput; rename to "Mini-PC"
- Postgres+Redis ADR: drop apps LXC IP
- Gitea CI/CD ADR: drop runner version, build image, host IPs, deploy mechanics
- Authentik ADR: unchanged
This commit is contained in:
lerko96
2026-04-26 23:19:16 -04:00
parent 4e51dd4a83
commit 5dea6121a3

View File

@@ -22,98 +22,90 @@ const glanceStats = [
const vlans = [ const vlans = [
{ {
id: "1000", id: "MGMT",
name: "MGMT", name: "MGMT",
subnet: "10.0.0.0/24",
purpose: "Network equipment only", purpose: "Network equipment only",
}, },
{ {
id: "1010", id: "LAN",
name: "LAN", name: "LAN",
subnet: "10.1.0.0/24",
purpose: "Trusted personal devices", purpose: "Trusted personal devices",
}, },
{ {
id: "1020", id: "Lab",
name: "Homelab", name: "Homelab",
subnet: "10.2.0.0/24",
purpose: "All self-hosted services", purpose: "All self-hosted services",
}, },
{ {
id: "1030", id: "Guest",
name: "Guests", name: "Guests",
subnet: "10.3.0.0/24",
purpose: "Internet only, RFC1918 blocked", purpose: "Internet only, RFC1918 blocked",
}, },
{ {
id: "1040", id: "IoT",
name: "IoT", name: "IoT",
subnet: "10.4.0.0/24",
purpose: "Smart home, isolated", purpose: "Smart home, isolated",
}, },
{ {
id: "1050", id: "WFH",
name: "WFH", name: "WFH",
subnet: "10.5.0.0/24",
purpose: "Work devices, no personal access", purpose: "Work devices, no personal access",
}, },
{ {
id: "1099", id: "DMZ",
name: "DMZ", name: "DMZ",
subnet: "10.99.0.0/24",
purpose: "Public-facing, hard-blocked internally", purpose: "Public-facing, hard-blocked internally",
}, },
{ {
id: "VPN", id: "VPN",
name: "VPN", name: "VPN",
subnet: "10.200.0.0/24", purpose: "WireGuard clients, LAN-equivalent access",
purpose: "WireGuard clients = LAN access",
}, },
]; ];
const adrs = [ const adrs = [
{ {
title: "AT&T Gateway: IP Passthrough over EAP bypass", title: "ISP gateway: passthrough mode",
decision: decision:
"BGW320 stays in-line with IP Passthrough mode. pfSense gets the public IP directly. Gateway WiFi disabled.", "ISP gateway stays in-line in passthrough mode, pfSense gets the public IP directly. Gateway WiFi disabled.",
why: "AT&T locks 802.1X auth to their gateway hardware. EAP proxy bypass is brittle — breaks on firmware updates and only saves 12ms. True bridge mode isn't supported.", why: "Carrier locks 802.1X auth to their own gateway hardware, and bypassing it is brittle — breaks on firmware updates and only saves a millisecond or two. True bridge mode isn't supported. Passthrough is the cleanest option that keeps pfSense as the actual perimeter.",
}, },
{ {
title: "Caddy over NGINX Proxy Manager", title: "Caddy over NGINX Proxy Manager",
decision: decision:
"Caddy with DNS-01 challenge via Cloudflare API. All subdomains resolve to Caddy internally via Pi-hole. Caddy terminates SSL and proxies to backends.", "Caddy with DNS-01 challenge via Cloudflare API. All subdomains resolve to Caddy internally via Pi-hole. Caddy terminates TLS and proxies to backends.",
why: "Single Caddyfile, auto-cert without exposing port 80/443 to the internet. NPM has more UI overhead for the same outcome. Traefik is more complex for no benefit here.", why: "Single Caddyfile, automatic certs without ever needing to expose internal services to the internet for an HTTP-01 challenge. NPM has more UI overhead for the same outcome. Traefik is more complex for no benefit at this scale.",
}, },
{ {
title: "WireGuard over OpenVPN", title: "WireGuard over OpenVPN",
decision: decision:
"WireGuard on pfSense, UDP 51820, VPN subnet 10.200.0.0/24. Clients get LAN + MGMT access, blocked from Guest/IoT/WFH.", "WireGuard on pfSense as the only remote-access path. Clients get the access tier documented in the access model — same as LAN, plus the admin surfaces that aren't reachable any other way.",
why: "Faster, simpler config, better battery life on mobile. ~600900 Mbps on an N100. OpenVPN has no advantage here. Tailscale adds an external relay dependency.", why: "Faster, simpler config, better battery life on mobile. Throughput on the firewall hardware comfortably exceeds the WAN link. OpenVPN has no advantage here. Tailscale would add an external relay dependency for a problem WireGuard already solves.",
}, },
{ {
title: "Pi-hole in Homelab VLAN, not MGMT", title: "Pi-hole in Homelab VLAN, not MGMT",
decision: decision:
"Pi-hole at 10.2.0.11 (VLAN 1020). Firewall allows port 53 inbound from all VLANs. MGMT uses pfSense Unbound as its primary DNS.", "Pi-hole runs in the Homelab VLAN. Firewall allows port 53 inbound from VLANs that need local resolution. MGMT uses pfSense Unbound as its primary resolver instead.",
why: "Putting Pi-hole in MGMT would require opening MGMT to all VLANs — a larger attack surface. DNS traffic crossing into Homelab VLAN is the lesser risk.", why: "Putting Pi-hole in MGMT would mean opening MGMT to all the VLANs that need DNS — much bigger attack surface for the most sensitive tier. DNS traffic crossing into the Homelab VLAN is the lesser risk, and Homelab is already where service traffic terminates anyway.",
}, },
{ {
title: "N100 for pfSense", title: "Mini-PC for pfSense",
decision: decision:
"Intel N100 mini PC: 4-core 3.4 GHz, ~6W idle. Handles 23 Gbps routing and 600900 Mbps WireGuard.", "Intel N100 mini-PC as the firewall host. ~6W idle, handles multi-Gbps routing, saturates the WAN link with WireGuard headroom to spare.",
why: "Right-sized for 1 Gbps fiber with headroom. Raspberry Pi can't handle 1 Gbps + VPN. A full rack server wastes power for this role.", why: "Right-sized for 1 Gbps fiber. A Raspberry Pi can't handle 1 Gbps plus VPN. A full rack server wastes power for this role and adds noise to a room I sit in.",
}, },
{ {
title: "Shared Postgres + Redis in apps LXC", title: "Shared Postgres + Redis in apps LXC",
decision: decision:
"One Postgres instance, multiple databases. One Redis instance. A single init script provisions all schemas on first run.", "One Postgres instance hosting multiple databases. One Redis instance. A single init script provisions schemas on first run.",
why: "Avoids 15 separate DB containers. Reduces RAM overhead significantly. All productivity apps share the same LXC (10.2.0.60).", why: "Avoids ~15 separate DB containers. Big RAM savings. Productivity apps colocate in one LXC anyway, so a shared backing store there is the natural shape.",
}, },
{ {
title: title:
"Gitea CI/CD: Self-hosted runner with container build + SSH rsync deploy", "Gitea CI/CD: self-hosted runner, internal pipeline, static deploy",
decision: decision:
"act_runner v0.3.1 on Gitea LXC (10.99.0.22). Push to dev → node:22-alpine container builds Next.js → rsync out/ to Portfolio LXC → SSH docker rebuild.", "Self-hosted Gitea Actions runner builds the portfolio on push, then deploys pre-built static files to the public-facing host. Build runs in an isolated container so the runner host stays clean. Public host serves static files only — no build toolchain on it.",
why: "Keeps the full pipeline internal — no GitHub Actions, no external runners. Build runs in an isolated Alpine container so the Gitea LXC isn't polluted. Portfolio LXC (10.99.0.23) just serves pre-built static files via nginx.", why: "Keeps the whole pipeline internal. No external runners, no GitHub Actions. The build/serve split means the public-facing host has the smallest possible footprint — static file server, nothing more.",
}, },
{ {
title: "Authentik over Authelia", title: "Authentik over Authelia",
@@ -176,9 +168,6 @@ export default function HomelabPage() {
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
Name Name
</th> </th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh pr-[3ch] uppercase">
Subnet
</th>
<th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh uppercase"> <th className="font-mono text-[var(--color-text-dim)] text-left py-qtr-lh uppercase">
Purpose Purpose
</th> </th>
@@ -196,9 +185,6 @@ export default function HomelabPage() {
<td className="font-mono text-[var(--color-text)] py-half-lh pr-[3ch]"> <td className="font-mono text-[var(--color-text)] py-half-lh pr-[3ch]">
{v.name} {v.name}
</td> </td>
<td className="font-mono text-[var(--color-text-label)] py-half-lh pr-[3ch]">
{v.subnet}
</td>
<td className="font-mono text-sm text-[var(--color-text)] py-2.5 opacity-80"> <td className="font-mono text-sm text-[var(--color-text)] py-2.5 opacity-80">
{v.purpose} {v.purpose}
</td> </td>