From 6730781dd0f6c657f0e272ca566efc6f76c2fae0 Mon Sep 17 00:00:00 2001 From: lerko Date: Mon, 20 Apr 2026 20:49:48 -0400 Subject: [PATCH] chore: initial public release --- CHANGELOG.md | 17 +++ README.md | 55 ++++++++ REBUILD.md | 60 +++++++++ assets/.gitkeep | 0 assets/auth-flow.md | 15 +++ assets/dns-chain.md | 9 ++ assets/network-topology.md | 15 +++ docs/DECISIONS.md | 87 ++++++++++++ docs/INVENTORY.md | 69 ++++++++++ docs/NETWORK.md | 130 ++++++++++++++++++ docs/RUNBOOKS.md | 3 + docs/SECURITY.md | 54 ++++++++ docs/SERVICES.md | 97 ++++++++++++++ setup/.gitkeep | 0 setup/apps-lxc.md | 263 +++++++++++++++++++++++++++++++++++++ setup/authentik.md | 227 ++++++++++++++++++++++++++++++++ setup/caddy.md | 224 +++++++++++++++++++++++++++++++ setup/monitor-lxc.md | 157 ++++++++++++++++++++++ setup/pfsense-vlans.md | 116 ++++++++++++++++ setup/pihole.md | 96 ++++++++++++++ setup/servarr.md | 143 ++++++++++++++++++++ setup/vaultwarden.md | 157 ++++++++++++++++++++++ setup/wireguard.md | 130 ++++++++++++++++++ 23 files changed, 2124 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 REBUILD.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/DECISIONS.md create mode 100644 docs/INVENTORY.md create mode 100644 docs/NETWORK.md create mode 100644 docs/RUNBOOKS.md create mode 100644 docs/SECURITY.md create mode 100644 docs/SERVICES.md create mode 100644 setup/.gitkeep create mode 100644 setup/apps-lxc.md create mode 100644 setup/authentik.md create mode 100644 setup/caddy.md create mode 100644 setup/monitor-lxc.md create mode 100644 setup/pfsense-vlans.md create mode 100644 setup/pihole.md create mode 100644 setup/servarr.md create mode 100644 setup/vaultwarden.md create mode 100644 setup/wireguard.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7a0340f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this homelab, in reverse chronological order. + +## [2026.04.3] — 2026-04-20 + +### Changed +- Hardened public tree: generalized hardware SKUs, removed pinned software versions, timezone, admin URLs, DMZ service mapping, and technical-debt enumeration. +- Public repository history reset — this is the genesis commit of the hardened tree. +- Retired `git subtree` publishing in favor of a direct publish workflow. + +## [2026.04.1] — 2026-04-17 + +### Added +- Initial repo structure with public/private split +- Public documentation: services, network, decisions, security, inventory, runbooks +- Rebuild sequence (8-phase ordered recovery) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe3ee2d --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# homelab + +Personal homelab running 24/7 on production-grade hardware. Domain: `lerkolabs.com`. Single Proxmox host running 9 LXC containers + 2 VMs across 8 isolated VLANs with 20+ self-hosted services. + +## At a Glance + +| Component | Technology | +|-----------|-----------| +| Hypervisor | Proxmox VE | +| Firewall | pfSense (Intel N100) | +| Switching | TP-Link Omada (managed VLANs) | +| Reverse Proxy | Caddy + Cloudflare DNS-01 | +| Auth | Authentik SSO (OIDC + forward auth) | +| DNS | Pi-hole → pfSense Unbound → Cloudflare | +| VPN | WireGuard, UDP 51820 | +| Monitoring | Victoria Metrics + Grafana + Beszel | +| Backups | Proxmox Backup Server (PBS) | + +## Compute Layout + +| Container | IP | Cores | RAM | What Runs | +|-----------|-----|-------|-----|-----------| +| `pihole` | 10.2.0.11 | 1 | 512MB | Pi-hole DNS + ad blocking | +| `auth` | 10.2.0.25 | 1 | 512MB | Authentik SSO | +| `infra` | 10.2.0.20 | 2 | 1GB | Caddy reverse proxy, ntfy | +| `monitor` | 10.2.0.51 | 4 | 4GB | Victoria Metrics, Grafana, Beszel | +| `apps` | 10.2.0.60 | 4 | 6GB | 15+ productivity apps (Docker Compose) | +| `vault` | 10.2.0.X | 1 | 256MB | Vaultwarden (isolated) | +| `servarr` (VM) | — | 4 | 8GB | Plex, Jellyfin, *arr stack, qBittorrent | +| `haos` (VM) | — | 2 | 4GB | Home Assistant OS | + +## DMZ (Public-Facing) + +| Container | IP | Service | +|-----------|-----|---------| +| `caddy-dmz` | 10.99.0.20 | Public reverse proxy | +| `gitea` | 10.99.0.22 | gitea.lerkolabs.com | +| `portfolio` | 10.99.0.23 | lerkolabs.com | + +## Key Principles + +- All services require Authentik authentication — no anonymous access +- No management ports exposed to internet — all admin access via WireGuard first +- Caddy handles TLS termination; internal services run plain HTTP +- Secrets never committed — all referenced by Vaultwarden entry name + +## Navigation + +- [Services](docs/SERVICES.md) — full service registry with URLs and access matrix +- [Network](docs/NETWORK.md) — VLANs, firewall policy, DNS architecture, physical topology +- [Decisions](docs/DECISIONS.md) — architecture decision records (D001–D010) +- [Security](docs/SECURITY.md) — security posture, auth layers, update cadence, known debt +- [Inventory](docs/INVENTORY.md) — hardware inventory +- [Rebuild](REBUILD.md) — disaster recovery sequence (8 phases) +- [Setup guides](setup/) — per-service installation and configuration diff --git a/REBUILD.md b/REBUILD.md new file mode 100644 index 0000000..161b1f1 --- /dev/null +++ b/REBUILD.md @@ -0,0 +1,60 @@ +# Rebuild + +Ordered recovery sequence from scratch or after catastrophic failure. **Nothing works until the thing before it works.** For step-by-step setup, see individual service [setup guides](setup/). + +## Phase 1 — Network Foundation + +1. **pfSense** — restore `config.xml`; verify WAN gets public IP (IP Passthrough active on BGW320); verify all VLAN interfaces up + DHCP serving; verify firewall rules loaded +2. **Omada Switch** — restore controller backup; verify port VLANs match [Network](docs/NETWORK.md) topology; verify trunk port carrying all VLANs tagged +3. **Access points** — auto-adopt into Omada Controller; verify SSIDs on correct VLANs + +**Gate:** LAN device gets IP and reaches internet. + +## Phase 2 — DNS + +4. **Pi-hole LXC** — restore from PBS snapshot (or fresh deploy); restore Teleporter backup; verify all local DNS records → 10.2.0.20 (Caddy); verify ad blocking active +5. **pfSense DNS Resolver** — auto-configured from `config.xml`; verify Pi-hole is upstream for all VLANs + +**Gate:** `nslookup outline.lerkolabs.com` returns 10.2.0.20 from LAN. + +## Phase 3 — Reverse Proxy + TLS + +6. **Infra LXC (Caddy)** — restore from PBS (or fresh deploy); verify Cloudflare API token valid; start Caddy — certs auto-issue (allow 2–3 min); add Pi-hole DNS record: `*.lerkolabs.com → 10.2.0.20` + +**Gate:** `curl -I https://pihole.lerkolabs.com` returns HTTP/2 200. + +## Phase 4 — Auth + +7. **Auth LXC (Authentik)** — restore from PBS; verify admin accessible at `https://auth.lerkolabs.com`; verify OIDC apps configured (Outline, Gitea, Vikunja); verify forward auth flows + +## Phase 5 — Secrets + +8. **Vault LXC (Vaultwarden)** — restore from PBS; verify accessible at `https://vault.lerkolabs.com`; confirm all credentials accessible before proceeding + +## Phase 6 — Core Services + +9. **Apps LXC** — restore from PBS (or fresh deploy); start shared Postgres + Redis first; bring up services one by one: Outline → Gitea → Vikunja → Ghostfolio → Hoarder → Grist → Glance → Actual → FreshRSS → Memos → Traggo → Baikal → Filebrowser → Bytestash +10. **Monitor LXC** — restore from PBS; verify Grafana dashboards loading; verify Beszel agents reporting from all LXCs; verify Victoria Metrics receiving metrics + +## Phase 7 — VMs + +11. **Servarr VM** — restore from PBS; verify Plex/Jellyfin accessible; verify arr stack healthy; verify Gluetun VPN tunnel active for qBittorrent +12. **Home Assistant OS VM** — restore from PBS (or HAOS backup); verify integrations reconnect + +## Phase 8 — VPN + +13. **WireGuard** — restored with `config.xml`; verify peer configs valid; test from cellular; if keys rotated, distribute new configs + +## Post-Rebuild Checklist + +- [ ] Internet works from LAN devices +- [ ] DNS resolves internal and external names +- [ ] All `*.lerkolabs.com` reachable via HTTPS +- [ ] Authentik SSO working (log into Outline via Authentik) +- [ ] WireGuard connects from external network +- [ ] Vaultwarden accessible and credentials intact +- [ ] All Docker containers healthy in Beszel +- [ ] PBS scheduled backups running +- [ ] Pi-hole blocking ads +- [ ] Home Assistant automations running +- [ ] Media stack healthy (Plex/Jellyfin playback works) 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..acd7457 --- /dev/null +++ b/assets/auth-flow.md @@ -0,0 +1,15 @@ +# Authentication Flow + +```mermaid +sequenceDiagram + User->>Caddy: HTTPS request + Caddy->>Authentik: Forward auth check + Authentik-->>Caddy: 401 if unauthenticated + Caddy-->>User: Redirect to auth.lerkolabs.com + User->>Authentik: Login (OIDC or forward auth) + Authentik-->>User: Session cookie + User->>Caddy: HTTPS request + cookie + Caddy->>Authentik: Forward auth check + Authentik-->>Caddy: 200 OK + Caddy->>Service: Proxy request +``` diff --git a/assets/dns-chain.md b/assets/dns-chain.md new file mode 100644 index 0000000..db04ef1 --- /dev/null +++ b/assets/dns-chain.md @@ -0,0 +1,9 @@ +# DNS Resolution Chain + +```mermaid +graph LR + D[Device] --> PH[Pi-hole\n10.2.0.11] + PH --> UB[pfSense Unbound\n10.x.0.1] + UB --> CF[Cloudflare\n1.1.1.1] + PH -- "*.lerkolabs.com" --> CADDY[Caddy\n10.2.0.20] +``` diff --git a/assets/network-topology.md b/assets/network-topology.md new file mode 100644 index 0000000..74b8ce1 --- /dev/null +++ b/assets/network-topology.md @@ -0,0 +1,15 @@ +# Network Topology + +```mermaid +graph TD + ONT[AT&T Fiber ONT] --> BGW[BGW320 IP Passthrough] + BGW --> PF[pfSense N100] + PF --> SW[Omada Switch] + SW --> MGMT[VLAN 1000 MGMT\n10.0.0.0/24] + SW --> LAN[VLAN 1010 LAN\n10.1.0.0/24] + SW --> HL[VLAN 1020 Homelab\n10.2.0.0/24] + SW --> GUEST[VLAN 1030 Guests\n10.3.0.0/24] + SW --> IOT[VLAN 1040 IoT\n10.4.0.0/24] + SW --> WFH[VLAN 1050 WFH\n10.5.0.0/24] + SW --> DMZ[VLAN 1 DMZ\n10.99.0.0/24] +``` diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..cce0e61 --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,87 @@ +# 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 1–2ms 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 2–3Gbps routing, 600–900Mbps WireGuard. +**Why:** Right-sized for 1Gbps fiber with headroom. Raspberry Pi insufficient for 1Gbps + VPN. Full rack server overkill power draw. +**Status:** decided diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md new file mode 100644 index 0000000..01e37ce --- /dev/null +++ b/docs/INVENTORY.md @@ -0,0 +1,69 @@ +# 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 2–3Gbps 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 | 2–3Gbps | +| 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 | diff --git a/docs/NETWORK.md b/docs/NETWORK.md new file mode 100644 index 0000000..5f0f7ac --- /dev/null +++ b/docs/NETWORK.md @@ -0,0 +1,130 @@ +# 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. + +## VLAN Map + +| VLAN ID | Name | Subnet | Gateway | DHCP Range | DNS | +|---------|------|--------|---------|------------|-----| +| 1000 | MGMT | 10.0.0.0/24 | 10.0.0.1 | 10.0.0.100–150 | pfSense only | +| 1010 | LAN | 10.1.0.0/24 | 10.1.0.1 | 10.1.0.100–200 | Pi-hole → pfSense | +| 1020 | Homelab | 10.2.0.0/24 | 10.2.0.1 | 10.2.0.100–200 | Pi-hole → pfSense | +| 1030 | Guests | 10.3.0.0/24 | 10.3.0.1 | 10.3.0.100–250 | Pi-hole → pfSense | +| 1040 | IoT | 10.4.0.0/24 | 10.4.0.1 | 10.4.0.100–250 | Pi-hole → pfSense | +| 1050 | WFH | 10.5.0.0/24 | 10.5.0.1 | 10.5.0.100–200 | 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 | + +## Firewall Policy + +Default: **deny all inter-VLAN unless explicitly allowed.** + +| 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 | + +## Static IP Reservations + +### VLAN 1000 — MGMT + +| 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 | + +### VLAN 1010 — LAN + +| IP | Device | +|----|--------| +| 10.1.0.1 | pfSense LAN gateway | + +### VLAN 1020 — Homelab + +| 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 | + +### VLAN 1099 — DMZ + +| 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 | + +## IP Block Allocation (VLAN 1020) + +| Block | Purpose | +|-------|---------| +| 10.2.0.1–9 | Infrastructure (gateway, pfSense interfaces) | +| 10.2.0.10–19 | Network critical (Proxmox, Pi-hole) | +| 10.2.0.20–29 | Auth / Proxy (Caddy, Authentik, Vaultwarden) | +| 10.2.0.30–39 | Observability | +| 10.2.0.40–49 | Dev tools | +| 10.2.0.50–59 | Data | +| 10.2.0.60–69 | Apps | +| 10.2.0.70–79 | Files | +| 10.2.0.80–99 | Media | +| 10.2.0.100+ | DHCP pool (dynamic) | + +## DNS Architecture + +``` +Device → Pi-hole (10.2.0.11) + ↓ + pfSense Unbound (10.x.0.1) — local records + DHCP hostnames + ↓ + Cloudflare 1.1.1.1 (upstream) +``` + +- 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 + +## Physical Topology + +``` +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 +``` + +## 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). diff --git a/docs/RUNBOOKS.md b/docs/RUNBOOKS.md new file mode 100644 index 0000000..dc0254f --- /dev/null +++ b/docs/RUNBOOKS.md @@ -0,0 +1,3 @@ +# RUNBOOKS + +_stub_ diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..af294a9 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,54 @@ +# Security + +Security posture — what's exposed, how auth works, update cadence, known debt. See [Network](NETWORK.md) for VLAN isolation details. + +## Internet-Exposed Ports + +| Port | Protocol | Destination | Purpose | +|------|----------|-------------|---------| +| 51820 | UDP | pfSense WAN | WireGuard VPN | + +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. + +## Authentication Layers + +| 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 | + +No service is accessible anonymously. Guests and IoT have zero access to any internal service. + +## Secrets Policy + +- 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 + +## Certificate Management + +| Domain | Provider | Method | Renewal | +|--------|----------|--------|---------| +| `*.lerkolabs.com` | Let's Encrypt via Cloudflare | DNS-01 challenge | Automatic (Caddy) | + +Caddy handles all cert issuance and renewal automatically. No manual action unless Cloudflare API token expires. + +## Update Cadence + +| 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 | + +## Known Technical Debt + +Known gaps tracked privately. diff --git a/docs/SERVICES.md b/docs/SERVICES.md new file mode 100644 index 0000000..258215b --- /dev/null +++ b/docs/SERVICES.md @@ -0,0 +1,97 @@ +# 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. + +## Status Key + +| Symbol | Meaning | +|--------|---------| +| ✅ | Running, healthy | +| ⚠️ | Running, needs attention | +| 🔴 | Down / broken | +| 🚧 | In progress | +| ➖ | Decommissioned | + +## Core Network (VLAN 1000/1010/1020) + +Admin consoles at .lerkolabs.com, VPN-gated. + +| 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 | + +## Observability (monitor LXC — 10.2.0.51) + +Observability at .lerkolabs.com, VPN-gated. + +| Service | Notes | +|---------|-------| +| Grafana | Dashboards, alerting | +| Victoria Metrics | Metrics storage | +| Beszel | Container + host monitoring | + +## Productivity Apps (apps LXC — 10.2.0.60) + +All apps served at .lerkolabs.com behind Authentik. + +| 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 | + +Shared infrastructure in apps LXC: single Postgres instance (multi-DB) + Redis. See [D004](DECISIONS.md#d004--shared-postgres--redis-in-apps-lxc). + +## Secrets (vault LXC — 10.2.0.21) + +Served at .lerkolabs.com, VPN-gated. + +| Service | Notes | +|---------|-------| +| Vaultwarden | Isolated LXC — not shared with apps | + +## Media (servarr VM) + +| 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 | + +## DMZ (VLAN 1099 — 10.99.0.0/24) + +| Service | URL | Status | Notes | +|---------|-----|--------|-------| +| Caddy (DMZ) | — | ✅ | Public reverse proxy | +| Gitea | https://gitea.lerkolabs.com | ✅ | Public Git | +| Portfolio | https://lerkolabs.com | ✅ | Personal site | + +## Access Matrix + +| Service | LAN | Homelab | Guest | IoT | WFH | VPN | +|---------|-----|---------|-------|-----|-----|-----| +| pfSense Web GUI | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| Pi-hole Admin | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| All *.lerkolabs.com | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| Proxmox | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| Internet | ✅ | limited | ✅ | ✅ | ✅ | optional | diff --git a/setup/.gitkeep b/setup/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/setup/apps-lxc.md b/setup/apps-lxc.md new file mode 100644 index 0000000..6e54e91 --- /dev/null +++ b/setup/apps-lxc.md @@ -0,0 +1,263 @@ +# Apps LXC Setup + +## Overview + +The `apps` LXC (10.2.0.60) in VLAN 1020 runs 15+ productivity apps via Docker Compose. All services run behind Authentik SSO (OIDC or forward auth). Shared infrastructure: single Postgres instance + single Redis instance, both local to the LXC. All services use `network_mode: host` to reach shared Postgres/Redis on localhost. + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | apps | +| IP | 10.2.0.60/24 | +| Gateway | 10.2.0.1 | +| DNS | 10.2.0.11 | +| Cores | 4 | +| RAM | 6GB | +| Disk | 80GB | +| Template | debian-12-standard | +| Nesting | ✓ (required for Docker) | +| Unprivileged | ✓ | + +## Services + +| Service | Port | Domain | DB | Auth | +|---------|------|--------|----|------| +| Outline | 3000 | outline.lerkolabs.com | Postgres | OIDC | +| Gitea | 3001 | gitea.lerkolabs.com | Postgres | OIDC | +| Vikunja | 3456 | tasks.lerkolabs.com | Postgres | OIDC | +| Ghostfolio | 3333 | finance.lerkolabs.com | Postgres | Forward auth | +| Hoarder | 3002 | hoarder.lerkolabs.com | Postgres | Forward auth | +| Grist | 8484 | grist.lerkolabs.com | SQLite | Forward auth | +| Glance | 8080 | glance.lerkolabs.com | — | Forward auth | +| Actual Budget | 5006 | budget.lerkolabs.com | File | Forward auth | +| FreshRSS | 8081 | rss.lerkolabs.com | SQLite | Forward auth | +| Memos | 5230 | memos.lerkolabs.com | SQLite | Forward auth | +| Traggo | 3030 | time.lerkolabs.com | SQLite | Forward auth | +| Baikal | 8082 | dav.lerkolabs.com | SQLite | Forward auth | +| Filebrowser | 8083 | files.lerkolabs.com | SQLite | Forward auth | +| Bytestash | 8084 | — | SQLite | Forward auth | + +## Prerequisites + +- LXC created with nesting enabled before first start +- Authentik OIDC providers created for Outline, Gitea, Vikunja before starting those services +- Caddy Caddyfile updated with all service blocks (see Phase: Caddy) + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl wget git nano ufw +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +``` + +## Directory Structure + +```bash +mkdir -p /opt/docker/apps/{shared,outline,gitea,vikunja,ghostfolio,hoarder,grist,glance,actual,freshrss,memos,traggo,baikal,filebrowser,bytestash} +``` + +## Shared Infrastructure (Postgres + Redis) + +Start this first, before anything else. + +### Shared .env + +```bash +# /opt/docker/apps/.env +PG_ROOT_PASSWORD= # openssl rand -base64 32 +REDIS_PASSWORD= # openssl rand -base64 24 +PG_PASS_OUTLINE= # openssl rand -base64 24 +PG_PASS_GITEA= # openssl rand -base64 24 +PG_PASS_VIKUNJA= # openssl rand -base64 24 +PG_PASS_GHOSTFOLIO= # openssl rand -base64 24 +PG_PASS_HOARDER= # openssl rand -base64 24 +PG_PASS_GRIST= # openssl rand -base64 24 +``` + +```bash +chmod 600 /opt/docker/apps/.env +``` + +### shared/docker-compose.yml + +```yaml +services: + postgres: + image: postgres: + container_name: apps-postgres + restart: unless-stopped + env_file: /opt/docker/apps/.env + environment: + POSTGRES_PASSWORD: ${PG_ROOT_PASSWORD} + POSTGRES_USER: postgres + volumes: + - ./pgdata:/var/lib/postgresql/data + - ./init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + ports: + - "127.0.0.1:5432:5432" + networks: + - apps-net + + redis: + image: redis: + container_name: apps-redis + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1 --loglevel warning + env_file: /opt/docker/apps/.env + volumes: + - ./redisdata:/data + ports: + - "127.0.0.1:6379:6379" + networks: + - apps-net + +networks: + apps-net: + name: apps-net + driver: bridge +``` + +### Database init script + +```bash +mkdir -p /opt/docker/apps/shared/init +``` + +`/opt/docker/apps/shared/init/01-create-databases.sql`: + +```sql +CREATE USER outline WITH PASSWORD 'PG_PASS_OUTLINE_VALUE'; +CREATE DATABASE outline OWNER outline; + +CREATE USER gitea WITH PASSWORD 'PG_PASS_GITEA_VALUE'; +CREATE DATABASE gitea OWNER gitea; + +CREATE USER vikunja WITH PASSWORD 'PG_PASS_VIKUNJA_VALUE'; +CREATE DATABASE vikunja OWNER vikunja; + +CREATE USER ghostfolio WITH PASSWORD 'PG_PASS_GHOSTFOLIO_VALUE'; +CREATE DATABASE ghostfolio OWNER ghostfolio; + +CREATE USER hoarder WITH PASSWORD 'PG_PASS_HOARDER_VALUE'; +CREATE DATABASE hoarder OWNER hoarder; + +CREATE USER grist WITH PASSWORD 'PG_PASS_GRIST_VALUE'; +CREATE DATABASE grist OWNER grist; +``` + +Replace each `_VALUE` with the actual password from your .env. This script runs once on first `docker compose up`. + +### Start shared infrastructure + +```bash +cd /opt/docker/apps/shared +docker compose --env-file /opt/docker/apps/.env up -d +docker compose logs -f # wait for "ready to accept connections" +``` + +### Verify + +```bash +docker exec apps-postgres pg_isready -U postgres +docker exec apps-postgres psql -U postgres -c "\l" +# Should show: outline, gitea, vikunja, ghostfolio, hoarder, grist +``` + +## Startup Order + +```bash +# 1. Shared infrastructure always first +cd /opt/docker/apps/shared && docker compose up -d +sleep 15 # give postgres time on first run + +# 2. Run Outline migrations before starting Outline +cd /opt/docker/apps/outline && docker compose run --rm outline-migrate +docker compose up -d outline + +# 3. Everything else in parallel +for svc in gitea vikunja ghostfolio hoarder grist glance actual freshrss memos traggo baikal filebrowser bytestash; do + cd /opt/docker/apps/$svc && docker compose up -d +done +``` + +## Caddy Configuration + +Add to Caddyfile on the infra LXC (10.2.0.20). Services with native OIDC don't need forward auth; others do. + +```caddyfile +# OIDC-native (no forward auth needed) +outline.lerkolabs.com { + reverse_proxy 10.2.0.60:3000 +} +gitea.lerkolabs.com { + reverse_proxy 10.2.0.60:3001 +} +tasks.lerkolabs.com { + reverse_proxy 10.2.0.60:3456 +} + +# Forward auth +finance.lerkolabs.com { + import authentik_forward_auth + reverse_proxy 10.2.0.60:3333 +} +# ... repeat pattern for remaining services +``` + +## Pi-hole DNS Records + +All records point to 10.2.0.20 (Caddy), not 10.2.0.60 directly: + +``` +outline.lerkolabs.com → 10.2.0.20 +gitea.lerkolabs.com → 10.2.0.20 +tasks.lerkolabs.com → 10.2.0.20 +finance.lerkolabs.com → 10.2.0.20 +hoarder.lerkolabs.com → 10.2.0.20 +grist.lerkolabs.com → 10.2.0.20 +glance.lerkolabs.com → 10.2.0.20 +budget.lerkolabs.com → 10.2.0.20 +rss.lerkolabs.com → 10.2.0.20 +memos.lerkolabs.com → 10.2.0.20 +time.lerkolabs.com → 10.2.0.20 +dav.lerkolabs.com → 10.2.0.20 +files.lerkolabs.com → 10.2.0.20 +``` + +## Verification + +```bash +# All containers running +docker ps --format "table {{.Names}}\t{{.Status}}" | sort + +# Outline health +curl -s http://localhost:3000/api/health + +# From LAN — check Authentik gate works +curl -I https://outline.lerkolabs.com +``` + +## Useful Commands + +```bash +# Logs for a service +docker logs -f outline + +# Postgres: connect to a database +docker exec -it apps-postgres psql -U postgres -d outline + +# Postgres: backup +docker exec apps-postgres pg_dump -U postgres outline > /opt/backups/outline-$(date +%Y%m%d).sql + +# Disk usage by service +du -sh /opt/docker/apps/*/ +``` diff --git a/setup/authentik.md b/setup/authentik.md new file mode 100644 index 0000000..6e85509 --- /dev/null +++ b/setup/authentik.md @@ -0,0 +1,227 @@ +# Authentik Setup + +## Overview + +Authentik is the centralized identity provider for the entire homelab. It runs in the `auth` LXC (10.2.0.25) in VLAN 1020. It provides two auth mechanisms: + +1. **Forward auth** — Caddy asks Authentik "is this person logged in?" before proxying any request. Services that don't support SSO natively get a login wall this way. +2. **OIDC provider** — Services that support OAuth2/OIDC (Outline, Gitea, Vikunja, Grafana, Proxmox) get true single sign-on. + +Stack: Postgres + Authentik server + Authentik worker (Redis removed as of 2025.10). + +## Prerequisites + +- LXC at 10.2.0.25 with nesting enabled (Docker requires it) +- Caddy already deployed at 10.2.0.20 +- Pi-hole DNS record: `auth.lerkolabs.com → 10.2.0.20` + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | auth | +| IP | 10.2.0.25/24 | +| Gateway | 10.2.0.1 | +| Cores | 2 | +| RAM | 2GB | +| Disk | 10GB | +| Template | debian-12-standard | +| Nesting | ✓ | + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl nano +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +mkdir -p /opt/docker/authentik/{data,certs,custom-templates} +``` + +## Configuration + +### Generate secrets and create .env + +```bash +echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> /opt/docker/authentik/.env +echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> /opt/docker/authentik/.env +``` + +Add remaining config: + +```bash +# PostgreSQL +PG_USER=authentik +PG_DB=authentik + +# Pin to current version +AUTHENTIK_TAG= + +AUTHENTIK_LOG_LEVEL=info +``` + +```bash +chmod 600 /opt/docker/authentik/.env +``` + +### docker-compose.yml + +```yaml +services: + postgresql: + image: docker.io/library/postgres: + container_name: authentik-db + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d ${PG_DB} -U ${PG_USER}"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 5s + volumes: + - ./postgres:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${PG_PASS} + POSTGRES_USER: ${PG_USER} + POSTGRES_DB: ${PG_DB} + + server: + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG} + container_name: authentik-server + restart: unless-stopped + command: server + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB} + AUTHENTIK_LOG_LEVEL: ${AUTHENTIK_LOG_LEVEL:-info} + volumes: + - ./data:/data + - ./custom-templates:/templates + ports: + - "9000:9000" + - "9443:9443" + depends_on: + postgresql: + condition: service_healthy + + worker: + image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG} + container_name: authentik-worker + restart: unless-stopped + command: worker + environment: + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB} + AUTHENTIK_LOG_LEVEL: ${AUTHENTIK_LOG_LEVEL:-info} + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data + - ./certs:/certs + - ./custom-templates:/templates + depends_on: + postgresql: + condition: service_healthy +``` + +## Start Authentik + +```bash +cd /opt/docker/authentik +docker compose up -d +docker logs -f authentik-server +# Wait for: "Everything is ready" +``` + +## Initial Admin Setup + +Navigate to: `http://10.2.0.25:9000/if/flow/initial-setup/` (include trailing slash) + +Set admin password for the `akadmin` account. Save in Vaultwarden. + +## Caddy Integration + +Add to Caddyfile on the infra LXC (see [caddy.md](caddy.md)): + +```caddyfile +# Forward auth snippet +(authentik_forward_auth) { + forward_auth 10.2.0.25:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \ + X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks \ + X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \ + X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } +} + +auth.lerkolabs.com { + reverse_proxy 10.2.0.25:9000 +} +``` + +## Configure Outpost + +In Authentik admin: Applications → Outposts → authentik Embedded Outpost → Edit + +Set both: +- `authentik Host`: `https://auth.lerkolabs.com` +- `authentik Host (browser)`: `https://auth.lerkolabs.com` + +## Adding Applications + +### Forward auth pattern (services without native OIDC) + +1. Applications → Providers → Create → Proxy Provider + - Mode: Forward auth (single application) + - External host: `https://.lerkolabs.com` +2. Applications → Applications → Create + - Assign provider, set slug and launch URL +3. Outposts → Embedded Outpost → Edit → add application to Selected Applications + +### OIDC pattern (Outline, Gitea, Vikunja) + +1. Applications → Providers → Create → OAuth2/OpenID Provider + - Client type: Confidential + - Redirect URI: service-specific (see table below) + - Scopes: openid, email, profile +2. Note the Client ID and Client Secret — configure in the service's .env + +| Service | Redirect URI | +|---------|-------------| +| Outline | `https://outline.lerkolabs.com/auth/oidc.callback` | +| Gitea | `https://gitea.lerkolabs.com/user/oauth2/authentik/callback` | +| Vikunja | `https://tasks.lerkolabs.com/auth/openid/authentik` | +| Grafana | `https://grafana.lerkolabs.com/login/generic_oauth` | + +OIDC discovery URL pattern: `https://auth.lerkolabs.com/application/o//.well-known/openid-configuration` + +## Verification + +```bash +# All containers healthy +docker ps +# authentik-db Up X minutes (healthy) +# authentik-server Up X minutes +# authentik-worker Up X minutes + +# Accessible via Caddy +curl -I https://auth.lerkolabs.com +# Expected: HTTP/2 302 (redirect to login) +``` + +## Updates + +```bash +# Edit .env, bump AUTHENTIK_TAG to new version +docker compose pull +docker compose up -d +``` diff --git a/setup/caddy.md b/setup/caddy.md new file mode 100644 index 0000000..1ae3e51 --- /dev/null +++ b/setup/caddy.md @@ -0,0 +1,224 @@ +# Caddy (infra LXC) Setup + +## Overview + +The `infra` LXC (10.2.0.20) in VLAN 1020 runs Caddy as the internal reverse proxy. It handles TLS termination for all `*.lerkolabs.com` services using wildcard certs via Cloudflare DNS-01 challenge. Also hosts ntfy (push notifications) and Uptime Kuma (service monitoring) as co-located services. + +| Service | Port | Domain | +|---------|------|--------| +| Caddy | 80/443 | reverse proxy — no direct domain | +| ntfy | 8090 | ntfy.lerkolabs.com | +| Uptime Kuma | 3001 | uptime.lerkolabs.com | + +## Prerequisites + +- LXC created at 10.2.0.20 in VLAN 1020 +- Cloudflare API token with Zone → DNS → Edit permissions for lerkolabs.com (stored in Vaultwarden) +- Pi-hole DNS record: `*.lerkolabs.com → 10.2.0.20` + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | infra | +| IP | 10.2.0.20/24 | +| Gateway | 10.2.0.1 | +| Cores | 2 | +| RAM | 1GB | +| Template | debian-12-standard | +| Nesting | ✓ (required for Docker) | + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl nano ufw +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +``` + +## Directory Structure + +``` +/opt/docker/ +├── caddy/ +│ ├── Caddyfile +│ ├── docker-compose.yml +│ ├── Dockerfile +│ ├── .env +│ ├── data/ +│ ├── config/ +│ └── logs/ +└── infra/ + ├── uptimekuma/ + │ ├── docker-compose.yml + │ └── data/ + └── ntfy/ + ├── docker-compose.yml + ├── server.yml + └── data/ +``` + +## Caddy Deployment + +### Dockerfile (custom build with Cloudflare DNS plugin) + +```dockerfile +FROM caddy: AS builder +RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare + +FROM caddy: +COPY --from=builder /usr/bin/caddy /usr/bin/caddy +``` + +### .env + +```bash +CLOUDFLARE_API_TOKEN= +``` + +```bash +chmod 600 /opt/docker/caddy/.env +``` + +### docker-compose.yml + +```yaml +services: + caddy: + build: . + container_name: caddy + restart: unless-stopped + network_mode: host + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./data:/data + - ./config:/config + - ./logs:/logs + env_file: + - .env +``` + +### Caddyfile structure + +```caddyfile +{ + email + acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} +} + +# Forward auth snippet — reuse in every protected service block +(authentik_forward_auth) { + forward_auth 10.2.0.25:9000 { + uri /outpost.goauthentik.io/auth/caddy + copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \ + X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks \ + X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \ + X-Authentik-Meta-App X-Authentik-Meta-Version + trusted_proxies private_ranges + } +} + +# Authentik +auth.lerkolabs.com { + reverse_proxy 10.2.0.25:9000 +} + +# Pi-hole (forward auth) +pihole.lerkolabs.com { + import authentik_forward_auth + reverse_proxy 10.2.0.11:80 +} + +# Add remaining services following the same pattern +# Services with native OIDC (Outline, Gitea, Vikunja): no forward auth needed +# Services without OIDC: import authentik_forward_auth +``` + +### Build and start + +```bash +cd /opt/docker/caddy +docker compose build +docker compose up -d +docker logs -f caddy +# Wait for: "certificate obtained successfully" +``` + +## ntfy Deployment + +### server.yml + +```yaml +base-url: https://ntfy.lerkolabs.com +listen-http: ":8090" +cache-file: /var/cache/ntfy/cache.db +auth-file: /var/lib/ntfy/auth.db +auth-default-access: deny-all +behind-proxy: true +``` + +### docker-compose.yml + +```yaml +services: + ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + restart: unless-stopped + command: serve + ports: + - "8090:8090" + volumes: + - ./server.yml:/etc/ntfy/server.yml:ro + - ./data:/var/cache/ntfy + - ./data:/var/lib/ntfy +``` + +```bash +cd /opt/docker/infra/ntfy && docker compose up -d +docker exec -it ntfy ntfy user add --role=admin +``` + +## Uptime Kuma Deployment + +```yaml +services: + uptime-kuma: + image: louislam/uptime-kuma:latest + container_name: uptime-kuma + restart: unless-stopped + ports: + - "3001:3001" + volumes: + - ./data:/app/data +``` + +```bash +cd /opt/docker/infra/uptimekuma && docker compose up -d +``` + +## Caddy Reload + +After editing the Caddyfile: + +```bash +docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile +docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile +``` + +## Verification + +```bash +# Cert issued +docker logs caddy | grep "certificate obtained" + +# Service reachable +curl -I https://pihole.lerkolabs.com +# Expected: HTTP/2 200 + +# Ports bound +ss -tlnp | grep -E "443|8090|3001" +``` diff --git a/setup/monitor-lxc.md b/setup/monitor-lxc.md new file mode 100644 index 0000000..570ef3d --- /dev/null +++ b/setup/monitor-lxc.md @@ -0,0 +1,157 @@ +# Monitor LXC Setup + +## Overview + +The `monitor` LXC (10.2.0.51) in VLAN 1020 runs the full observability stack: Victoria Metrics (metrics storage), Grafana (dashboards and alerting), and Beszel (container + host monitoring). All services run via Docker Compose. + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | monitor | +| IP | 10.2.0.51/24 | +| Gateway | 10.2.0.1 | +| DNS | 10.2.0.11 | +| Cores | 4 | +| RAM | 4GB | +| Template | debian-12-standard | +| Nesting | ✓ | + +## Prerequisites + +- Caddy running at 10.2.0.20 +- Pi-hole DNS records added (see Verification) +- Beszel agents deployed on all LXCs to be monitored + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl nano +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +mkdir -p /opt/docker/monitor/{victoria-metrics,grafana,beszel} +``` + +## Victoria Metrics + +```yaml +# /opt/docker/monitor/victoria-metrics/docker-compose.yml +services: + victoria-metrics: + image: victoriametrics/victoria-metrics:latest + container_name: victoria-metrics + restart: unless-stopped + ports: + - "8428:8428" + volumes: + - ./data:/storage + command: + - "--storageDataPath=/storage" + - "--retentionPeriod=90d" +``` + +```bash +cd /opt/docker/monitor/victoria-metrics && docker compose up -d +``` + +## Grafana + +```yaml +# /opt/docker/monitor/grafana/docker-compose.yml +services: + grafana: + image: grafana/grafana:latest + container_name: grafana + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - ./data:/var/lib/grafana + environment: + GF_SERVER_ROOT_URL: https://grafana.lerkolabs.com + GF_AUTH_GENERIC_OAUTH_ENABLED: "true" + GF_AUTH_GENERIC_OAUTH_NAME: Authentik + GF_AUTH_GENERIC_OAUTH_CLIENT_ID: + GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: + GF_AUTH_GENERIC_OAUTH_SCOPES: openid email profile + GF_AUTH_GENERIC_OAUTH_AUTH_URL: https://auth.lerkolabs.com/application/o/authorize/ + GF_AUTH_GENERIC_OAUTH_TOKEN_URL: https://auth.lerkolabs.com/application/o/token/ + GF_AUTH_GENERIC_OAUTH_API_URL: https://auth.lerkolabs.com/application/o/userinfo/ + GF_AUTH_SIGNOUT_REDIRECT_URL: https://auth.lerkolabs.com/application/o/grafana/end-session/ + GF_AUTH_OAUTH_AUTO_LOGIN: "true" +``` + +```bash +cd /opt/docker/monitor/grafana && docker compose up -d +``` + +Add Victoria Metrics as a data source in Grafana: `http://localhost:8428` + +## Beszel + +Beszel hub runs on the monitor LXC. Beszel agents run on each LXC/VM being monitored. + +### Hub (monitor LXC) + +```yaml +# /opt/docker/monitor/beszel/docker-compose.yml +services: + beszel: + image: henrygd/beszel:latest + container_name: beszel + restart: unless-stopped + ports: + - "8090:8090" + volumes: + - ./data:/beszel_data +``` + +```bash +cd /opt/docker/monitor/beszel && docker compose up -d +``` + +### Agents (each LXC) + +On each LXC that needs monitoring: + +```bash +curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh +chmod +x install-agent.sh +./install-agent.sh # follow prompts, enter hub address and key +``` + +## Caddy Configuration + +Add to Caddyfile on infra LXC: + +```caddyfile +grafana.lerkolabs.com { + reverse_proxy 10.2.0.51:3000 +} +``` + +Beszel and Victoria Metrics are internal-only (no public Caddy entries needed unless you want external access). + +## Pi-hole DNS Records + +``` +grafana.lerkolabs.com → 10.2.0.20 +``` + +## Verification + +```bash +# All containers running +docker ps + +# Victoria Metrics health +curl http://localhost:8428/health + +# Grafana reachable +curl -I https://grafana.lerkolabs.com + +# Beszel agents reporting +# Check Beszel web UI at http://10.2.0.51:8090 +``` diff --git a/setup/pfsense-vlans.md b/setup/pfsense-vlans.md new file mode 100644 index 0000000..6ec8bec --- /dev/null +++ b/setup/pfsense-vlans.md @@ -0,0 +1,116 @@ +# pfSense VLAN Setup + +## Overview + +pfSense (Intel N100 mini PC at 10.0.0.1 / 10.1.0.1) handles firewall, routing, DHCP, DNS resolution, and WireGuard VPN for all 8 VLANs. See [Network](../docs/NETWORK.md) for the full VLAN map and firewall policy. See [Decisions](../docs/DECISIONS.md) D005 for the AT&T IP Passthrough rationale. + +## Prerequisites + +- pfSense installed on Intel N100 mini PC +- AT&T BGW320 in IP Passthrough mode (pfSense WAN gets public IP) +- Omada managed switch connected to pfSense +- Trunk port between pfSense and switch carrying all VLANs tagged + +## VLAN Configuration + +### 1. Create VLAN Interfaces + +Navigate to: **Interfaces → VLANs → Add** + +Create one entry per VLAN: + +| VLAN Tag | Parent | Description | +|----------|--------|-------------| +| 1000 | (WAN NIC or LAN NIC) | MGMT | +| 1010 | (LAN NIC) | LAN | +| 1020 | (LAN NIC) | Homelab | +| 1030 | (LAN NIC) | Guests | +| 1040 | (LAN NIC) | IoT | +| 1050 | (LAN NIC) | WFH | +| 1099 | (LAN NIC) | DMZ | + +### 2. Assign VLAN Interfaces + +Navigate to: **Interfaces → Assignments** + +Add each VLAN as a new interface. Enable and configure each: + +| Interface | IP | Subnet | +|-----------|-----|--------| +| MGMT (1000) | 10.0.0.1 | /24 | +| LAN (1010) | 10.1.0.1 | /24 | +| Homelab (1020) | 10.2.0.1 | /24 | +| Guests (1030) | 10.3.0.1 | /24 | +| IoT (1040) | 10.4.0.1 | /24 | +| WFH (1050) | 10.5.0.1 | /24 | +| DMZ (1099) | 10.99.0.1 | /24 | + +### 3. DHCP Servers + +Navigate to: **Services → DHCP Server** — configure one per VLAN: + +| VLAN | DHCP Range | DNS | +|------|------------|-----| +| MGMT | 10.0.0.100–150 | pfSense (10.0.0.1) | +| LAN | 10.1.0.100–200 | Pi-hole (10.2.0.11) | +| Homelab | 10.2.0.100–200 | Pi-hole (10.2.0.11) | +| Guests | 10.3.0.100–250 | Pi-hole (10.2.0.11) | +| IoT | 10.4.0.100–250 | Pi-hole (10.2.0.11) | +| WFH | 10.5.0.100–200 | pfSense (10.5.0.1) — Pi-hole intentionally excluded | +| DMZ | static only | pfSense (10.99.0.1) | + +### 4. Firewall Rules + +Navigate to: **Firewall → Rules** — configure per-interface rules following the policy in [NETWORK.md](../docs/NETWORK.md#firewall-policy). + +Key rules: + +- Default deny all inter-VLAN (floating rule or per-interface block at end) +- LAN → Homelab: allow (LAN users reach services) +- LAN → MGMT: allow (admin access from home devices) +- Homelab → internet: HTTP/S, SSH, NTP only (for updates) +- Guests → internet only: block all RFC1918 +- IoT → internet + Home Assistant: block everything else +- WFH → internet only: block all RFC1918, pfSense DNS only +- MGMT → internet: NTP + updates only; inbound from LAN + VPN only +- DMZ → internet: HTTP/S + NTP; block all internal VLANs + +### 5. DNS Resolver (Unbound) + +Navigate to: **Services → DNS Resolver** + +- Enable: ✓ +- Listen on: all interfaces +- Upstream DNS: Cloudflare 1.1.1.1 +- DNSSEC: ✓ (optional) + +Pi-hole (10.2.0.11) uses pfSense Unbound as its upstream. WFH VLAN devices use pfSense Unbound directly — Pi-hole is unreachable from WFH by firewall rule. + +### 6. Static DHCP Reservations + +Navigate to: **Services → DHCP Server → [interface] → DHCP Static Mappings** + +Add reservations for all homelab hosts from [NETWORK.md](../docs/NETWORK.md#static-ip-reservations). + +## Configuration Backup + +Navigate to: **Diagnostics → Backup & Restore → Backup Configuration** + +Download `config.xml`. Store in Vaultwarden or PBS. This is the single file needed to restore pfSense from scratch. + +## Verification + +```bash +# From a LAN device: +# 1. Gets IP from DHCP in 10.1.0.100–200 range +ip addr + +# 2. DNS resolves via Pi-hole +nslookup google.com # should show answer from 10.2.0.11 + +# 3. Internal service resolves +nslookup outline.lerkolabs.com # should return 10.2.0.20 + +# 4. Internet access works +curl -I https://google.com +``` diff --git a/setup/pihole.md b/setup/pihole.md new file mode 100644 index 0000000..6fcb898 --- /dev/null +++ b/setup/pihole.md @@ -0,0 +1,96 @@ +# Pi-hole Setup + +## Overview + +Pi-hole runs in the `pihole` LXC (10.2.0.11) in VLAN 1020 (Homelab). It is the primary DNS server for all VLANs, providing ad/tracker blocking, local DNS records, and query logging. All `*.lerkolabs.com` subdomains resolve to 10.2.0.20 (Caddy). Upstream resolver is pfSense Unbound → Cloudflare 1.1.1.1. + +## Prerequisites + +- LXC created in VLAN 1020 with static IP 10.2.0.11 +- Debian 12 template +- pfSense DHCP reservations updated to point VLANs at 10.2.0.11 for DNS + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | pihole | +| IP | 10.2.0.11/24 | +| Gateway | 10.2.0.1 | +| Cores | 1 | +| RAM | 512MB | +| Template | debian-12-standard | + +## Installation + +```bash +apt update && apt upgrade -y +curl -sSL https://install.pi-hole.net | bash +``` + +Installer prompts: +- Upstream DNS: Custom (set to pfSense: 10.2.0.1) +- Blocklists: Default (customize later) +- Admin Web Interface: Yes +- Web Server: lighttpd +- Query Logging: Yes +- Privacy Mode: Show everything (0) + +## Configuration + +### Local DNS Records + +Add all internal domains via **Local DNS → DNS Records**. Every entry points to 10.2.0.20 (Caddy), not the service directly. + +Key records to add: + +| Domain | IP | +|--------|----| +| pihole.lerkolabs.com | 10.2.0.20 | +| auth.lerkolabs.com | 10.2.0.20 | +| outline.lerkolabs.com | 10.2.0.20 | +| gitea.lerkolabs.com | 10.2.0.20 | +| tasks.lerkolabs.com | 10.2.0.20 | +| finance.lerkolabs.com | 10.2.0.20 | +| grafana.lerkolabs.com | 10.2.0.20 | +| proxmox.lerkolabs.com | 10.2.0.20 | +| vault.lerkolabs.com | 10.2.0.20 | + +Add remaining services from [SERVICES.md](../docs/SERVICES.md) following the same pattern. + +### Upstream DNS + +Settings → DNS → Custom upstream: `10.2.0.1` (pfSense Unbound) + +Uncheck all other upstream providers. + +### pfSense DHCP Integration + +In pfSense: set DNS server for each VLAN's DHCP scope to 10.2.0.11. The WFH VLAN (1050) is the exception — it uses pfSense DNS only (Pi-hole unreachable by design). + +## Backup / Restore + +Use Teleporter for full config export: Settings → Teleporter → Backup. Store the teleporter zip in Vaultwarden or PBS. + +On restore: Settings → Teleporter → Restore. All DNS records, blocklists, and settings are included. + +## Verification + +```bash +# DNS resolves internal names +nslookup outline.lerkolabs.com 10.2.0.11 +# Expected: 10.2.0.20 + +# Ad blocking active +nslookup doubleclick.net 10.2.0.11 +# Expected: 0.0.0.0 + +# Admin interface +curl -s http://10.2.0.11/admin | grep -i pi-hole +``` + +## Updates + +```bash +pihole -up +``` diff --git a/setup/servarr.md b/setup/servarr.md new file mode 100644 index 0000000..fc9a0fd --- /dev/null +++ b/setup/servarr.md @@ -0,0 +1,143 @@ +# Servarr (Media VM) Setup + +## Overview + +The `servarr` VM runs the complete media stack: Plex and Jellyfin for streaming, the *arr suite for automated media management, and qBittorrent routed through Gluetun VPN for downloads. All services run via Docker Compose. The VM lives on Proxmox in VLAN 1020 (Homelab). + +## VM Spec + +| Property | Value | +|----------|-------| +| Hostname | servarr | +| VLAN | 1020 (Homelab) | +| Cores | 4 | +| RAM | 8GB | +| OS | Debian 12 | +| Nesting | ✓ | + +## Services + +| Service | Purpose | +|---------|---------| +| Plex | Media streaming (hardware transcoding) | +| Jellyfin | Open-source media streaming alternative | +| Sonarr | TV show management | +| Radarr | Movie management | +| Lidarr | Music management | +| Prowlarr | Indexer aggregation (feeds Sonarr/Radarr/Lidarr) | +| Bazarr | Subtitle management | +| qBittorrent | Downloads — routed through Gluetun VPN container | +| Calibre-Web Automated | Book library with auto-ingest | +| Kavita | E-reader / comic reader | + +## Prerequisites + +- VM created in VLAN 1020 +- Gluetun-compatible VPN credentials (stored in Vaultwarden) +- Media storage mounted (NFS or local disk) +- Caddy routing configured for any public-facing services + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl nano +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +``` + +## Directory Structure + +``` +/opt/docker/servarr/ +├── docker-compose.yml +├── .env +└── config/ + ├── plex/ + ├── jellyfin/ + ├── sonarr/ + ├── radarr/ + ├── lidarr/ + ├── prowlarr/ + ├── bazarr/ + ├── qbittorrent/ + ├── calibre/ + └── kavita/ + +/media/ +├── tv/ +├── movies/ +├── music/ +├── books/ +└── downloads/ + ├── complete/ + └── incomplete/ +``` + +## qBittorrent + Gluetun (VPN-gated downloads) + +qBittorrent runs inside the Gluetun network namespace. All download traffic exits through the VPN — no VPN = no download traffic. + +```yaml +# docker-compose.yml excerpt +services: + gluetun: + image: qmcgaw/gluetun:latest + container_name: gluetun + cap_add: + - NET_ADMIN + environment: + - VPN_SERVICE_PROVIDER= + - VPN_TYPE=wireguard + - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY} + - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES} + - SERVER_COUNTRIES=${SERVER_COUNTRIES} + ports: + - "8080:8080" # qBittorrent WebUI via Gluetun + + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + network_mode: "service:gluetun" # all traffic through VPN + environment: + - WEBUI_PORT=8080 + volumes: + - ./config/qbittorrent:/config + - /media/downloads:/downloads +``` + +## *arr Suite Configuration + +Prowlarr is the central indexer — configure it first, then connect Sonarr/Radarr/Lidarr to it. All *arr services connect to qBittorrent as the download client (pointing to Gluetun's exposed port). + +## Caddy Configuration + +Add to Caddyfile on infra LXC for any *arr services you want accessible via HTTPS. Replace `` with the VM's IP. + +```caddyfile +# Example — Plex handles its own auth, no forward auth needed +plex.lerkolabs.com { + reverse_proxy :32400 +} + +# *arr services — protect with Authentik forward auth +sonarr.lerkolabs.com { + import authentik_forward_auth + reverse_proxy :8989 +} +``` + +## Verification + +```bash +# All containers running +docker ps + +# Gluetun VPN tunnel active +docker exec gluetun wget -qO- https://api.ipinfo.io/ip +# Should return VPN provider IP, not home WAN IP + +# qBittorrent accessible +curl -I http://localhost:8080 +``` diff --git a/setup/vaultwarden.md b/setup/vaultwarden.md new file mode 100644 index 0000000..cd42db1 --- /dev/null +++ b/setup/vaultwarden.md @@ -0,0 +1,157 @@ +# Vaultwarden Setup + +## Overview + +Vaultwarden runs in the `vault` LXC (10.2.0.X) in VLAN 1020 (Homelab). It is isolated — no shared containers, no shared Postgres. Accessible at `https://vault.lerkolabs.com` via Caddy with Authentik forward auth. VPN-only access (not exposed to internet directly). + +## LXC Spec + +| Property | Value | +|----------|-------| +| Hostname | vault | +| IP | 10.2.0.X/24 (TBD) | +| Gateway | 10.2.0.1 | +| DNS | 10.2.0.11 | +| Cores | 1 | +| RAM | 256MB | +| Disk | 4GB | +| Template | debian-12-standard | +| Nesting | ✓ | + +## Prerequisites + +- Caddy running at 10.2.0.20 +- Pi-hole DNS record: `vault.lerkolabs.com → 10.2.0.20` + +## Installation + +```bash +apt update && apt upgrade -y +apt install -y curl nano +timedatectl set-timezone +curl -fsSL https://get.docker.com | sh +systemctl enable docker +mkdir -p /opt/docker/vaultwarden/data +``` + +## Configuration + +```yaml +# /opt/docker/vaultwarden/docker-compose.yml +services: + vaultwarden: + image: vaultwarden/server:latest + container_name: vaultwarden + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./data:/data + environment: + - DOMAIN=https://vault.lerkolabs.com + - SIGNUPS_ALLOWED=true # set false after creating your account + - WEBSOCKET_ENABLED=true + - LOG_FILE=/data/vaultwarden.log + - LOG_LEVEL=warn + - ROCKET_PORT=80 +``` + +```bash +cd /opt/docker/vaultwarden +docker compose up -d +docker logs -f vaultwarden +``` + +## Initial Account Setup + +1. Navigate to `https://vault.lerkolabs.com` +2. Create your account +3. Set `SIGNUPS_ALLOWED=false` in docker-compose.yml and restart: + ```bash + docker compose up -d + ``` + +## Enable Admin Panel + +```bash +openssl rand -base64 48 # generate admin token +``` + +Add to environment in docker-compose.yml: + +```yaml +- ADMIN_TOKEN= +``` + +Access admin panel at: `https://vault.lerkolabs.com/admin` + +## Caddy Configuration + +Add to Caddyfile on infra LXC: + +```caddyfile +vault.lerkolabs.com { + import authentik_forward_auth + reverse_proxy 10.2.0.X:80 + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "no-referrer" + } +} +``` + +## Connecting Bitwarden Clients + +In any official Bitwarden client (mobile, desktop, browser extension): + +``` +Settings → Self-hosted Environment +Server URL: https://vault.lerkolabs.com +``` + +## Backup + +```bash +#!/bin/bash +# /opt/backup-vaultwarden.sh +BACKUP_DIR="/opt/backups/vaultwarden" +DATE=$(date +%Y%m%d-%H%M%S) +mkdir -p "$BACKUP_DIR" + +docker stop vaultwarden +tar -czf "$BACKUP_DIR/vaultwarden-$DATE.tar.gz" /opt/docker/vaultwarden/data/ +docker start vaultwarden + +find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete +``` + +```bash +chmod +x /opt/backup-vaultwarden.sh +crontab -e +# Add: 0 2 * * * /opt/backup-vaultwarden.sh >> /var/log/vaultwarden-backup.log 2>&1 +``` + +## Verification + +```bash +# Container running +docker ps + +# Accessible via Caddy +curl -I https://vault.lerkolabs.com +# Expected: HTTP/2 200 or 302 (Authentik redirect) + +# Data directory exists +ls /opt/docker/vaultwarden/data/ +``` + +## Updates + +```bash +cd /opt/docker/vaultwarden +docker compose pull +docker compose up -d +docker image prune -f +``` diff --git a/setup/wireguard.md b/setup/wireguard.md new file mode 100644 index 0000000..8f6207a --- /dev/null +++ b/setup/wireguard.md @@ -0,0 +1,130 @@ +# WireGuard Setup + +## Overview + +WireGuard VPN is configured directly in pfSense. It runs on UDP port 51820 — the only inbound port on the WAN interface. VPN clients get IPs in the 10.200.0.0/24 subnet and receive the same network access as LAN (Homelab + MGMT web GUI + Pi-hole DNS). No external software needed — pfSense handles it natively. + +| Property | Value | +|----------|-------| +| Listen Port | 51820 UDP | +| VPN Subnet | 10.200.0.0/24 | +| Access granted | Homelab (10.2.0.0/24) + MGMT web GUI + Pi-hole DNS | +| Access blocked | Guest, IoT, WFH VLANs | + +## Prerequisites + +- pfSense running and accessible +- WireGuard package installed (System → Package Manager → Available Packages → WireGuard) +- Port 51820 UDP forwarded/open on WAN if behind NAT (not needed with IP Passthrough — pfSense has the public IP directly) +- DDNS client configured on pfSense if WAN IP is dynamic + +## Installation + +### 1. Install WireGuard Package + +Navigate to: **System → Package Manager → Available Packages** + +Search "WireGuard" → Install. + +### 2. Create WireGuard Tunnel + +Navigate to: **VPN → WireGuard → Tunnels → Add Tunnel** + +| Setting | Value | +|---------|-------| +| Enabled | ✓ | +| Description | HomeVPN | +| Listen Port | 51820 | +| Interface Keys | Click "Generate" | +| Interface Addresses | 10.200.0.1/24 | + +Save. Note the **server public key** — you'll need it in peer configs. + +### 3. Add Peers (Clients) + +Navigate to: **VPN → WireGuard → Peers → Add Peer** + +For each client device: + +| Setting | Value | +|---------|-------| +| Tunnel | HomeVPN | +| Description | e.g., iPhone | +| Public Key | (generate on client, paste here) | +| Allowed IPs | 10.200.0.X/32 (unique per peer) | + +### 4. Create WireGuard Interface + +Navigate to: **Interfaces → Assignments** + +Assign the WireGuard tunnel as a new interface (e.g., `OPT1`). Rename it to `WG` or `VPN`. + +Enable the interface: Interfaces → WG → Enable ✓ + +### 5. Firewall Rules + +#### WAN — allow inbound WireGuard + +Navigate to: **Firewall → Rules → WAN → Add** + +| Setting | Value | +|---------|-------| +| Action | Pass | +| Protocol | UDP | +| Destination | WAN address | +| Destination Port | 51820 | +| Description | WireGuard VPN | + +#### WG interface — allow VPN clients same access as LAN + +Navigate to: **Firewall → Rules → WG → Add** + +``` +Pass | IPv4 | Source: WG net | Destination: 10.2.0.0/24 | any | Homelab access +Pass | IPv4 | Source: WG net | Destination: 10.0.0.0/24 | 443 | MGMT web GUI +Pass | IPv4 | Source: WG net | Destination: 10.2.0.11 | 53 | Pi-hole DNS +``` + +### 6. DNS for VPN Clients + +In WireGuard peer config, set DNS to 10.2.0.11 (Pi-hole) so VPN clients get ad blocking and local name resolution. + +## Client Configuration + +Generate on each client device. Structure: + +```ini +[Interface] +PrivateKey = +Address = 10.200.0.X/24 +DNS = 10.2.0.11 + +[Peer] +PublicKey = +Endpoint = :51820 +AllowedIPs = 10.0.0.0/8 # route all RFC1918 through VPN, or use split tunnel +PersistentKeepalive = 25 +``` + +In pfSense you can generate QR codes for mobile clients: VPN → WireGuard → Peers → (peer) → QR code icon. + +## Key Rotation + +When rotating keys or adding/removing peers: + +1. Generate new key pair on client +2. Update peer's public key in pfSense: VPN → WireGuard → Peers → Edit +3. Update client config with new private key +4. Apply changes in pfSense + +## Verification + +```bash +# From a mobile device on cellular (not home WiFi): +# 1. Connect WireGuard +# 2. curl https://outline.lerkolabs.com → should load with Authentik login +# 3. curl http://10.2.0.11/admin → Pi-hole admin should be reachable + +# On pfSense shell: +wg show # should show peer with recent handshake +```