docs: publish 2026-04-27
This commit is contained in:
42
README.md
Normal file
42
README.md
Normal file
@@ -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.
|
||||||
0
assets/.gitkeep
Normal file
0
assets/.gitkeep
Normal file
30
assets/auth-flow.md
Normal file
30
assets/auth-flow.md
Normal file
@@ -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<br/>(reverse proxy)
|
||||||
|
participant A as Authentik<br/>(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<br/>(plain HTTP, internal hop)
|
||||||
|
S-->>U: Response
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If Authentik is down, internal services are unreachable. This is an accepted tradeoff.
|
||||||
47
assets/dns-chain.md
Normal file
47
assets/dns-chain.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# DNS Resolution
|
||||||
|
|
||||||
|
Two flows, one resolver chain.
|
||||||
|
|
||||||
|
## External resolution
|
||||||
|
|
||||||
|
Client asks for a public domain.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
CLIENT[Client<br/>most VLANs] --> PIHOLE[Pi-hole<br/>filtering + cache]
|
||||||
|
PIHOLE -->|miss| UNBOUND[Unbound on firewall<br/>recursive + DNSSEC]
|
||||||
|
UNBOUND --> UPSTREAM[Cloudflare<br/>fallback only]
|
||||||
|
|
||||||
|
PIHOLE -.->|blocked| BLOCKED[Ad/tracker<br/>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<br/>app.lerkolabs.com| PIHOLE[Pi-hole<br/>local A records]
|
||||||
|
PIHOLE -->|returns<br/>internal IP| CLIENT
|
||||||
|
CLIENT -->|HTTPS<br/>valid public cert| CADDY[Internal Caddy<br/>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
|
||||||
|
```
|
||||||
|
|
||||||
88
assets/network-topology.md
Normal file
88
assets/network-topology.md
Normal file
@@ -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<br/>reverse proxy + public services]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph TRUSTED["Trusted"]
|
||||||
|
LAN[LAN<br/>personal devices]
|
||||||
|
INT[Internal services<br/>app stack]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph MGMT["Management — VPN-only"]
|
||||||
|
ADMIN[Hypervisor, firewall,<br/>backup, switches, APs]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph REMOTE["Remote"]
|
||||||
|
VPN[WireGuard clients]
|
||||||
|
end
|
||||||
|
|
||||||
|
INTERNET((Internet))
|
||||||
|
|
||||||
|
UNTRUSTED -->|outbound only| INTERNET
|
||||||
|
INTERNET -->|HTTP/HTTPS<br/>tight allowlist| DMZ
|
||||||
|
INTERNET -->|WireGuard<br/>UDP| VPN
|
||||||
|
|
||||||
|
DMZ -.->|narrow allowlist<br/>firewall-enforced| INT
|
||||||
|
LAN -->|consume services| INT
|
||||||
|
VPN -->|LAN-equivalent +<br/>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<br/>passthrough mode]
|
||||||
|
GW --> FW[pfSense firewall]
|
||||||
|
FW --> SW[Managed switch<br/>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.
|
||||||
39
docs/NETWORK.md
Normal file
39
docs/NETWORK.md
Normal file
@@ -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.
|
||||||
23
docs/SECURITY.md
Normal file
23
docs/SECURITY.md
Normal file
@@ -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
|
||||||
97
docs/SERVICES.md
Normal file
97
docs/SERVICES.md
Normal file
@@ -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 |
|
||||||
|
|
||||||
Reference in New Issue
Block a user