Compare commits
1 Commits
c2dbfdbea8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 73c1dde7e5 |
@@ -1,10 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this homelab, in reverse chronological order.
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
@@ -1,55 +1,42 @@
|
|||||||
# homelab
|
# 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.
|
Self-hosted services on a single Proxmox host. Segmented network, runs 24/7.
|
||||||
|
|
||||||
## At a Glance
|
## Why I built this
|
||||||
|
|
||||||
| Component | Technology |
|
I started this while studying for CompTIA and the plan was a small router, a few VLANs, and maybe two or three services and then got carried away.
|
||||||
|-----------|-----------|
|
|
||||||
|
## What's running
|
||||||
|
|
||||||
|
| Layer | Tool |
|
||||||
|
|---|---|
|
||||||
| Hypervisor | Proxmox VE |
|
| Hypervisor | Proxmox VE |
|
||||||
| Firewall | pfSense (Intel N100) |
|
| Firewall | pfSense (low-power x86) |
|
||||||
| Switching | TP-Link Omada (managed VLANs) |
|
| Switching | TP-Link Omada (managed VLANs) |
|
||||||
| Reverse Proxy | Caddy + Cloudflare DNS-01 |
|
| Reverse proxy | Caddy (Cloudflare DNS-01) |
|
||||||
| Auth | Authentik SSO (OIDC + forward auth) |
|
| Identity | Authentik (OIDC + forward auth) |
|
||||||
| DNS | Pi-hole → pfSense Unbound → Cloudflare |
|
| DNS | Pi-hole → Unbound → Cloudflare |
|
||||||
| VPN | WireGuard, UDP 51820 |
|
| Remote access | WireGuard |
|
||||||
| Monitoring | Victoria Metrics + Grafana + Beszel |
|
| Monitoring | Victoria Metrics + Grafana + Beszel |
|
||||||
| Backups | Proxmox Backup Server (PBS) |
|
| Backups | Proxmox Backup Server |
|
||||||
|
|
||||||
## Compute Layout
|
## Scope
|
||||||
|
|
||||||
| Container | IP | Cores | RAM | What Runs |
|
Around 10 LXCs and a couple of VMs running about 20 services across 7 VLANs.
|
||||||
|-----------|-----|-------|-----|-----------|
|
|
||||||
| `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)
|
## Design choices
|
||||||
|
|
||||||
| Container | IP | Service |
|
- VLANs are organized by trust tier. Management is its own tier because a compromise there would be no bueno
|
||||||
|-----------|-----|---------|
|
- Internal services sit behind Authentik. OIDC where the app supports it and then Caddy forward auth where it doesn't
|
||||||
| `caddy-dmz` | 10.99.0.20 | Public reverse proxy |
|
- Public surface is small. A handful of services, behind a DMZ-isolated reverse proxy with firewall rules backing up the proxy config
|
||||||
| `gitea` | 10.99.0.22 | gitea.lerkolabs.com |
|
- Admin surfaces are only available from Management tier and VPN.
|
||||||
| `portfolio` | 10.99.0.23 | lerkolabs.com |
|
|
||||||
|
|
||||||
## Key Principles
|
## Documented here
|
||||||
|
|
||||||
- All services require Authentik authentication — no anonymous access
|
| Doc | About |
|
||||||
- No management ports exposed to internet — all admin access via WireGuard first
|
|---|---|
|
||||||
- Caddy handles TLS termination; internal services run plain HTTP
|
| [Services](docs/SERVICES.md) | What's deployed, grouped by what it does |
|
||||||
- Secrets never committed — all referenced by Vaultwarden entry name
|
| [Network](docs/NETWORK.md) | Segmentation, firewall posture, DNS |
|
||||||
|
| [Security](docs/SECURITY.md) | Layered controls, threat model, limitations |
|
||||||
|
|
||||||
## Navigation
|
The IP plan, hardware inventory, ADRs, rebuild runbook, and retention policies are in a private repo.
|
||||||
|
|
||||||
- [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
|
|
||||||
|
|||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
# 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)
|
|
||||||
+25
-10
@@ -1,15 +1,30 @@
|
|||||||
# Authentication Flow
|
# Authentication Flow
|
||||||
|
|
||||||
|
Forward auth path for an internal service that doesn't speak OIDC natively. OIDC-native services skip the Caddy auth hop and go to Authentik directly.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
User->>Caddy: HTTPS request
|
participant U as User
|
||||||
Caddy->>Authentik: Forward auth check
|
participant C as Caddy<br/>(reverse proxy)
|
||||||
Authentik-->>Caddy: 401 if unauthenticated
|
participant A as Authentik<br/>(IdP)
|
||||||
Caddy-->>User: Redirect to auth.lerkolabs.com
|
participant S as Internal service
|
||||||
User->>Authentik: Login (OIDC or forward auth)
|
|
||||||
Authentik-->>User: Session cookie
|
U->>C: HTTPS request
|
||||||
User->>Caddy: HTTPS request + cookie
|
C->>A: Forward auth check
|
||||||
Caddy->>Authentik: Forward auth check
|
A-->>C: 401 (no session)
|
||||||
Authentik-->>Caddy: 200 OK
|
C-->>U: 302 → auth.lerkolabs.com
|
||||||
Caddy->>Service: Proxy request
|
|
||||||
|
U->>A: Login (OIDC or password)
|
||||||
|
A-->>U: Set session cookie
|
||||||
|
|
||||||
|
U->>C: HTTPS request + cookie
|
||||||
|
C->>A: Forward auth check
|
||||||
|
A-->>C: 200 OK + identity headers
|
||||||
|
C->>S: Proxy request<br/>(plain HTTP, internal hop)
|
||||||
|
S-->>U: Response
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If Authentik is down, internal services are unreachable. This is an accepted tradeoff.
|
||||||
|
|||||||
+43
-5
@@ -1,9 +1,47 @@
|
|||||||
# DNS Resolution Chain
|
# DNS Resolution
|
||||||
|
|
||||||
|
Two flows, one resolver chain.
|
||||||
|
|
||||||
|
## External resolution
|
||||||
|
|
||||||
|
Client asks for a public domain.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
D[Device] --> PH[Pi-hole\n10.2.0.11]
|
CLIENT[Client<br/>most VLANs] --> PIHOLE[Pi-hole<br/>filtering + cache]
|
||||||
PH --> UB[pfSense Unbound\n10.x.0.1]
|
PIHOLE -->|miss| UNBOUND[Unbound on firewall<br/>recursive + DNSSEC]
|
||||||
UB --> CF[Cloudflare\n1.1.1.1]
|
UNBOUND --> UPSTREAM[Cloudflare<br/>fallback only]
|
||||||
PH -- "*.lerkolabs.com" --> CADDY[Caddy\n10.2.0.20]
|
|
||||||
|
PIHOLE -.->|blocked| BLOCKED[Ad/tracker<br/>domains]
|
||||||
|
|
||||||
|
classDef client fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0
|
||||||
|
classDef resolver fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0
|
||||||
|
classDef upstream fill:#3a2f1f,stroke:#8b6b3a,color:#f0e0d0
|
||||||
|
classDef blocked fill:#3a1f1f,stroke:#8b3a3a,color:#f0d0d0
|
||||||
|
|
||||||
|
class CLIENT client
|
||||||
|
class PIHOLE,UNBOUND resolver
|
||||||
|
class UPSTREAM upstream
|
||||||
|
class BLOCKED blocked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Local hostname resolution (split-horizon)
|
||||||
|
|
||||||
|
Client asks for an internal hostname. The query stays on the LAN. Pi-hole answers from local A records and the client connects to the internal reverse proxy.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
CLIENT[Client] -->|asks for<br/>app.lerkolabs.com| PIHOLE[Pi-hole<br/>local A records]
|
||||||
|
PIHOLE -->|returns<br/>internal IP| CLIENT
|
||||||
|
CLIENT -->|HTTPS<br/>valid public cert| CADDY[Internal Caddy<br/>reverse proxy]
|
||||||
|
CADDY --> SVC[Internal service]
|
||||||
|
|
||||||
|
classDef client fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0
|
||||||
|
classDef resolver fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0
|
||||||
|
classDef edge fill:#2f1f3a,stroke:#6b3a8b,color:#e0d0f0
|
||||||
|
|
||||||
|
class CLIENT client
|
||||||
|
class PIHOLE resolver
|
||||||
|
class CADDY,SVC edge
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
+84
-11
@@ -1,15 +1,88 @@
|
|||||||
# Network Topology
|
# Network Topology
|
||||||
|
|
||||||
|
Two views of the same network.
|
||||||
|
|
||||||
|
## Trust tiers and policy
|
||||||
|
|
||||||
|
Seven VLANs grouped by trust level. Edges show allowed inter-tier flows; everything else is default-deny.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TB
|
||||||
ONT[AT&T Fiber ONT] --> BGW[BGW320 IP Passthrough]
|
subgraph UNTRUSTED["Untrusted — internet only, no internal access"]
|
||||||
BGW --> PF[pfSense N100]
|
GUEST[Guest WiFi]
|
||||||
PF --> SW[Omada Switch]
|
IOT[IoT]
|
||||||
SW --> MGMT[VLAN 1000 MGMT\n10.0.0.0/24]
|
WFH[Work-from-home]
|
||||||
SW --> LAN[VLAN 1010 LAN\n10.1.0.0/24]
|
end
|
||||||
SW --> HL[VLAN 1020 Homelab\n10.2.0.0/24]
|
|
||||||
SW --> GUEST[VLAN 1030 Guests\n10.3.0.0/24]
|
subgraph PUBLIC["Public-facing"]
|
||||||
SW --> IOT[VLAN 1040 IoT\n10.4.0.0/24]
|
DMZ[DMZ<br/>reverse proxy + public services]
|
||||||
SW --> WFH[VLAN 1050 WFH\n10.5.0.0/24]
|
end
|
||||||
SW --> DMZ[VLAN 1 DMZ\n10.99.0.0/24]
|
|
||||||
|
subgraph TRUSTED["Trusted"]
|
||||||
|
LAN[LAN<br/>personal devices]
|
||||||
|
INT[Internal services<br/>app stack]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph MGMT["Management — VPN-only"]
|
||||||
|
ADMIN[Hypervisor, firewall,<br/>backup, switches, APs]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph REMOTE["Remote"]
|
||||||
|
VPN[WireGuard clients]
|
||||||
|
end
|
||||||
|
|
||||||
|
INTERNET((Internet))
|
||||||
|
|
||||||
|
UNTRUSTED -->|outbound only| INTERNET
|
||||||
|
INTERNET -->|HTTP/HTTPS<br/>tight allowlist| DMZ
|
||||||
|
INTERNET -->|WireGuard<br/>UDP| VPN
|
||||||
|
|
||||||
|
DMZ -.->|narrow allowlist<br/>firewall-enforced| INT
|
||||||
|
LAN -->|consume services| INT
|
||||||
|
VPN -->|LAN-equivalent +<br/>admin access| INT
|
||||||
|
VPN --> ADMIN
|
||||||
|
|
||||||
|
classDef untrusted fill:#3a1f1f,stroke:#8b3a3a,color:#f0d0d0
|
||||||
|
classDef public fill:#3a2f1f,stroke:#8b6b3a,color:#f0e0d0
|
||||||
|
classDef trusted fill:#1f3a2f,stroke:#3a8b6b,color:#d0f0e0
|
||||||
|
classDef mgmt fill:#1f2f3a,stroke:#3a6b8b,color:#d0e0f0
|
||||||
|
classDef remote fill:#2f1f3a,stroke:#6b3a8b,color:#e0d0f0
|
||||||
|
|
||||||
|
class GUEST,IOT,WFH untrusted
|
||||||
|
class DMZ public
|
||||||
|
class LAN,INT trusted
|
||||||
|
class ADMIN mgmt
|
||||||
|
class VPN remote
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Physical flow
|
||||||
|
|
||||||
|
What plugs into what. Tier labels, not addresses.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
ISP[ISP] --> GW[Carrier gateway<br/>passthrough mode]
|
||||||
|
GW --> FW[pfSense firewall]
|
||||||
|
FW --> SW[Managed switch<br/>VLAN-aware]
|
||||||
|
|
||||||
|
SW --> T_MGMT[MGMT tier]
|
||||||
|
SW --> T_INT[Internal services tier]
|
||||||
|
SW --> T_LAN[LAN tier]
|
||||||
|
SW --> T_WFH[WFH tier]
|
||||||
|
SW --> T_IOT[IoT tier]
|
||||||
|
SW --> T_GUEST[Guest tier]
|
||||||
|
SW --> T_DMZ[DMZ tier]
|
||||||
|
|
||||||
|
FW -.->|VPN concentrator| VPN[WireGuard]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Two reverse proxies
|
||||||
|
|
||||||
|
The DMZ-to-internal arrow above is by design. There are two Caddy instances:
|
||||||
|
|
||||||
|
- One in DMZ, internet-facing, fronting a small set of public services.
|
||||||
|
- One in internal services tier, LAN/VPN only, fronting everything else.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Inter-tier policy enforced at the firewall.
|
||||||
|
|||||||
@@ -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 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
|
|
||||||
@@ -1,78 +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 | Dell Precision Rack 7910 | Hosts all LXCs + VMs |
|
|
||||||
| Proxmox backup server | Backup server | HP 600 | Backup all LXCs + VMs |
|
|
||||||
| pfSense router | Firewall / VPN / DHCP / routing | Intel N100 mini PC | ~6W idle, handles 2–3Gbps routing + 600Mbps WireGuard |
|
|
||||||
| Managed switch | VLAN switching | TP-Link Omada SG2210MP | All port VLANs managed via Omada Controller |
|
|
||||||
| Access point | LAN / Guest / IoT / Work WiFi | TP-Link Omada AP 650 | Auto-adopted by Omada Controller |
|
|
||||||
| Access point | LAN / Guest / IoT / Work WiFi | TP-Link Omada AP 670 | Auto-adopted by Omada Controller |
|
|
||||||
| AT&T Gateway | ISP ONT + IP Passthrough | BGW320-500 | ISP-owned; WiFi disabled; IP Passthrough → pfSense WAN |
|
|
||||||
|
|
||||||
## pfSense Box Detail
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| CPU | Intel N100 (4-core, 3.4GHz) |
|
|
||||||
| Idle power | ~6W |
|
|
||||||
| Routing throughput | 2–3Gbps |
|
|
||||||
| WireGuard throughput | ~600Mbps |
|
|
||||||
| pfSense version | v25.11 |
|
|
||||||
|
|
||||||
## 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 | v8.4.17 |
|
|
||||||
|
|
||||||
## Proxmox Backup Detail |
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| CPU | Intel i5-4590T (4-core, 2.00GHz) |
|
|
||||||
| RAM | 16 GB |
|
|
||||||
| Boot drive | 256 GB |
|
|
||||||
| Storage | 2 TB |
|
|
||||||
| Proxmox version | v3.4.8 |
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
All LXCs and VMs are backed up via Proxmox Backup Server.
|
|
||||||
|
|
||||||
**Covered:** pihole, auth, infra, monitor, apps, vault, servarr VM, haos VM
|
|
||||||
|
|
||||||
### Schedule
|
|
||||||
|
|
||||||
| Job | Time |
|
|
||||||
|-----|------|
|
|
||||||
| Backup | 2,22:30 |
|
|
||||||
| Prune | 03:00 |
|
|
||||||
| Garbage Collect | sat 18:15 |
|
|
||||||
|
|
||||||
### Retention
|
|
||||||
|
|
||||||
| Keep | Amount |
|
|
||||||
|------|--------|
|
|
||||||
| Last | 3 |
|
|
||||||
| Daily | 13 |
|
|
||||||
| Weekly | 8 |
|
|
||||||
| Monthly | 11 |
|
|
||||||
| Yearly | 2 |
|
|
||||||
+27
-118
@@ -1,130 +1,39 @@
|
|||||||
# Network
|
# 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.
|
## Trust tiers
|
||||||
|
|
||||||
## VLAN Map
|
| Tier | What's on it | Posture |
|
||||||
|
|---|---|---|
|
||||||
|
| Management | Hypervisor, firewall, backup server, network controllers | Most trusted. VPN-only. No outbound unless required. |
|
||||||
|
| Internal services | LXCs and VMs running the app stack | Trusted. Serves clients in adjacent tiers per policy. |
|
||||||
|
| LAN | Personal devices on home WiFi/Ethernet | Trusted. Consumes internal services. |
|
||||||
|
| Work-from-home | Employer-owned laptop | Untrusted lateral. Internet only. Blocked from everything internal, including DNS. |
|
||||||
|
| IoT | Smart devices, cloud-managed appliances | Untrusted. Internet only. Isolated from internal. |
|
||||||
|
| Guest | Visitor WiFi | Untrusted. Internet only. |
|
||||||
|
| DMZ | Internet-facing services | Treated as compromised by default. Tight inbound allowlist to internal. |
|
||||||
|
| VPN (WireGuard) | Authenticated remote clients | Same posture as LAN, plus admin-tier visibility. |
|
||||||
|
|
||||||
| VLAN ID | Name | Subnet | Gateway | DHCP Range | DNS |
|
## Policy
|
||||||
|---------|------|--------|---------|------------|-----|
|
|
||||||
| 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 inter-VLAN. Every cross-tier flow is an explicit allow rule with a reason.
|
||||||
|
- WFH and IoT are restricted to internet only. Nothing internal, including DNS for local hostnames.
|
||||||
|
- Management is kept minimal. Only what runs the lab lives there.
|
||||||
|
- DMZ is one-way. Public services in there can only initiate inward through a firewall-enforced allowlist by source IP + destination port with reverse proxy reinforcing.
|
||||||
|
- Admin only accessible via Management + VPN
|
||||||
|
|
||||||
Default: **deny all inter-VLAN unless explicitly allowed.**
|
## DNS
|
||||||
|
|
||||||
| VLAN | Policy Summary |
|
Three layers:
|
||||||
|------|---------------|
|
|
||||||
| 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
|
1. **Pi-hole** — first hop for clients on most VLANs. Filters ad/tracker domains and holds local A records. Not used by Management hosts or by the WFH VLAN.
|
||||||
|
2. **Unbound on the firewall** — Pi-hole's upstream. Recursive resolver, validates DNSSEC.
|
||||||
|
3. **Cloudflare** — Unbound's upstream when needed.
|
||||||
|
|
||||||
### VLAN 1000 — MGMT
|
The hypervisor (which is the box Pi-hole runs on) statically resolves through the firewall, not Pi-hole. If it didn't, there'd be a circular dependency at boot.
|
||||||
|
|
||||||
| IP | Device |
|
## Internet exposure
|
||||||
|----|--------|
|
|
||||||
| 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
|
Three ports forwarded from WAN:
|
||||||
|
|
||||||
| IP | Device |
|
- HTTP and HTTPS to the DMZ reverse proxy.
|
||||||
|----|--------|
|
- WireGuard to the firewall.
|
||||||
| 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 1 — DMZ
|
|
||||||
|
|
||||||
| IP | Device |
|
|
||||||
|----|--------|
|
|
||||||
| 10.99.0.1 | pfSense DMZ gateway |
|
|
||||||
| 10.99.0.20 | Caddy (DMZ) |
|
|
||||||
| 10.99.0.22 | Gitea (public) |
|
|
||||||
| 10.99.0.23 | Portfolio site |
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# RUNBOOKS
|
|
||||||
|
|
||||||
_stub_
|
|
||||||
+13
-50
@@ -1,60 +1,23 @@
|
|||||||
# Security
|
# Security
|
||||||
|
|
||||||
Security posture — what's exposed, how auth works, update cadence, known debt. See [Network](NETWORK.md) for VLAN isolation details.
|
## Threat model
|
||||||
|
|
||||||
## Internet-Exposed Ports
|
One-person homelab on a residential connection.
|
||||||
|
|
||||||
| Port | Protocol | Destination | Purpose |
|
## Update
|
||||||
|------|----------|-------------|---------|
|
|
||||||
| 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.
|
- Edge components: patched promptly when CVEs land.
|
||||||
|
- Hypervisor and backup server: quarterly review, with security patches applied when needed.
|
||||||
|
- Application LXCs: rolling updates on a regular schedule. certain ones take precent
|
||||||
|
- Container images: re-pulled on the same rolling schedule.
|
||||||
|
|
||||||
## Authentication Layers
|
## Backups
|
||||||
|
|
||||||
| Layer | Mechanism | Coverage |
|
Hypervisor-level backups go to a dedicated backup server. Conservative retentions and backups are verified periofically.The rebuild order is documented.
|
||||||
|-------|-----------|----------|
|
|
||||||
| 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.
|
## Limitations
|
||||||
|
|
||||||
## Secrets Policy
|
This is a learning environment.
|
||||||
|
|
||||||
- No plaintext secrets in any config file committed to the repo
|
- No High Availability - One hypervisor, one firewall
|
||||||
- All secrets referenced by Vaultwarden entry name (e.g., `homelab/pfsense`)
|
- One-person ops
|
||||||
- `.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
|
|
||||||
|
|
||||||
| Item | Risk | Priority | Notes |
|
|
||||||
|------|------|----------|-------|
|
|
||||||
| IoT VLAN rules too broad | Medium | Medium | Currently allows all outbound internet; should restrict to known ports/destinations per device type |
|
|
||||||
| No IDS/IPS | Medium | Low | pfSense supports Suricata — not deployed |
|
|
||||||
| No automated patching | Low | Low | All updates are manual; no Watchtower or unattended-upgrades on most services |
|
|
||||||
| Cloudflare API token scope | Low | Low | Verify token is scoped to DNS-edit only, not zone-admin |
|
|
||||||
| Beszel agent coverage | Low | Low | Confirm all LXCs have Beszel agents deployed |
|
|
||||||
|
|||||||
+77
-71
@@ -1,91 +1,97 @@
|
|||||||
# Services
|
# 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.
|
## Identity & access
|
||||||
|
|
||||||
## Status Key
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Authentik | SSO for internal services, OIDC where supported + caddy forward auth otherwis |
|
||||||
|
| Pi-hole | LAN DNS, ad blocking + source of truth for internal hostnames |
|
||||||
|
| WireGuard | remote access |
|
||||||
|
|
||||||
| Symbol | Meaning |
|
## Reverse proxy & TLS
|
||||||
|--------|---------|
|
|
||||||
| ✅ | Running, healthy |
|
|
||||||
| ⚠️ | Running, needs attention |
|
|
||||||
| 🔴 | Down / broken |
|
|
||||||
| 🚧 | In progress |
|
|
||||||
| ➖ | Decommissioned |
|
|
||||||
|
|
||||||
## Core Network (VLAN 1000/1010/1020)
|
Two Caddy instances:
|
||||||
|
|
||||||
| Service | IP | Port | VLAN | URL | Status | Notes |
|
- **Internal Caddy** fronts everything internal. LAN or VPN only.
|
||||||
|---------|-----|------|------|-----|--------|-------|
|
- **DMZ Caddy** fronts the public services. Lives on its own VLAN with a firewall-enforced allowlist into internal.
|
||||||
| pfSense | 10.1.0.1 / 10.0.0.1 | 443 | LAN/MGMT | https://pfsense.lerkolabs.com | ✅ | Firewall, DHCP, WireGuard VPN |
|
|
||||||
| Omada Switch | 10.0.0.2 | 443 | MGMT | https://switch.lerkolabs.com | ✅ | Managed switch, VLAN config |
|
|
||||||
| AT&T Gateway | 192.168.1.254 | 80 | — | http://192.168.1.254 | ✅ | IP Passthrough only, WiFi disabled |
|
|
||||||
| Pi-hole | 10.2.0.11 | 80/53 | 1020 | https://pihole.lerkolabs.com | ✅ | 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 | https://auth.lerkolabs.com | ✅ | SSO — OIDC + forward auth |
|
|
||||||
| Proxmox | 10.2.0.10 | 8006 | 1020 | https://proxmox.lerkolabs.com | ✅ | Hypervisor |
|
|
||||||
|
|
||||||
## Observability (monitor LXC — 10.2.0.51)
|
Both use Cloudflare DNS-01 for ACME, which lets internal-only services get valid public certs without being exposed for issuance.
|
||||||
|
|
||||||
| Service | URL | Notes |
|
## Productivity & knowledge
|
||||||
|---------|-----|-------|
|
|
||||||
| Grafana | https://grafana.lerkolabs.com | Dashboards, alerting |
|
|
||||||
| Victoria Metrics | — | Metrics storage |
|
|
||||||
| Beszel | — | Container + host monitoring |
|
|
||||||
|
|
||||||
## Productivity Apps (apps LXC — 10.2.0.60)
|
| Service | What it replaces |
|
||||||
|
|---|---|
|
||||||
|
| Outline | notion |
|
||||||
|
| Vikunja | todoist / asana |
|
||||||
|
| Hoarder | pocket / raindrop |
|
||||||
|
| Memos | apple nnotes |
|
||||||
|
| FreshRSS | feedly |
|
||||||
|
| Bytestash | gist / pastebin |
|
||||||
|
| Filebrowser | dropbox |
|
||||||
|
| Baikal | iCloud calendar/contacts (CalDAV / CardDAV) |
|
||||||
|
|
||||||
All behind Authentik SSO.
|
## Money
|
||||||
|
|
||||||
| Service | URL | Auth | Purpose |
|
| Service | What it replaces |
|
||||||
|---------|-----|------|---------|
|
|---|---|
|
||||||
| Outline | https://outline.lerkolabs.com | OIDC | Team wiki |
|
| Actual Budget | YNAB |
|
||||||
| Vikunja | https://tasks.lerkolabs.com | OIDC | Task management |
|
| Ghostfolio | personal capital |
|
||||||
| Ghostfolio | https://finance.lerkolabs.com | Forward auth | Portfolio tracking |
|
|
||||||
| Hoarder | https://hoarder.lerkolabs.com | Forward auth | Bookmark manager |
|
|
||||||
| Grist | https://grist.lerkolabs.com | Forward auth | Spreadsheets / data |
|
|
||||||
| Actual Budget | https://budget.lerkolabs.com | Forward auth | Personal budgeting |
|
|
||||||
| FreshRSS | https://rss.lerkolabs.com | Forward auth | RSS reader |
|
|
||||||
| Memos | https://memos.lerkolabs.com | Forward auth | Quick notes |
|
|
||||||
| Traggo | https://time.lerkolabs.com | Forward auth | Time tracking |
|
|
||||||
| Baikal | https://dav.lerkolabs.com | Forward auth | CalDAV / CardDAV |
|
|
||||||
| Glance | https://glance.lerkolabs.com | Forward auth | Homepage dashboard |
|
|
||||||
| Filebrowser | https://files.lerkolabs.com | 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).
|
## Operations
|
||||||
|
|
||||||
## Secrets (vault LXC — 10.2.0.21)
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Grist | lightweight excel type |
|
||||||
|
| Glance | personal homepage |
|
||||||
|
| Traggo | time tracking |
|
||||||
|
|
||||||
| Service | URL | Notes |
|
## Media
|
||||||
|---------|-----|-------|
|
|
||||||
| Vaultwarden | https://vault.lerkolabs.com | Isolated LXC — not shared with apps |
|
|
||||||
|
|
||||||
## Media (servarr VM)
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Plex | mdia library (legacy clients) |
|
||||||
|
| Jellyfin | media library (primary) |
|
||||||
|
| *arr stack | library automation |
|
||||||
|
| qBittorrent | Downloads |
|
||||||
|
| Immich | photo backup and viewing |
|
||||||
|
|
||||||
| Service | Purpose |
|
## Home / IoT
|
||||||
|---------|---------|
|
|
||||||
| 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 1 — 10.99.0.0/24)
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Home Assistant OS | home automation hub |
|
||||||
|
|
||||||
| Service | IP | URL | Status | Notes |
|
## Secrets
|
||||||
|---------|----|-----|--------|-------|
|
|
||||||
| Caddy (DMZ) | 10.99.0.20 | — | ✅ | Public reverse proxy |
|
|
||||||
| Gitea | 10.99.0.22 | https://gitea.lerkolabs.com | ✅ | Public Git |
|
|
||||||
| Portfolio | 10.99.0.23 | https://lerkolabs.com | ✅ | Personal site |
|
|
||||||
|
|
||||||
## Access Matrix
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Vaultwarden | bitwarden-compatible password manager *Planned, not deployed yet |
|
||||||
|
|
||||||
|
## Bots & automation
|
||||||
|
|
||||||
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Vocard | discord music bot |
|
||||||
|
| MonitorRSS | rss-to-discord feed |
|
||||||
|
| ntfy | push notifications for ops alerts |
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
| Service | What it does |
|
||||||
|
|---|---|
|
||||||
|
| Victoria Metrics | time-series store |
|
||||||
|
| Grafana | dashboards |
|
||||||
|
| Beszel | host metrics |
|
||||||
|
| Uptime Kuma | uptime checks |
|
||||||
|
|
||||||
|
## Public services
|
||||||
|
|
||||||
|
A small set behind the DMZ reverse proxy on a VLAN with no inbound to internal.
|
||||||
|
|
||||||
|
| Service | Why it's public |
|
||||||
|
|---|---|
|
||||||
|
| Portfolio | it's a portfolio |
|
||||||
|
| Self-hosted Git | so you can read this |
|
||||||
|
| SSO endpoint | required for the OIDC flow on the Discord bot dashboard. the firewall is enabled so that the public proxy can only reach this one internal backend |
|
||||||
|
| Discord bot dashboard | so my friends can use pick tunes. authentik forward auth gates it |
|
||||||
|
|
||||||
| Service | LAN | Homelab | Guest | IoT | WFH | VPN |
|
|
||||||
|---------|-----|---------|-------|-----|-----|-----|
|
|
||||||
| pfSense Web GUI | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
||||||
| Pi-hole Admin | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
||||||
| All *.lerkolabs.com | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
||||||
| Proxmox | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
||||||
| Internet | ✅ | limited | ✅ | ✅ | ✅ | optional |
|
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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:18-alpine
|
|
||||||
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:7-alpine
|
|
||||||
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/*/
|
|
||||||
```
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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=2026.2.1
|
|
||||||
|
|
||||||
AUTHENTIK_LOG_LEVEL=info
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod 600 /opt/docker/authentik/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### docker-compose.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
postgresql:
|
|
||||||
image: docker.io/library/postgres:16-alpine
|
|
||||||
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:-2026.2.1}
|
|
||||||
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:-2026.2.1}
|
|
||||||
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://<service>.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/<slug>/.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
|
|
||||||
```
|
|
||||||
-224
@@ -1,224 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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:2-builder AS builder
|
|
||||||
RUN xcaddy build \
|
|
||||||
--with github.com/caddy-dns/cloudflare
|
|
||||||
|
|
||||||
FROM caddy:2-alpine
|
|
||||||
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
### .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CLOUDFLARE_API_TOKEN=<token from Vaultwarden: homelab/cloudflare-api>
|
|
||||||
```
|
|
||||||
|
|
||||||
```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 your@email.com
|
|
||||||
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 <username>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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"
|
|
||||||
```
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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: <from Authentik OIDC provider>
|
|
||||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: <from Authentik OIDC provider>
|
|
||||||
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
|
|
||||||
```
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
| 1 | (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 (1) | 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
|
|
||||||
```
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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=<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 `<servarr-ip>` with the VM's IP.
|
|
||||||
|
|
||||||
```caddyfile
|
|
||||||
# Example — Plex handles its own auth, no forward auth needed
|
|
||||||
plex.lerkolabs.com {
|
|
||||||
reverse_proxy <servarr-ip>:32400
|
|
||||||
}
|
|
||||||
|
|
||||||
# *arr services — protect with Authentik forward auth
|
|
||||||
sonarr.lerkolabs.com {
|
|
||||||
import authentik_forward_auth
|
|
||||||
reverse_proxy <servarr-ip>: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
|
|
||||||
```
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# 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 America/Chicago
|
|
||||||
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=<generated_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
|
|
||||||
```
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# 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 = <client private key>
|
|
||||||
Address = 10.200.0.X/24
|
|
||||||
DNS = 10.2.0.11
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <server public key from pfSense>
|
|
||||||
Endpoint = <WAN IP or DDNS hostname>: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
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user