docs: publish 2026-04-26

This commit is contained in:
lerko96
2026-04-26 22:26:14 -04:00
parent 6730781dd0
commit a061d37602
22 changed files with 388 additions and 2032 deletions
-87
View File
@@ -1,87 +0,0 @@
# Decisions
Architecture Decision Records (ADR-lite). Key choices and rationale.
---
## D001 — Public/private split: git subtree + push script + pre-push hook
**Decision:** Public content lives under `public/`. Push script uses `git subtree push --prefix=public` to publish it as root to the public remote. Pre-push hook blocks direct pushes that bypass the script.
**Why:** git-filter-repo is designed for one-time rewrites, not recurring pushes. Separate branches require manual discipline. git subtree is pure git, produces clean history on the public remote, and the script stays two lines.
**Status:** decided
---
## D002 — Public remote: self-hosted Gitea → GitHub mirror
**Decision:** Public remote is a self-hosted Gitea instance. Gitea mirrors to GitHub automatically.
**Why:** Matches existing portfolio site setup. Local workflow only pushes to Gitea; GitHub propagation is transparent. No extra tooling needed.
**Constraint:** Filtering must be airtight before push — whatever reaches Gitea lands on GitHub within seconds.
**Status:** decided
---
## D003 — Private remote: private repo on same Gitea instance
**Decision:** Private remote is a separate private repository on the same self-hosted Gitea instance.
**Why:** Easiest path — infrastructure already exists, one tool to manage.
**Risk:** Single point of failure. If Gitea host goes down, both remotes are inaccessible. Accepted for now.
**Status:** decided
---
## D004 — Shared Postgres + Redis in apps LXC
**Decision:** Single Postgres instance with multiple databases + single Redis instance, both in the `apps` LXC. All productivity apps share this infrastructure.
**Why:** Avoids 15 separate DB containers. A single init script provisions all schemas on first run.
**Risk:** If Postgres goes down, all productivity apps go down simultaneously.
**Status:** decided
---
## D005 — AT&T gateway kept in-line (IP Passthrough, not EAP bypass)
**Decision:** BGW320 stays in-line with IP Passthrough mode (DHCPS-fixed to pfSense WAN MAC). pfSense gets the public IP directly. Gateway WiFi disabled.
**Why:** AT&T locks 802.1X certificate auth to their gateway hardware. EAP proxy bypass breaks on AT&T firmware updates and only saves 12ms latency. True bridge mode not supported.
**Status:** decided
---
## D006 — Caddy over NGINX Proxy Manager, with Cloudflare DNS-01
**Decision:** Caddy with `caddy-dns/cloudflare` plugin. DNS-01 challenge via Cloudflare API. All `*.lerkolabs.com` subdomains → 10.2.0.20 in Pi-hole. Caddy terminates SSL, proxies to backends.
**Why:** Single Caddyfile, auto-cert, no UI overhead. No port 80/443 needed on WAN.
**Alternatives:** NGINX Proxy Manager (more UI overhead), Traefik (more complex config, same result), self-signed certs (browser warnings).
**Status:** decided
---
## D007 — WireGuard over OpenVPN
**Decision:** WireGuard on pfSense, UDP 51820, VPN subnet 10.200.0.0/24. VPN clients get same access as LAN.
**Why:** Lower latency, better mobile battery life, ~600Mbps on the N100. OpenVPN adds complexity with no advantage here.
**Status:** decided
---
## D008 — Authentik over Authelia
**Decision:** Authentik as SSO provider for all services.
**Why:** Full OIDC provider + forward auth in one. Lets services like Outline, Gitea, and Vikunja use real SSO rather than just a login gate. Authelia only does forward auth.
**Status:** decided
---
## D009 — Pi-hole in Homelab VLAN (1020), not MGMT
**Decision:** Pi-hole at 10.2.0.11 in VLAN 1020. Firewall allows port 53 inbound from all VLANs. MGMT VLAN uses pfSense as primary DNS.
**Why:** Placing Pi-hole in MGMT would require allowing all VLANs to reach MGMT — larger attack surface than filtering DNS traffic from Homelab VLAN.
**Status:** decided
---
## D010 — Intel N100 for pfSense
**Decision:** Intel N100 mini PC. 4-core 3.4GHz, ~6W idle. Handles 23Gbps routing, 600900Mbps WireGuard.
**Why:** Right-sized for 1Gbps fiber with headroom. Raspberry Pi insufficient for 1Gbps + VPN. Full rack server overkill power draw.
**Status:** decided
-69
View File
@@ -1,69 +0,0 @@
# Inventory
Hardware inventory — make/model, role, specs. See [README](../README.md) for how everything fits together.
## Active Hardware
| Device | Role | Model | Notes |
|--------|------|-------|-------|
| Proxmox host | Hypervisor | Dual-socket Xeon rackmount | 32c / 128GB RAM |
| Proxmox backup server | Backup server | HP desktop | Backup all LXCs + VMs |
| pfSense router | Firewall / VPN / DHCP / routing | Netgate 1100 | ~6W idle, handles 23Gbps routing + 600Mbps WireGuard |
| Managed switch | VLAN switching | TP-Link Omada L2+ managed | All port VLANs managed via Omada Controller |
| Access points | WiFi | TP-Omada WiFi 6 | Auto-adopted by Omada Controller |
| AT&T Gateway | ISP ONT | AT&T-issued | IP Passthrough |
## pfSense Box Detail
| Property | Value |
|----------|-------|
| CPU | Intel N100 (4-core, 3.4GHz) |
| Idle power | ~6W |
| Routing throughput | 23Gbps |
| WireGuard throughput | ~600Mbps |
| pfSense version | pinned |
## Proxmox Host Detail
| Property | Value |
|----------|-------|
| CPU | Intel Xeon E5-2683 v4 (32-core, 2.10GHz) |
| RAM | 128 GB |
| Boot drive | 128 GB |
| Storage | 1500 GB |
| Proxmox version | pinned |
## Proxmox Backup Detail |
| Property | Value |
|----------|-------|
| CPU | Intel i5-4590T (4-core, 2.00GHz) |
| RAM | 16 GB |
| Boot drive | 256 GB |
| Storage | 2 TB |
| PBS version | pinned |
## Licensing / Subscriptions
| Service | Type | Notes |
|---------|------|-------|
| Cloudflare | Free | lerkolabs.com DNS + DNS-01 challenge |
| Let's Encrypt | Free | Via Caddy — auto-renewal |
| AT&T Fiber | Monthly | 1Gbps symmetric |
| Domain Name | Annually | `lerkolabs.com` |
## Backup (PBS)
Nightly backups + weekly GC for all LXCs + VMs, managed by Proxmox Backup Server.
**Covered:** pihole, auth, infra, monitor, apps, vault, servarr VM, haos VM
### Retention
| Keep | Amount |
|------|--------|
| Last | 3 |
| Daily | 13 |
| Weekly | 8 |
| Monthly | 11 |
| Yearly | 2 |
+40 -106
View File
@@ -1,130 +1,64 @@
# Network
VLAN map, firewall policy, DNS architecture, and physical topology. See [README](../README.md) for the big picture and [Services](SERVICES.md) for what lives where.
How I think about segmentation and why the policy looks the way it does. Specific subnets, VLAN IDs, IP plans, and firewall rule listings live in the private repo.
## VLAN Map
## Why segmentation matters here
| VLAN ID | Name | Subnet | Gateway | DHCP Range | DNS |
|---------|------|--------|---------|------------|-----|
| 1000 | MGMT | 10.0.0.0/24 | 10.0.0.1 | 10.0.0.100150 | pfSense only |
| 1010 | LAN | 10.1.0.0/24 | 10.1.0.1 | 10.1.0.100200 | Pi-hole → pfSense |
| 1020 | Homelab | 10.2.0.0/24 | 10.2.0.1 | 10.2.0.100200 | Pi-hole → pfSense |
| 1030 | Guests | 10.3.0.0/24 | 10.3.0.1 | 10.3.0.100250 | Pi-hole → pfSense |
| 1040 | IoT | 10.4.0.0/24 | 10.4.0.1 | 10.4.0.100250 | Pi-hole → pfSense |
| 1050 | WFH | 10.5.0.0/24 | 10.5.0.1 | 10.5.0.100200 | pfSense only |
| 1099 | DMZ | 10.99.0.0/24 | 10.99.0.1 | static only | pfSense only |
| — | VPN | 10.200.0.0/24 | pfSense | assigned by WG | Pi-hole → pfSense |
A homelab pulls together an unusually wide trust spread on one piece of hardware: cloud-managed IoT devices that phone home constantly, a work laptop that touches an employer network, a guest WiFi that strangers join, internal services holding sensitive data, and admin surfaces that should never be exposed. Treating all of that as one flat network treats it like it has the same trust level. It doesn't.
## Firewall Policy
The model here is **trust-tier VLANs** with explicit policy between them. Every tier has a documented purpose and a defined inbound/outbound posture.
Default: **deny all inter-VLAN unless explicitly allowed.**
## Trust tiers
| VLAN | Policy Summary |
|------|---------------|
| LAN (1010) | Full internet; can reach Homelab + MGMT; blocked from Guest/IoT/WFH |
| Homelab (1020) | Internet for updates (HTTP/S, SSH, NTP); cannot initiate to other VLANs |
| Guests (1030) | Internet only — hard block on all RFC1918 |
| IoT (1040) | Internet + Home Assistant (explicit rule); blocked from LAN |
| WFH (1050) | Internet only; pfSense DNS only; no personal network access |
| MGMT (1000) | Updates + NTP outbound; inbound from LAN + VPN only |
| DMZ (1099) | HTTP/S + NTP outbound; hard-blocked from all internal VLANs |
| VPN (10.200.0.0/24) | Same as LAN: Homelab + MGMT web GUI + Pi-hole DNS |
Seven VLANs, organized roughly by how much I trust what's on them:
## Static IP Reservations
| Tier | What's on it | Posture |
|---|---|---|
| **Management** | Hypervisor, firewall, backup server, network controllers | Most trusted. Reachable only via VPN. Doesn't initiate outbound unless it has to. |
| **Internal services** | LXCs and VMs running the internal 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 else, including internal DNS. |
| **IoT** | Smart devices, cloud-managed appliances | Untrusted. Internet only. Isolated from everything internal. |
| **Guest** | Visitor WiFi | Untrusted. Internet only. |
| **DMZ** | Internet-facing services | Treated as compromised by default. Locked down on outbound; inbound to internal is a tight allowlist. |
| **VPN (WireGuard)** | Authenticated remote clients | Same posture as LAN, plus admin-tier visibility. |
### VLAN 1000 — MGMT
## Policy posture
| IP | Device |
|----|--------|
| 10.0.0.1 | pfSense MGMT |
| 10.0.0.2 | Omada Switch |
| 10.0.0.3 | Guest AP |
| 10.0.0.4 | IoT AP |
- **Default deny inter-VLAN.** Every cross-tier flow is an explicit allow rule with a reason written next to it.
- **WFH and IoT are jailed.** They reach the internet and nothing else internal — not even DNS for the local hostnames. This is the most important rule in the firewall.
- **Management is the smallest possible tier.** Only what *runs* the lab lives there. No user-facing services. No outbound internet from anything that doesn't strictly need it.
- **DMZ is one-way.** Public services live there. They can't initiate connections inward except through a tight, firewall-enforced allowlist by source IP and destination port. The reverse proxy in the DMZ is *configured* to respect that, and the firewall is *also* configured to enforce it. Two layers, on purpose — misconfiguring the proxy is way easier than misconfiguring the firewall.
- **Admin surfaces are VPN-only.** Hypervisor, firewall, backup server, switches, APs — none of them are reachable from the internet. WireGuard first or it doesn't happen.
### VLAN 1010 — LAN
## DNS
| IP | Device |
|----|--------|
| 10.1.0.1 | pfSense LAN gateway |
Three layers, each doing one job:
### VLAN 1020 — Homelab
1. **Pi-hole** — first hop for clients on most VLANs. Filters ad/tracker domains and holds the local A records that map internal hostnames to internal IPs. Not used by management hosts (see below) or by the WFH VLAN.
2. **Unbound on the firewall** — Pi-hole's upstream. Recursive resolver, validates DNSSEC.
3. **Cloudflare** — Unbound's eventual upstream when needed.
| IP | Device |
|----|--------|
| 10.2.0.1 | pfSense Homelab gateway |
| 10.2.0.10 | Proxmox |
| 10.2.0.11 | Pi-hole |
| 10.2.0.20 | Caddy (infra LXC) |
| 10.2.0.21 | Vaultwarden (vault LXC) |
| 10.2.0.25 | Authentik (auth LXC) |
| 10.2.0.51 | Monitor LXC |
| 10.2.0.60 | Apps LXC |
**Bootstrap exception:** the hypervisor itself (which is the box Pi-hole runs on) is statically pointed at the firewall's resolver, not Pi-hole. Otherwise there's a circular dependency at boot — the hypervisor needs DNS to come up, and Pi-hole is one of the things the hypervisor brings up.
### VLAN 1099 — DMZ
**Known SPOF:** Pi-hole is the only thing resolving internal hostnames. If it dies, internal hostnames stop resolving until it's back. I thought about mirroring the records into Unbound on pfSense and decided not to — I'd rather know if Pi-hole is unhealthy than paper over it. Documented as a known limitation in the private repo.
| IP | Device |
|----|--------|
| 10.99.0.1 | pfSense DMZ gateway |
| 10.99.0.20 | Public Service A |
| 10.99.0.22 | Public Service B |
| 10.99.0.23 | Public Service C |
## Internet exposure
## IP Block Allocation (VLAN 1020)
Three ports forwarded from WAN to internal:
| Block | Purpose |
|-------|---------|
| 10.2.0.19 | Infrastructure (gateway, pfSense interfaces) |
| 10.2.0.1019 | Network critical (Proxmox, Pi-hole) |
| 10.2.0.2029 | Auth / Proxy (Caddy, Authentik, Vaultwarden) |
| 10.2.0.3039 | Observability |
| 10.2.0.4049 | Dev tools |
| 10.2.0.5059 | Data |
| 10.2.0.6069 | Apps |
| 10.2.0.7079 | Files |
| 10.2.0.8099 | Media |
| 10.2.0.100+ | DHCP pool (dynamic) |
- **HTTP / HTTPS** — to the DMZ reverse proxy. Serves the small public service set.
- **WireGuard** — to the firewall. The only remote admin path.
## DNS Architecture
Everything else is closed. I verify this from outside the network on a regular basis — the only way to actually know what's exposed is to scan from somewhere that isn't the LAN.
```
Device → Pi-hole (10.2.0.11)
pfSense Unbound (10.x.0.1) — local records + DHCP hostnames
Cloudflare 1.1.1.1 (upstream)
```
## IPv6
- Pi-hole: ad/tracker blocking, local DNS records (all `*.lerkolabs.com` → 10.2.0.20 Caddy), query logging
- pfSense Unbound: DHCP hostname registration, backup resolver if Pi-hole is down
- WFH VLAN: pfSense DNS only — Pi-hole unreachable by design
Disabled at the carrier-provided gateway. The lab is IPv4-only by design — fewer surfaces, simpler firewall reasoning, no AAAA leakage. I'll revisit this if I have a reason to; today I don't.
## Physical Topology
## Things that are easy to overlook
```
AT&T Fiber ONT
|
AT&T BGW320 (IP Passthrough)
|
pfSense N100 (WAN/LAN)
|
Omada Managed Switch
├── Trunk port → pfSense (all VLANs tagged)
├── VLAN 1000 — MGMT devices
├── VLAN 1010 — Desktop / LAN
├── VLAN 1020 — Proxmox / Homelab servers
├── VLAN 1030 — Guest WiFi AP
├── VLAN 1040 — IoT WiFi AP
├── VLAN 1050 — Work laptop
└── VLAN 1099 — DMZ
```
A couple of things worth being explicit about, because they bit me at some point:
## WireGuard VPN
| Property | Value |
|----------|-------|
| Listen Port | 51820 UDP |
| VPN Subnet | 10.200.0.0/24 |
| Access granted | Homelab + MGMT web GUI + Pi-hole DNS |
| Access blocked | Guest, IoT, WFH |
No management ports (22, 8006, 443) exposed to the internet. WireGuard is the only inbound port on the WAN interface (aside from Cloudflare DNS-01 challenge traffic, which uses no inbound ports).
- **Intra-VLAN traffic between LXCs on the same Proxmox bridge doesn't traverse the firewall.** Isolation is enforced *per-VLAN*, not *per-LXC*. Two LXCs sharing a tier can talk to each other directly. Useful to remember when you're reasoning about blast radius — the firewall doesn't see anything that doesn't cross a VLAN boundary.
- **Certificate Transparency.** Caddy uses Cloudflare DNS-01 for cert issuance, which is great because services don't have to be exposed to the internet to get a cert. But every cert that gets issued lands in CT logs forever, and per-hostname certs basically publish the internal hostname inventory to anyone who runs a CT search on the domain. A wildcard cert would limit CT exposure to `*.lerkolabs.com` and the apex; it's on my list as a future change, with the tradeoff being that wildcard compromise is worse than per-host.
-3
View File
@@ -1,3 +0,0 @@
# RUNBOOKS
_stub_
+38 -37
View File
@@ -1,54 +1,55 @@
# Security
Security posture — what's exposed, how auth works, update cadence, known debt. See [Network](NETWORK.md) for VLAN isolation details.
Posture and practices. No enumerated weaknesses or specific control parameters here — those live in the private repo where they belong.
## Internet-Exposed Ports
## Threat model
| Port | Protocol | Destination | Purpose |
|------|----------|-------------|---------|
| 51820 | UDP | pfSense WAN | WireGuard VPN |
Operating assumption: this is a one-person homelab on a residential connection, running a mix of services I rely on (password manager, calendar, photos) and a few that are reachable from the internet (portfolio, self-hosted Git). The realistic threat is opportunistic — bots scanning my public IP, automated exploitation of known CVEs in exposed services, credential stuffing against any public login.
No management ports (22, 8006, 443) exposed to the internet. All admin access requires an active WireGuard connection first. Cloudflare DNS-01 challenge handles TLS — no port 80/443 needed on WAN.
A targeted, well-resourced adversary is out of scope. The defenses are designed to make the cost of opportunistic attacks high enough that automation gives up and moves on.
## Authentication Layers
## Layered controls
| Layer | Mechanism | Coverage |
|-------|-----------|----------|
| All web services | Authentik SSO (OIDC or forward auth) | 100% of `*.lerkolabs.com` |
| VPN | WireGuard pre-shared keys | Required for all remote access |
| pfSense | Web GUI + SSH key | VPN-only access |
| Proxmox | Web GUI + SSH key | VPN-only access |
| Secrets | Vaultwarden (isolated LXC) | All credentials |
The defenses are intentionally redundant. Any single layer failing should leave the next one intact.
No service is accessible anonymously. Guests and IoT have zero access to any internal service.
1. **Network segmentation.** Trust-tier VLANs with default-deny inter-tier policy. A compromise on one VLAN doesn't get lateral movement to another without crossing an explicit firewall rule.
2. **Public surface stays small.** Only a handful of services are reachable from the internet, all proxied through a DMZ-isolated host with a tight firewall-enforced allowlist into internal.
3. **TLS at the edge, properly.** ACME-automated certs via Cloudflare DNS-01. Public hostnames carry HSTS preload-eligible headers.
4. **Identity in front of everything internal.** Authentik handles SSO. OIDC where the app supports it; reverse-proxy forward auth where it doesn't. There's no "log in with the app's local account" bypass.
5. **Admin is VPN-only.** Hypervisor, firewall, backup server, switches, APs — none of them reachable from the internet. WireGuard is the only remote admin path, and it has its own keypair-based auth.
6. **Secrets aren't in git.** Configs reference secrets by name; values live in a password manager.
## Secrets Policy
## Update cadence
- No plaintext secrets in any config file committed to the repo
- All secrets referenced by Vaultwarden entry name (e.g., `homelab/pfsense`)
- `.env` files in `.gitignore`
- Vaultwarden lives in its own isolated LXC — no shared container
- **Edge components** (firewall, reverse proxies, identity provider) — patched promptly when CVEs land. Highest blast radius if compromised.
- **Hypervisor and backup server** — quarterly review, with security patches applied out-of-cycle when needed.
- **Application LXCs** — rolling updates on a regular schedule, with the sensitive ones (password manager, photos, identity) bumped ahead of less-sensitive ones.
- **Container images** — re-pulled on the same rolling schedule.
## Certificate Management
A version tracker in the private repo records what's running where, so drift is visible at a glance.
| Domain | Provider | Method | Renewal |
|--------|----------|--------|---------|
| `*.lerkolabs.com` | Let's Encrypt via Cloudflare | DNS-01 challenge | Automatic (Caddy) |
## Backups
Caddy handles all cert issuance and renewal automatically. No manual action unless Cloudflare API token expires.
- Hypervisor-level backups to a dedicated backup server on a separate VLAN.
- Conservative retention, with the most recent backups always preserved no matter what gets pruned.
- Backups verified periodically; restores exercised on a non-production host so I find out if something is broken *before* I need it.
- A documented rebuild order means the lab can come back from cold in a few hours, assuming I have physical access to the firewall.
## Update Cadence
## Credentials & rotation
| System | Frequency | Method |
|--------|-----------|--------|
| pfSense | Monthly | Manual — System → Update |
| Proxmox | Monthly | `apt update && apt dist-upgrade` |
| Pi-hole | Monthly | `pihole -up` |
| Docker services | Weekly | `docker compose pull && docker compose up -d` |
| Omada firmware | Quarterly | Omada Controller → Devices |
| AT&T Gateway | Automatic | AT&T pushes updates |
| WireGuard keys | Annually (or on peer change) | Rotate in pfSense VPN config |
- Identity provider passwords: in a password manager, used through it.
- Service-to-service secrets (API tokens, DB passwords): rotated when there's a reason to (component change, suspected exposure), not on a fixed calendar.
- VPN credentials: per-device. Each remote client has its own keypair.
## Known Technical Debt
## Honest limitations
Known gaps tracked privately.
This is a learning environment, not a hardened production estate. A couple of things I'd rather be upfront about:
- **No HA.** One hypervisor, one firewall. The mitigation isn't redundancy, it's a tested rebuild path and conservative backups.
- **One-person ops.** Everything runs as it would in a prod environment with on-call staff, except there's only me. The choice of tooling reflects that — anything that needs constant attention has been swapped out for something simpler.
Neither of those is unmanaged risk. They're scoped accepted risks, reviewed quarterly in the private repo.
## Security contact
Found something concerning about a public-facing endpoint? `admin@lerkolabs.com`. I'll respond.
+85 -74
View File
@@ -1,97 +1,108 @@
# Services
Full registry of what's running, where it lives, and how to reach it. See [README](../README.md) for compute layout and [Network](NETWORK.md) for VLAN/IP context.
Everything I'm running, grouped by what it does. URLs, ports, and which host runs what are operational details — those live in the private repo.
## Status Key
## Identity & access
| Symbol | Meaning |
|--------|---------|
| ✅ | Running, healthy |
| ⚠️ | Running, needs attention |
| 🔴 | Down / broken |
| 🚧 | In progress |
| | Decommissioned |
| Service | What it does |
|---|---|
| Authentik | SSO for everything internal. OIDC where the app supports it, Caddy forward auth where it doesn't. |
| Pi-hole | DNS for the LAN, ad blocking, and the source of truth for internal hostnames. |
| WireGuard | The only way in from outside. All admin work happens through the tunnel. |
## Core Network (VLAN 1000/1010/1020)
## Reverse proxy & TLS
Admin consoles at <service>.lerkolabs.com, VPN-gated.
Two Caddy instances, by design:
| Service | IP | Port | VLAN | Status | Notes |
|---------|----|------|------|--------|-------|
| pfSense | 10.1.0.1 / 10.0.0.1 | 443 | LAN/MGMT | ✅ | Firewall, DHCP, WireGuard VPN |
| Omada Switch | 10.0.0.2 | 443 | MGMT | ✅ | Managed switch, VLAN config |
| AT&T Gateway | 192.168.1.254 | 80 | — | ✅ | IP Passthrough only, WiFi disabled |
| Pi-hole | 10.2.0.11 | 80/53 | 1020 | ✅ | Primary DNS, ad blocking |
| Caddy (infra) | 10.2.0.20 | 80/443 | 1020 | ✅ | Reverse proxy, wildcard SSL via Cloudflare DNS-01 |
| ntfy | 10.2.0.20 | — | 1020 | ✅ | Push notifications (infra LXC) |
| Authentik | 10.2.0.25 | 9000 | 1020 | ✅ | SSO — OIDC + forward auth |
| Proxmox | 10.2.0.10 | 8006 | 1020 | ✅ | Hypervisor |
- **Internal Caddy** — fronts everything internal. Reachable from inside the LAN or via VPN. Does most of the routing.
- **DMZ Caddy** — fronts the small set of things I want public. Lives on its own VLAN with no inbound access to internal services beyond a tight, firewall-enforced allowlist.
## Observability (monitor LXC — 10.2.0.51)
Both use Cloudflare DNS-01 for ACME, which is how internal-only services get valid public certs without ever being exposed to the internet for issuance.
Observability at <service>.lerkolabs.com, VPN-gated.
## Productivity & knowledge
| Service | Notes |
|---------|-------|
| Grafana | Dashboards, alerting |
| Victoria Metrics | Metrics storage |
| Beszel | Container + host monitoring |
| Service | What it replaces |
|---|---|
| Outline | Notion / Confluence |
| Vikunja | Todoist / Asana |
| Hoarder | Pocket / Raindrop |
| Memos | Apple Notes (the quick-capture kind) |
| FreshRSS | Feedly |
| Bytestash | gist / pastebin |
| Filebrowser | Dropbox-style file access |
| Baikal | iCloud calendar/contacts (CalDAV / CardDAV) |
## Productivity Apps (apps LXC — 10.2.0.60)
## Money
All apps served at <service>.lerkolabs.com behind Authentik.
| Service | What it replaces |
|---|---|
| Actual Budget | YNAB / Mint |
| Ghostfolio | Personal Capital |
| Service | Auth | Purpose |
|---------|------|---------|
| Outline | OIDC | Team wiki |
| Vikunja | OIDC | Task management |
| Ghostfolio | Forward auth | Portfolio tracking |
| Hoarder | Forward auth | Bookmark manager |
| Grist | Forward auth | Spreadsheets / data |
| Actual Budget | Forward auth | Personal budgeting |
| FreshRSS | Forward auth | RSS reader |
| Memos | Forward auth | Quick notes |
| Traggo | Forward auth | Time tracking |
| Baikal | Forward auth | CalDAV / CardDAV |
| Glance | Forward auth | Homepage dashboard |
| Filebrowser | Forward auth | File management |
| Bytestash | Forward auth | Snippet storage |
## Operations & day-to-day
Shared infrastructure in apps LXC: single Postgres instance (multi-DB) + Redis. See [D004](DECISIONS.md#d004--shared-postgres--redis-in-apps-lxc).
| Service | What it does |
|---|---|
| Grist | Lightweight relational tracking — anything that wants to be in a spreadsheet but shouldn't be |
| Glance | Personal landing page / dashboard |
| Traggo | Time tracking |
## Secrets (vault LXC — 10.2.0.21)
## Media
Served at <service>.lerkolabs.com, VPN-gated.
| Service | What it does |
|---|---|
| Plex | Media library (legacy clients) |
| Jellyfin | Media library (primary, open source) |
| *arr stack | Library automation |
| qBittorrent | Downloads |
| Immich | Self-hosted Google Photos replacement |
| Service | Notes |
|---------|-------|
| Vaultwarden | Isolated LXC — not shared with apps |
## Home / IoT
## Media (servarr VM)
| Service | What it does |
|---|---|
| Home Assistant OS | Home automation hub |
| Service | Purpose |
|---------|---------|
| Plex + Jellyfin | Media streaming |
| Sonarr / Radarr / Lidarr | Automated media management |
| Prowlarr + Bazarr | Indexer aggregation + subtitles |
| qBittorrent (via Gluetun) | Downloads — VPN-gated |
| Calibre-Web Automated | Book library with auto-ingest |
| Kavita | E-reader |
## Secrets
## DMZ (VLAN 1099 — 10.99.0.0/24)
| Service | What it does |
|---|---|
| Vaultwarden | Bitwarden-compatible password manager. **Planned, not deployed yet.** |
| Service | URL | Status | Notes |
|---------|-----|--------|-------|
| Caddy (DMZ) | — | ✅ | Public reverse proxy |
| Gitea | https://gitea.lerkolabs.com | ✅ | Public Git |
| Portfolio | https://lerkolabs.com | ✅ | Personal site |
## Bots & automation
## Access Matrix
| Service | What it does |
|---|---|
| Vocard | Discord music bot |
| MonitorRSS | RSS-to-Discord notifications |
| ntfy | Push notifications for ops alerts |
| Service | LAN | Homelab | Guest | IoT | WFH | VPN |
|---------|-----|---------|-------|-----|-----|-----|
| pfSense Web GUI | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Pi-hole Admin | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| All *.lerkolabs.com | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Proxmox | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Internet | ✅ | limited | ✅ | ✅ | ✅ | optional |
## Monitoring
| Service | What it does |
|---|---|
| Victoria Metrics | Time-series store |
| Grafana | Dashboards |
| Beszel | Lightweight host metrics |
| Uptime Kuma | Synthetic uptime checks |
## Public services
A small, intentional set of things that are reachable from the open internet. They all sit behind the DMZ reverse proxy on a VLAN with no inbound access to internal subnets.
| Service | Why it's public |
|---|---|
| Portfolio | It's a portfolio. |
| Self-hosted Git | Where you're reading this. |
| SSO endpoint | Has to be reachable for an OIDC flow on one specific public-facing service (the Discord bot dashboard). It's the only internal-VLAN backend the public proxy is allowed to talk to, and the firewall enforces that — not just the proxy config. |
| One Authentik-gated app | The Discord bot dashboard. Public so I can hit it from outside the LAN; gated by Authentik forward auth before anything responds. |
## Who can access what
Three audiences, three levels:
- **Internet, anonymous** — sees only the small public set above.
- **Internet, signed into Authentik** — same as above, plus access to the Authentik-gated public services.
- **Connected via WireGuard** — gets everything: internal apps and admin surfaces (hypervisor, firewall, backup server, network controller, monitoring). This is the only way to reach any admin surface.
The WFH and IoT VLANs are deliberately *outside* this access model. Those are for me-as-a-user (work laptop, smart devices), not me-as-an-operator. They never see the internal service plane.