From 73c1dde7e514201171b637f7b86bff85b4f5a32d Mon Sep 17 00:00:00 2001 From: lerko96 Date: Mon, 27 Apr 2026 02:22:31 -0400 Subject: [PATCH] docs: publish 2026-04-27 --- README.md | 42 +++++++++++++++++ assets/.gitkeep | 0 assets/auth-flow.md | 30 ++++++++++++ assets/dns-chain.md | 47 ++++++++++++++++++ assets/network-topology.md | 88 ++++++++++++++++++++++++++++++++++ docs/NETWORK.md | 39 +++++++++++++++ docs/SECURITY.md | 23 +++++++++ docs/SERVICES.md | 97 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 366 insertions(+) create mode 100644 README.md create mode 100644 assets/.gitkeep create mode 100644 assets/auth-flow.md create mode 100644 assets/dns-chain.md create mode 100644 assets/network-topology.md create mode 100644 docs/NETWORK.md create mode 100644 docs/SECURITY.md create mode 100644 docs/SERVICES.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7038a07 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# homelab + +Self-hosted services on a single Proxmox host. Segmented network, runs 24/7. + +## Why I built this + +I started this while studying for CompTIA and the plan was a small router, a few VLANs, and maybe two or three services and then got carried away. + +## What's running + +| Layer | Tool | +|---|---| +| Hypervisor | Proxmox VE | +| Firewall | pfSense (low-power x86) | +| Switching | TP-Link Omada (managed VLANs) | +| Reverse proxy | Caddy (Cloudflare DNS-01) | +| Identity | Authentik (OIDC + forward auth) | +| DNS | Pi-hole → Unbound → Cloudflare | +| Remote access | WireGuard | +| Monitoring | Victoria Metrics + Grafana + Beszel | +| Backups | Proxmox Backup Server | + +## Scope + +Around 10 LXCs and a couple of VMs running about 20 services across 7 VLANs. + +## Design choices + +- VLANs are organized by trust tier. Management is its own tier because a compromise there would be no bueno +- Internal services sit behind Authentik. OIDC where the app supports it and then Caddy forward auth where it doesn't +- Public surface is small. A handful of services, behind a DMZ-isolated reverse proxy with firewall rules backing up the proxy config +- Admin surfaces are only available from Management tier and VPN. + +## Documented here + +| Doc | About | +|---|---| +| [Services](docs/SERVICES.md) | What's deployed, grouped by what it does | +| [Network](docs/NETWORK.md) | Segmentation, firewall posture, DNS | +| [Security](docs/SECURITY.md) | Layered controls, threat model, limitations | + +The IP plan, hardware inventory, ADRs, rebuild runbook, and retention policies are in a private repo. diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/auth-flow.md b/assets/auth-flow.md new file mode 100644 index 0000000..2fe09b8 --- /dev/null +++ b/assets/auth-flow.md @@ -0,0 +1,30 @@ +# Authentication Flow + +Forward auth path for an internal service that doesn't speak OIDC natively. OIDC-native services skip the Caddy auth hop and go to Authentik directly. + +```mermaid +sequenceDiagram + participant U as User + participant C as Caddy
(reverse proxy) + participant A as Authentik
(IdP) + participant S as Internal service + + U->>C: HTTPS request + C->>A: Forward auth check + A-->>C: 401 (no session) + C-->>U: 302 → auth.lerkolabs.com + + U->>A: Login (OIDC or password) + A-->>U: Set session cookie + + U->>C: HTTPS request + cookie + C->>A: Forward auth check + A-->>C: 200 OK + identity headers + C->>S: Proxy request
(plain HTTP, internal hop) + S-->>U: Response +``` + + +## Notes + +- If Authentik is down, internal services are unreachable. This is an accepted tradeoff. diff --git a/assets/dns-chain.md b/assets/dns-chain.md new file mode 100644 index 0000000..569aa8b --- /dev/null +++ b/assets/dns-chain.md @@ -0,0 +1,47 @@ +# DNS Resolution + +Two flows, one resolver chain. + +## External resolution + +Client asks for a public domain. + +```mermaid +graph LR + CLIENT[Client
most VLANs] --> PIHOLE[Pi-hole
filtering + cache] + PIHOLE -->|miss| UNBOUND[Unbound on firewall
recursive + DNSSEC] + UNBOUND --> UPSTREAM[Cloudflare
fallback only] + + PIHOLE -.->|blocked| BLOCKED[Ad/tracker
domains] + + classDef client fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0 + classDef resolver fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0 + classDef upstream fill:#3a2f1f,stroke:#8b6b3a,color:#f0e0d0 + classDef blocked fill:#3a1f1f,stroke:#8b3a3a,color:#f0d0d0 + + class CLIENT client + class PIHOLE,UNBOUND resolver + class UPSTREAM upstream + class BLOCKED blocked +``` + +## Local hostname resolution (split-horizon) + +Client asks for an internal hostname. The query stays on the LAN. Pi-hole answers from local A records and the client connects to the internal reverse proxy. + +```mermaid +graph LR + CLIENT[Client] -->|asks for
app.lerkolabs.com| PIHOLE[Pi-hole
local A records] + PIHOLE -->|returns
internal IP| CLIENT + CLIENT -->|HTTPS
valid public cert| CADDY[Internal Caddy
reverse proxy] + CADDY --> SVC[Internal service] + + classDef client fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0 + classDef resolver fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0 + classDef edge fill:#2f1f3a,stroke:#6b3a8b,color:#e0d0f0 + + class CLIENT client + class PIHOLE resolver + class CADDY,SVC edge +``` + diff --git a/assets/network-topology.md b/assets/network-topology.md new file mode 100644 index 0000000..085e5c4 --- /dev/null +++ b/assets/network-topology.md @@ -0,0 +1,88 @@ +# Network Topology + +Two views of the same network. + +## Trust tiers and policy + +Seven VLANs grouped by trust level. Edges show allowed inter-tier flows; everything else is default-deny. + +```mermaid +graph TB + subgraph UNTRUSTED["Untrusted — internet only, no internal access"] + GUEST[Guest WiFi] + IOT[IoT] + WFH[Work-from-home] + end + + subgraph PUBLIC["Public-facing"] + DMZ[DMZ
reverse proxy + public services] + end + + subgraph TRUSTED["Trusted"] + LAN[LAN
personal devices] + INT[Internal services
app stack] + end + + subgraph MGMT["Management — VPN-only"] + ADMIN[Hypervisor, firewall,
backup, switches, APs] + end + + subgraph REMOTE["Remote"] + VPN[WireGuard clients] + end + + INTERNET((Internet)) + + UNTRUSTED -->|outbound only| INTERNET + INTERNET -->|HTTP/HTTPS
tight allowlist| DMZ + INTERNET -->|WireGuard
UDP| VPN + + DMZ -.->|narrow allowlist
firewall-enforced| INT + LAN -->|consume services| INT + VPN -->|LAN-equivalent +
admin access| INT + VPN --> ADMIN + + classDef untrusted fill:#3a1f1f,stroke:#8b3a3a,color:#f0d0d0 + classDef public fill:#3a2f1f,stroke:#8b6b3a,color:#f0e0d0 + classDef trusted fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0 + classDef mgmt fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0 + classDef remote fill:#2f1f3a,stroke:#6b3a8b,color:#e0d0f0 + + class GUEST,IOT,WFH untrusted + class DMZ public + class LAN,INT trusted + class ADMIN mgmt + class VPN remote +``` + +## Physical flow + +What plugs into what. Tier labels, not addresses. + +```mermaid +graph LR + ISP[ISP] --> GW[Carrier gateway
passthrough mode] + GW --> FW[pfSense firewall] + FW --> SW[Managed switch
VLAN-aware] + + SW --> T_MGMT[MGMT tier] + SW --> T_INT[Internal services tier] + SW --> T_LAN[LAN tier] + SW --> T_WFH[WFH tier] + SW --> T_IOT[IoT tier] + SW --> T_GUEST[Guest tier] + SW --> T_DMZ[DMZ tier] + + FW -.->|VPN concentrator| VPN[WireGuard] +``` + +## Two reverse proxies + +The DMZ-to-internal arrow above is by design. There are two Caddy instances: + +- One in DMZ, internet-facing, fronting a small set of public services. +- One in internal services tier, LAN/VPN only, fronting everything else. + +## Notes + +- Inter-tier policy enforced at the firewall. diff --git a/docs/NETWORK.md b/docs/NETWORK.md new file mode 100644 index 0000000..560a801 --- /dev/null +++ b/docs/NETWORK.md @@ -0,0 +1,39 @@ +# Network + +## Trust tiers + +| Tier | What's on it | Posture | +|---|---|---| +| Management | Hypervisor, firewall, backup server, network controllers | Most trusted. VPN-only. No outbound unless required. | +| Internal services | LXCs and VMs running the app stack | Trusted. Serves clients in adjacent tiers per policy. | +| LAN | Personal devices on home WiFi/Ethernet | Trusted. Consumes internal services. | +| Work-from-home | Employer-owned laptop | Untrusted lateral. Internet only. Blocked from everything internal, including DNS. | +| IoT | Smart devices, cloud-managed appliances | Untrusted. Internet only. Isolated from internal. | +| Guest | Visitor WiFi | Untrusted. Internet only. | +| DMZ | Internet-facing services | Treated as compromised by default. Tight inbound allowlist to internal. | +| VPN (WireGuard) | Authenticated remote clients | Same posture as LAN, plus admin-tier visibility. | + +## Policy + +- Default deny inter-VLAN. Every cross-tier flow is an explicit allow rule with a reason. +- WFH and IoT are restricted to internet only. Nothing internal, including DNS for local hostnames. +- Management is kept minimal. Only what runs the lab lives there. +- DMZ is one-way. Public services in there can only initiate inward through a firewall-enforced allowlist by source IP + destination port with reverse proxy reinforcing. +- Admin only accessible via Management + VPN + +## DNS + +Three layers: + +1. **Pi-hole** — first hop for clients on most VLANs. Filters ad/tracker domains and holds local A records. Not used by Management hosts or by the WFH VLAN. +2. **Unbound on the firewall** — Pi-hole's upstream. Recursive resolver, validates DNSSEC. +3. **Cloudflare** — Unbound's upstream when needed. + +The hypervisor (which is the box Pi-hole runs on) statically resolves through the firewall, not Pi-hole. If it didn't, there'd be a circular dependency at boot. + +## Internet exposure + +Three ports forwarded from WAN: + +- HTTP and HTTPS to the DMZ reverse proxy. +- WireGuard to the firewall. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..31be711 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,23 @@ +# Security + +## Threat model + +One-person homelab on a residential connection. + +## Update + +- Edge components: patched promptly when CVEs land. +- Hypervisor and backup server: quarterly review, with security patches applied when needed. +- Application LXCs: rolling updates on a regular schedule. certain ones take precent +- Container images: re-pulled on the same rolling schedule. + +## Backups + +Hypervisor-level backups go to a dedicated backup server. Conservative retentions and backups are verified periofically.The rebuild order is documented. + +## Limitations + +This is a learning environment. + +- No High Availability - One hypervisor, one firewall +- One-person ops diff --git a/docs/SERVICES.md b/docs/SERVICES.md new file mode 100644 index 0000000..dd01218 --- /dev/null +++ b/docs/SERVICES.md @@ -0,0 +1,97 @@ +# Services + +## Identity & access + +| Service | What it does | +|---|---| +| Authentik | SSO for internal services, OIDC where supported + caddy forward auth otherwis | +| Pi-hole | LAN DNS, ad blocking + source of truth for internal hostnames | +| WireGuard | remote access | + +## Reverse proxy & TLS + +Two Caddy instances: + +- **Internal Caddy** fronts everything internal. LAN or VPN only. +- **DMZ Caddy** fronts the public services. Lives on its own VLAN with a firewall-enforced allowlist into internal. + +Both use Cloudflare DNS-01 for ACME, which lets internal-only services get valid public certs without being exposed for issuance. + +## Productivity & knowledge + +| Service | What it replaces | +|---|---| +| Outline | notion | +| Vikunja | todoist / asana | +| Hoarder | pocket / raindrop | +| Memos | apple nnotes | +| FreshRSS | feedly | +| Bytestash | gist / pastebin | +| Filebrowser | dropbox | +| Baikal | iCloud calendar/contacts (CalDAV / CardDAV) | + +## Money + +| Service | What it replaces | +|---|---| +| Actual Budget | YNAB | +| Ghostfolio | personal capital | + +## Operations + +| Service | What it does | +|---|---| +| Grist | lightweight excel type | +| Glance | personal homepage | +| Traggo | time tracking | + +## Media + +| Service | What it does | +|---|---| +| Plex | mdia library (legacy clients) | +| Jellyfin | media library (primary) | +| *arr stack | library automation | +| qBittorrent | Downloads | +| Immich | photo backup and viewing | + +## Home / IoT + +| Service | What it does | +|---|---| +| Home Assistant OS | home automation hub | + +## Secrets + +| Service | What it does | +|---|---| +| Vaultwarden | bitwarden-compatible password manager *Planned, not deployed yet | + +## Bots & automation + +| Service | What it does | +|---|---| +| Vocard | discord music bot | +| MonitorRSS | rss-to-discord feed | +| ntfy | push notifications for ops alerts | + +## Monitoring + +| Service | What it does | +|---|---| +| Victoria Metrics | time-series store | +| Grafana | dashboards | +| Beszel | host metrics | +| Uptime Kuma | uptime checks | + +## Public services + +A small set behind the DMZ reverse proxy on a VLAN with no inbound to internal. + +| Service | Why it's public | +|---|---| +| Portfolio | it's a portfolio | +| Self-hosted Git | so you can read this | +| SSO endpoint | required for the OIDC flow on the Discord bot dashboard. the firewall is enabled so that the public proxy can only reach this one internal backend | +| Discord bot dashboard | so my friends can use pick tunes. authentik forward auth gates it | +