- Jinja 42.9%
- Python 36.3%
- HCL 9.9%
- Shell 3.6%
- HTML 3%
- Other 4.3%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| directory_services | ||
| docs | ||
| iac | ||
| revamp_plans_jan2026 | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| bucharest_move_plan.md | ||
| HOMELAB_INTEGRATION.md | ||
| README.md | ||
Homelab
Infrastructure configuration for a 3-node home Proxmox VE cluster, a dedicated Proxmox server, two external VPS providers, and 50+ self-hosted services. Terraform manages selected VMs and LXCs. Ansible configures managed hosts. OPNsense, Alwyzon, Maghost, and parts of the dedicated environment retain manual ownership.
Pending / TODO
- Home Assistant + n8n: planned for services-internal (201), roles not yet written
- SoulSync testing: installed and working but not thoroughly tested; qobuz-dl remains a fallback
*.hometo*.infrarename cleanup: remove legacy redirect routers and Authelia redirect URIs after the grace window
Architecture
Public Internet
+--------------+---------------+
| |
ns1.step1.ro, 89.39.64.51 ns2.step1.ro, 45.8.47.198
OPNsense -> Knot primary Maghost Knot secondary
| signed AXFR/IXFR, no DNSSEC keys
|
+---------+---------------- Dedicated Proxmox ----------------+
| OPNsense .47, public VIPs .48-.51, VLANs 10/20/30/40/ |
| 50/55/60/70/90, Traefik, VertexWatch, resolver, DNS, |
| and repository-scoped Forgejo runner VM 112 |
+-------------------------------------------------------------+
Alwyzon VPS, 205.147.200.16
CrowdSec, UFW, WireWarp server
|
WireWarp 10.21.0.0/24
|
Gateway LXC 110 -> home Traefik LXC 111 -> home services
|
+---------+---------------- Home Proxmox cluster -------------+
| px01 core and resolver-home 107 | px02 media | px03 apps |
| FreeIPA VM | Pi-hole client DNS | global Forgejo runner 207 |
+-------------------------------------------------------------+
Home public traffic: Client to Alwyzon VPS, then WireWarp, Gateway LXC, home Traefik, and the backend. DNAT preserves the client address. SSH access uses Warpgate LXC 115.
Dedicated public traffic: Client to an OPNsense public VIP, then a scoped destination NAT rule and the target VLAN. Web traffic on 89.39.64.49 reaches dedicated Traefik at 10.50.40.10.
Public DNS: RoTLD delegates step1.ro to the dedicated primary and Maghost secondary. Knot signs the zone on the primary. The secondary receives signed transfers and has no DNSSEC private keys.
Split-horizon DNS: PiHole resolves all *.step1.ro (*.step1.ro, *.home.step1.ro, *.infra.step1.ro) to 192.168.20.111 (Traefik) on LAN, so internal clients skip the VPS round-trip. Bare-hostname records (pihole, media-management, etc.) resolve directly to LXC IPs for ssh root@<host>.
Recursive DNS: Pi-hole remains the single client-facing home DNS endpoint. It uses both Knot resolvers, including resolver-home LXC 107 and resolver-dedicated LXC 114.
Cluster Nodes
| Node | IP | Role |
|---|---|---|
| px01 | 192.168.20.11 | Core infrastructure (gateway, traefik, authelia, vaultwarden, warpgate, pihole, infra-services, OpenCodex, FreeIPA) |
| px02 | 192.168.20.12 | Media, heavy applications, development hosts, and global Forgejo runner LXC 207 |
| px03 | 192.168.20.13 | Monitoring and shared internal and public services |
| TrueNAS | 192.168.20.20 | NFS storage — mounted at Proxmox host level, bind-mounted into LXCs |
| VPS | 205.147.200.16 | Public entry point, WireWarp tunnel server, CrowdSec |
| px-vie-n01 | private management path | Dedicated Proxmox server behind OPNsense |
| OPNsense | 89.39.64.47 | Dedicated firewall, VLAN gateways, NAT, and public VIP ownership |
| Maghost | 45.8.47.198 | Independent authoritative DNS secondary for ns2.step1.ro |
Full Service Inventory
Core Infrastructure (px01)
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| PiHole | 105 | 192.168.20.5 | LXC (unpriv) | pihole.infra.step1.ro |
DNS server, split-horizon for *.step1.ro |
| Resolver Home | 107 | 192.168.20.7 | LXC (unpriv) | None | Private Knot Resolver used by Pi-hole |
| Gateway | 110 | 192.168.20.110 | LXC (priv) | — | WireWarp tunnel endpoint, IP forwarding |
| Traefik | 111 | 192.168.20.111 | LXC (unpriv) | traefik.infra.step1.ro |
Reverse proxy, TLS termination, wildcard certs |
| Authelia | 112 | 192.168.20.112 | LXC (unpriv) | auth.step1.ro |
SSO/OIDC provider backed by FreeIPA |
| Vaultwarden | 113 | 192.168.20.113 | LXC (unpriv) | passwords.step1.ro |
Password manager (Bitwarden-compatible) |
| Warpgate | 115 | 192.168.20.115 | LXC (unpriv) | warpgate.infra.step1.ro, ssh.home.step1.ro |
Identity-aware SSH/HTTPS bastion (OIDC via Authelia) |
| Infra Services | 116 | 192.168.20.116 | LXC (unpriv) | Various *.infra.step1.ro |
Semaphore, Portainer, WUD, WireWarp control |
| OpenCodex | 117 | 192.168.20.117 | LXC (unpriv) | opencodex.infra.step1.ro |
Shared LLM provider proxy and dashboard |
| FreeIPA | 121 | 192.168.20.21 | VM (Rocky 9) | ipa.home.step1.ro |
LDAP/Kerberos identity management, realm HOME.STEP1.RO |
Infra Services (LXC 116):
| App | Port | Domain | Purpose |
|---|---|---|---|
| Semaphore | 3000 | semaphore.infra.step1.ro |
Web UI for running this repo's Ansible playbooks (OIDC, SQLite, ansible 11.x) |
| Portainer | 9000 | portainer.infra.step1.ro |
Docker management UI |
| What's Up Docker | 3001 | wud.infra.step1.ro |
Container update notifications |
| WireWarp Control | 8100 | wirewarp.infra.step1.ro |
WireWarp tunnel management UI |
Media Stack (px02)
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| Media Management | 150 | 192.168.20.150 | LXC (priv) | Various *.home.step1.ro |
Sonarr, Radarr, Prowlarr, Bazarr, qBittorrent, Autobrr, Lazylibrarian, Listenarr, Seerr, Maintainerr, SoulSync, FlareSolverr, Recyclarr, Subgen |
| Media Playback | 151 | 192.168.20.151 | LXC (unpriv) | media.step1.ro, music.step1.ro |
Jellyfin (Intel GPU HW transcode), Feishin (music player), Tdarr, Audiobookshelf |
Media management services:
| App | Port | Domain | Auth | Purpose |
|---|---|---|---|---|
| Sonarr | 8989 | sonarr.home.step1.ro |
Authelia | TV show management |
| Radarr | 7878 | radarr.home.step1.ro |
Authelia | Movie management |
| Prowlarr | 9696 | prowlarr.home.step1.ro |
Authelia | Indexer management |
| Bazarr | 6767 | bazarr.home.step1.ro |
Authelia | Subtitle management |
| qBittorrent | 8080 | qbit.home.step1.ro |
Authelia | Torrent client (VueTorrent UI) |
| Maintainerr | 6246 | maintainerr.home.step1.ro |
Authelia | Jellyfin/*arr media retention automation |
| Autobrr | 7474 | autobrr.home.step1.ro |
Authelia | Automated torrent filtering |
| Lazylibrarian | 5299 | lazylibrarian.home.step1.ro |
Authelia | Book/audiobook management |
| Listenarr | 4545 | listenarr.home.step1.ro |
Authelia | Podcast management |
| Seerr | 5055 | catalog.step1.ro |
OIDC | Media request portal |
| SoulSync | 8008 | soulsync.home.step1.ro |
Authelia | Music discovery & download |
| Subgen | 8090 | — | — | Whisper AI subtitle generation |
| FlareSolverr | 8191 | — | — | Cloudflare challenge solver |
| Recyclarr | — | — | — | Quality profile sync (manual) |
Monitoring (px03)
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| Monitoring | 200 | 192.168.20.200 | LXC (unpriv) | Various *.infra.step1.ro |
Uptime Kuma, LibreNMS + full Grafana observability stack |
| App | Port | Domain | Purpose |
|---|---|---|---|
| Uptime Kuma | 3001 | uptime.infra.step1.ro / status.step1.ro |
Uptime monitoring + public status page |
| LibreNMS | 8000 | librenms.infra.step1.ro |
Network monitoring (SNMP + check_mk agents) |
| Grafana | 3000 | grafana.infra.step1.ro |
Dashboards (OIDC via Authelia, ~25 dashboards: fleet overview, host drilldown, per-service, Proxmox cluster, Flint router, etc.) |
| Loki | 3100 | loki.infra.step1.ro |
Centralized logs (Alloy fleet shipper → 30-day retention) |
| Prometheus | 9090 | prometheus.infra.step1.ro |
Metrics + alerting (90d / 5GB cap, 10+ alert rules + 3 recording rules) |
Alerting runs on LXC 209 at 192.168.20.209. Alertmanager routes alerts to Alerta. Alerta publishes notifications through the authenticated ntfy relay on the Alwyzon VPS.
Exporters fleet-wide: node_exporter (all LXCs/VPS/PVE), cAdvisor (all Docker hosts on :18080), smartctl_exporter (PVE nodes + TrueNAS), custom TrueNAS pool/dataset exporter, pve-exporter (Proxmox cluster API), restic-exporter (one per backed-up host), pihole-exporter, traefik per-router metrics, speedtest-exporter, crowdsec (VPS, via WG tunnel), Flint router lua node_exporter.
Internal Services (px03)
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| Services Internal | 201 | 192.168.20.201 | LXC (unpriv) | Various *.home.step1.ro |
Homepage, Paperless-ngx, Stirling-PDF, SparkyFitness, Proton Mail Bridge |
| App | Port | Domain | Purpose |
|---|---|---|---|
| Homepage | 3000 | dash.home.step1.ro |
Dashboard |
| Zeitmark | — | zeitmark.home.step1.ro |
Timestamping |
| Paperless-ngx | 8000 | paperless.home.step1.ro |
Document management (custom build w/ mail-parser plugin for single-PDF mail ingest) |
| Stirling-PDF | 8080 | stirling-pdf.home.step1.ro |
PDF toolbox |
| SparkyFitness | 3004 | fitness.home.step1.ro |
Health and fitness tracking |
| Proton Mail Bridge | 1143/1025 | — (loopback only) | IMAP/SMTP gateway for Paperless to ingest Proton mailboxes (with imap-relay TLS sidecar) |
Public Services (px03)
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| Services Public | 202 | 192.168.20.202 | LXC (unpriv) | Various *.step1.ro |
Forgejo, PrivateBin, Stockpot, password reset, Discord bot, landing pages, static assets |
| App | Port | Domain | Purpose |
|---|---|---|---|
| Forgejo | 3000 | git.step1.ro |
Git forge (SQLite, SSH on port 2222, NFS-backed via TrueNAS) |
| PrivateBin | 8080 | paste.step1.ro |
Encrypted pastebin |
| Stockpot | — | stockpot.step1.ro |
Inventory tracker |
| Password Reset | 5001 | reset.step1.ro |
FreeIPA self-service password reset |
| Red Discord Bot | 42356 | discordbot.step1.ro |
Modular Discord bot |
| Playify | — | lk.step1.ro (LiveKit) |
Discord voice channel utility |
| Landing | — | step1.ro, www.step1.ro |
Landing pages |
| Static Assets | 8082 | assets.step1.ro |
Custom files (Authelia background, etc.) |
Communication, Development, and CI
| Service | VMID | IP | Type | Domain | Description |
|---|---|---|---|---|---|
| Nextcloud | 203 | 192.168.20.203 | LXC (unpriv) | cloud.step1.ro |
Nextcloud AIO (manages own sub-containers) |
| Dev Docker | 204 | 192.168.20.204 | LXC (unpriv) | — | Throwaway sandbox for ad-hoc Docker workloads |
| Forgejo Runner | 207 | 192.168.20.207 | LXC (priv) | — | Forgejo Actions CI/CD runner |
| GNS3 | 220 | 192.168.20.220 | VM (Ubuntu 22.04) | gns3.home.step1.ro |
Network emulation lab |
Dedicated Server
| Service | VMID | Private address | Public path | Ownership |
|---|---|---|---|---|
| OPNsense | 100 | VLAN gateways | 89.39.64.47 and VIPs .48 to .51 |
Manual UI configuration |
| DayZ | 101 | 10.50.90.10 |
Game ports on 89.39.64.48 |
Existing VM |
| Arma Reforger | 102 | 10.50.90.11 |
Game ports on 89.39.64.48 |
Existing VM |
| Dedicated Traefik | 110 | 10.50.40.10 |
TCP 80 and 443 on 89.39.64.49 |
Terraform and Ansible |
| VertexWatch | 111 | 10.50.55.20 |
Dedicated Traefik | Terraform and Ansible |
| Repository Forgejo runner | 112 | 10.50.70.10 |
Private outbound worker | Terraform and Ansible |
| Authoritative DNS primary | 113 | 10.50.30.53 |
UDP and TCP 53 on 89.39.64.51 |
Terraform and Ansible |
| Dedicated resolver | 114 | 10.50.20.7 |
Private only | Terraform and Ansible |
The global runner is home LXC 207. Dedicated VM 112 is a separate runner for repository workloads that require the dedicated Rust label.
Dedicated VLANs use matching 10.50.<vlan>.0/24 subnets: management 10, resolver 20, authority 30, ingress 40, customer 50, applications 55, operations 60, CI 70, and games 90.
Network Map
Subnet: 192.168.20.0/24 Gateway: 192.168.20.254 (Flint router)
VMID = last octet of IP (exceptions: PiHole 105 -> .5, FreeIPA 121 -> .21)
.5 PiHole (DNS) .113 Vaultwarden .203 Nextcloud
.7 Resolver Home .114 .204 Dev Docker
.11 px01 .114 .204 Dev Docker
.12 px02 .115 Warpgate (bastion) .206
.13 px03 .116 Infra Services .207 Forgejo Runner
.20 TrueNAS (NFS) .117 OpenCodex .220 GNS3 (VM)
.21 FreeIPA .121 FreeIPA (VM) .150 Media Management
.110 Gateway (tunnel) .151 Media Playback
.111 Traefik (proxy) .200 Monitoring
.112 Authelia (SSO) .201 Services Internal
.202 Services Public
WireWarp tunnel (Gateway↔VPS): 10.21.0.0/24
VPS=10.21.0.1, Gateway=10.21.0.3
Monitoring LXC keeps a local route via 192.168.20.110 for VPS exporter
scrapes, so router resets do not wipe Prometheus' tunnel path.
Domains
| Pattern | Scope | Cert | DNS |
|---|---|---|---|
*.step1.ro |
Public services (Forgejo, PrivateBin, Authelia, Jellyfin, Nextcloud, etc.) | Knot RFC 2136 DNS-01 | Signed Knot zone on ns1 and ns2; RoTLD DS is live and validated; Pi-hole overrides selected names internally |
*.home.step1.ro |
Internal user apps (Homepage, Paperless, *arr stack, etc.) | Knot RFC 2136 DNS-01 | Pi-hole only at 192.168.20.111 |
*.infra.step1.ro |
Internal infra tooling (Pi-hole, Traefik dashboard, Warpgate, Grafana, Semaphore, Portainer, Proxmox UI, etc.) | Knot RFC 2136 DNS-01 | Pi-hole only at 192.168.20.111 |
The *.home → *.infra split is intentional: user-app churn (restart, restic, apt-upgrade) doesn't bounce the tooling that watches/runs them. During the rename grace window (until 2026-06-01), every old *.home.step1.ro infra FQDN keeps working as a 301 redirect.
Security
Identity: FreeIPA (realm HOME.STEP1.RO, base DN dc=home,dc=step1,dc=ro) provides LDAP and Kerberos. Authelia acts as an OIDC provider on top of FreeIPA. SSO groups follow the pattern sso_<service>_<role>.
Access control (Traefik middlewares):
secured— LAN + VPN allowlist only (192.168.0.0/16,10.100.0.0/24), plus security headers. For services with their own auth.secured-auth— same allowlist + Authelia forward auth. For internal services using SSO.authelia— Authelia forward auth only (no IP restriction). For public services using OIDC.default-headers— security headers only. For fully public services.
SSH access: All target hosts in [warpgate_nodes] are accessed via Warpgate (LXC 115) — ssh <warpgate-user>:<target>@ssh.home.step1.ro. RBAC via FreeIPA groups (sso_warpgate_admins / sso_warpgate_users). Target hosts trust Warpgate's outbound SSH client pubkey (enrolled by warpgate_ssh_target role). Direct break-glass SSH still works via each host's own sshd.
VPS hardening: CrowdSec (collaborative threat intelligence — local detections + ~33k passively-imported community blocklist), UFW firewall (interface-bound rules — public eth0 stays default-deny while wg0 carries monitoring scrape traffic), SSH key-only auth. NAT rules deliberately skip MASQUERADE on wg0 so real client IPs flow through to Traefik. CrowdSec Prometheus metrics surfaced on the fleet overview dashboard.
Automatic updates: Per-host unattended-upgrades (Debian) / dnf-automatic (Rocky) install security updates passively, no auto-reboot. Manual full upgrade pass is wave-serialized (leaves → mid-tier → auth/edge → critical infra) to keep DNS/proxy/auth available throughout. Forgejo Actions runs the weekly upgrade workflow on Sundays.
Secrets: Ansible Vault (group_vars/all/vault.yml) and Terraform vars (terraform.tfvars) — both gitignored. See .example files for reference. Vault is intentionally not encrypted; do not pass --ask-vault-pass.
Tech Stack
| Layer | Tool | Notes |
|---|---|---|
| Hypervisor | Proxmox VE | Home cluster plus dedicated server, LXC and VM workloads |
| Provisioning | Terraform | Home root uses bpg/proxmox ~> 0.70; dedicated root uses ~> 0.111.0 |
| Configuration | Ansible | Separate home, dedicated, and authoritative DNS inventories |
| Containers | Docker + Compose | Deployed via Ansible templates |
| Reverse Proxy | Traefik v3.6 | File + Docker providers, wildcard TLS, per-router metrics on :8082 |
| DNS | Pi-hole and Knot | Pi-hole is the client endpoint; Knot Resolver provides recursion; Knot DNS serves the signed public zone |
| Identity | FreeIPA | Rocky Linux 9 VM, LDAP/Kerberos |
| SSO | Authelia | OIDC provider, forward auth |
| SSH bastion | Warpgate | Identity-aware SSH/HTTPS bastion, OIDC via Authelia |
| Tunnel | WireWarp | Self-hosted WireGuard-based tunnel project (manages the Gateway↔VPS link) |
| Storage | TrueNAS | NFS shares, mounted at Proxmox host level |
| CI/CD | Forgejo Actions | Global runner on home LXC 207 and repository-scoped runner on dedicated VM 112 |
| Metrics | Prometheus | 90d / 5GB cap, alert + recording rules in roles/monitoring/files/rules/ |
| Logs | Loki + Alloy | Fleet-wide log aggregation (30d retention), per-host Alloy on Docker hosts + journald |
| Dashboards | Grafana | OIDC, ~25 dashboards including custom Fleet Overview / Host Drilldown / per-service |
| Uptime | Uptime Kuma | Public status page at status.step1.ro |
| SNMP | LibreNMS | Network device + Linux-host visibility |
| Updates | What's Up Docker (WUD) | Container update notifications, host-managed via auto_updates role |
| Ansible UI | Semaphore | Web UI for ad-hoc playbook runs (OIDC) |
Deployment
See iac/DEPLOY.md for the current commands and order. It covers all three paths:
- Home:
iac/terraform,inventory.ini, andsite.yml - Dedicated:
iac/terraform/dedicated,inventory.dedicated.yml, anddedicated.yml - Authoritative DNS:
inventory.dns.ymlanddns.yml
Always pass --private-key ~/.ssh/homelab_iac to Ansible. The vault is intentionally unencrypted. Do not use --ask-vault-pass.
Directory Structure
iac/
terraform/
main.tf # Provider config (API token + SSH for root@pam ops)
containers.tf # All LXC definitions (for_each pattern + local-exec mounts)
ipa.tf # FreeIPA VM (Rocky Linux 9)
variables.tf # Input variables with defaults
backend.tf # State backend (local)
terraform.tfvars.example
dedicated/ # Independent dedicated server Terraform root and state
ansible/
site.yml # Master playbook (40+ plays, tag-based targeting, wave-serialised apt-upgrade)
inventory.ini # Host inventory (per-host SSH user/key, update_wave_N groups)
dedicated.yml # Dedicated services playbook
inventory.dedicated.yml
dns.yml # Public authoritative DNS playbook
inventory.dns.yml
ansible.cfg # Config (pipelining, fact caching)
group_vars/all/
vars.yml.example # Variable reference (~400 lines)
vault.yml # Secrets (gitignored, NOT encrypted)
host_vars/ # authelia, infra-services, ipa, monitoring/, nextcloud, services-internal, services-public, vaultwarden, vps
roles/ # 39 roles — one per service or cross-cutting concern
alloy_agent/ audiobookshelf/ authelia/ auto_updates/
cadvisor/ common/ dev_docker/
downloads/ forgejo_runner/ freeipa/
gns3/ jellyfin/ librenms_agent/ monitoring/
nextcloud/ node_exporter/ paperless_mail_merger/
pihole/ playify/ protonmail_bridge/ redbot/
restic/ services/ services_infra/ services_internal/
smartctl_exporter/ snmp/ soulsync/ traefik/
vaultwarden/ vps/ warpgate/ warpgate_ssh_target/
wirewarp_client/ wirewarp_control/ wirewarp_server_agent/
guides/ # Long-form architecture + setup guides
DEPLOY.md # Step-by-step deployment guide
DEDICATED-SERVER.md # Dedicated network, OPNsense, and recovery runbook
MAGHOST.md # Authoritative DNS secondary runbook
scripts/
flint-setup.sh # Idempotent Flint router opkg + Prometheus exporter setup
truenas-deploy.sh # Deploy TrueNAS SMART + pool/dataset exporters
truenas-setup.sh # Remote TrueNAS exporter installer
grafana-push.sh # Fast inner-loop dashboard push (~0.2s vs ~10s Ansible)
revamp_plans_jan2026/ # Architecture docs and roadmap
Conventions
- VMID = last octet of IP address (documented exceptions only)
- Version pinning:
Major.MinorDocker tags (e.g.,:3.6), never:latest. Exceptions for linuxserver.io (full build tag), Portainer (semver), LibreNMS (YY.MM.patch), Nextcloud AIO (latest), Red Discord Bot (full) - WUD tag filtering: every versioned image carries
wud.tag.includewith anchored regex (^\d+\.\d+\.\d+$$or variant) so RC/nightly tags don't masquerade as updates - Unprivileged LXCs by default — privileged only when kernel access required (documented in
containers.tf) - Persistent volumes: compose files at
/opt/<service>/docker-compose.yml, app data at/opt/appdata/<service>/ - NFS mounts: at Proxmox host level via Datacenter Storage, bind-mounted into LXCs — never inside containers. NFS-backed dirs in unprivileged LXCs created via
delegate_to: <pve-node>to land correct ownership - LXC conf edits:
lineinfile(notblockinfile) — write to/tmp/thencpto/etc/pve/lxc/(pmxcfs FUSE bug) - LXC search domain: every LXC sets
searchdomain: localdomainto prevent the PVE host inheritinghome.step1.rointo LXC/etc/resolv.confand breaking bare-name SSH - Bare-hostname DNS: every host has a
host-recordin PiHole's05-hosts.confsossh root@<name>works LAN-wide - SSO groups:
sso_<service>_<role>pattern in FreeIPA - Single-file Docker bind mounts pin inodes: rewriting the host file leaves the container reading stale config. For Prometheus + similar, force-recreate the container after a config change OR use a directory bind mount