No description
  • Jinja 42.9%
  • Python 36.3%
  • HCL 9.9%
  • Shell 3.6%
  • HTML 3%
  • Other 4.3%
Find a file
2026-08-24 18:16:22 +00:00
.forgejo/workflows add-ssh-key: declare workflow_dispatch input type=string 2026-05-07 08:56:38 +02:00
directory_services add Stockpot service; WUD tag filtering across compose templates 2026-04-09 19:19:17 +02:00
docs code-server: add remote dev workstation VM and role 2026-07-12 21:49:25 +02:00
iac fix(playify): pin compatible dependencies 2026-08-24 18:04:04 +00:00
revamp_plans_jan2026 add Matrix, Gryt, Fluxer, Forgejo, Stoat, Firefly III; LibreNMS/SNMP agents; Traefik routes 2026-03-14 21:04:42 +01:00
scripts iac: remove retired homelab services 2026-08-24 17:27:01 +00:00
.gitignore chore: gitignore graphify knowledge-graph output 2026-07-22 14:39:36 +02:00
AGENTS.md fix(playify): add YouTube PO token provider 2026-08-24 17:54:07 +00:00
bucharest_move_plan.md add Stockpot service; WUD tag filtering across compose templates 2026-04-09 19:19:17 +02:00
HOMELAB_INTEGRATION.md docs: remove retired services from inventory 2026-08-24 17:27:04 +00:00
README.md docs: remove retired services from inventory 2026-08-24 17:27:04 +00:00

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
  • *.home to *.infra rename 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, and site.yml
  • Dedicated: iac/terraform/dedicated, inventory.dedicated.yml, and dedicated.yml
  • Authoritative DNS: inventory.dns.yml and dns.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.Minor Docker 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.include with 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 (not blockinfile) — write to /tmp/ then cp to /etc/pve/lxc/ (pmxcfs FUSE bug)
  • LXC search domain: every LXC sets searchdomain: localdomain to prevent the PVE host inheriting home.step1.ro into LXC /etc/resolv.conf and breaking bare-name SSH
  • Bare-hostname DNS: every host has a host-record in PiHole's 05-hosts.conf so ssh 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