A personal homelab managed as code: Proxmox-based hyper-converged private cloud, Home Assistant smart home automation, and a collection of utility scripts.
This repository manages a hyper-converged private cloud built on Proxmox VE. The lab is treated as a software-defined datacenter, with Ansible providing end-to-end automation of infrastructure, networking, and services.
- Orchestrator: Ansible (roles & playbooks).
- Hypervisor: Proxmox VE — 3-node cluster (
pve01,pve02,pve03). - Networking:
- Physical: LAN (
vmbr0,192.168.86.0/24). - SDN: VXLAN-based private network (
172.16.0.0/24) for isolated service communication. - Ingress: Tailscale Gateway (
172.16.0.1) for secure remote access and NAT — also acts as the SDN VNet gateway.
- Physical: LAN (
- Identity: Authentik (
auth.chi.lab.amd-e.com) with OIDC integration for Proxmox SSO. - Storage: ZFS (NVMe) with automated replication for High Availability.
| Service | Hostname | Private IP | Public/LAN | Description |
|---|---|---|---|---|
| DNS | dns-01 |
172.16.0.10 |
- | Internal Bind9 authority (chi.lab.amd-e.com) |
| NTP | ntp-01 |
172.16.0.11 |
- | Chrony, syncs against time.nist.gov |
| Squid Proxy | proxy-01 |
172.16.0.12 |
- | Outbound caching proxy |
| Identity | authentik |
172.16.0.20 |
100.x.y.z (TS) |
SSO provider; access via load balancer |
| Load Balancer | lb-01 |
172.16.0.30 |
LAN/DHCP | Nginx Layer 7 proxy for the cluster and services |
| Mail Forwarder | mail-01 |
172.16.0.40 |
- | Postfix → iCloud SMTP relay for alert email |
| VictoriaMetrics | vm-01 |
172.16.0.42 |
- | TSDB + blackbox_exporter + Loki |
| Grafana | graf-01 |
172.16.0.41 |
- | Dashboards + unified alerting + image renderer |
| Tailscale GW | tailscale-gw |
172.16.0.1 |
LAN + 100.x.y.z |
SDN VNet gateway + Tailscale subnet router |
External access URL: https://proxmox.chi.lab.amd-e.com → lb-01.
The core infrastructure survives a single-node failure (N-1 redundancy).
- Mechanism: Proxmox HA Manager (watchdog) + ZFS replication.
- Replication rate: every 15 minutes.
- Target nodes: all peers (
pve02,pve03). - Protected resources:
ct:100— Authentikct:101— Tailscale GWct:102— DNSct:103— Load Balancerct:104— Mail forwarderct:106— VictoriaMetrics (vm-01)ct:107— Grafana (graf-01)
If a node fails, Proxmox HA automatically restarts protected containers on a healthy node. Check status with ha-manager status on any node.
Single-host metrics + logs stack on vm-01 and graf-01:
- Agents — Grafana Alloy is the unified telemetry agent on every
managed host except the Tanium appliances (the
tanium_clustergroup). It runsprometheus.exporter.unixfor node-exporter-shape metrics andloki.source.journalfor the systemd journal, then pushes both viaprometheus.remote_write(to VM at:8428/api/v1/write) andloki.write(to Loki at:3100/loki/api/v1/push). Theroles/alloy/role supports apt (Debian/Ubuntu), dnf (RHEL 9+), dnf via shell (EL8 — python3.9 lacks dnf bindings), and zypper (SUSE). Tanium appliances run a narrownode_exporterinstall viaroles/tanium_node_exporter/instead — different hardening profile. - Metrics — VictoriaMetrics receives node_exporter-shape metrics
two ways: pushed by Alloy (every Alloy host) and pulled at
:9100fromtanium_cluster. Blackbox_exporter probes also land in VM:- TCP probes to Tanium postgres (5432 on TS, 5433 on TMS) →
tanium_postgres_unreachable - TCP probe to Tanium server console (:443) →
tanium_console_unreachable - HTTPS probes to grafana / auth / proxmox public URLs →
https_endpoint_unreachable,cert_expiring_soon(cert <14 days)
- TCP probes to Tanium postgres (5432 on TS, 5433 on TMS) →
- Logs — Loki on
vm-01:3100. All Alloy hosts ship vialoki.source.journal; Tanium appliances don't ship logs. Labels (host,job,unit,severity,nodename) are stable across hosts. 14-day retention, filesystem storage. Loki is also a Grafana datasource — query logs in the same UI as metrics. - Alerts — Grafana unified alerting. Rules live in
roles/grafana/templates/alerting_rules.yml.j2. Categories:- Reachability:
host_down,https_endpoint_unreachable,tanium_postgres_unreachable,tanium_console_unreachable. - Capacity / health:
disk_full,loki_disk_pressure,loki_write_errors. - Cert lifecycle:
cert_renewer_wedged(warning at <30 days runway — acme.sh's renewal cadence),cert_expiring_soon(critical at <14 days; backstop). - Cluster:
log_pve_replication_failed,log_pve_quorum_lost. - Log-backed events:
log_oom_kill,log_service_restart_loop,log_ssh_brute_force,log_kernel_io_error,log_postgres_fatal,log_postfix_relay_failure. - App-specific staleness:
smart_vent_controller_frozen(HVAC vent-control AppDaemon heartbeat, seeroles/grafana_ack_botfor how these route through Discord),tati_phone_tracking_stale. - Meta:
dead_mans_switch— always-firing rule routed exclusively to a healthchecks.io webhook so an outage of Grafana / mail-01 / the LAN doesn't leave you blind.
- Reachability:
- Delivery — Four contact points, all fanned out in parallel
(
continue: true):- Home Assistant webhook → iOS critical push (
severity=criticaloverrides Do Not Disturb; warnings come in as normal pushes). - Discord (
#infra-alerts, viaroles/grafana_ack_bot— a standalone bot on hermes-gw-01, NOT Grafana's native Discord integration directly) — clickthrough link to the firing alert, permanent scrollable history, and reaction-based ack (EM7-style: suppresses new notifications until the alert actually resolves, not a fixed timer) / timed silence (1h/4h/24h). - iCloud SMTP fallback via
mail-01for resilience if HA is down. - healthchecks.io webhook for the dead-man's-switch only — pages out-of-band (gmail, not via mail-01) if the rest of the pipeline is broken.
- Home Assistant webhook → iOS critical push (
Self-hosted Renovate runs weekly via GitHub Actions
(.github/workflows/renovate.yml). Tracks version pins in role
defaults/main.yml files plus a few Docker image tags:
- Binaries from GitHub releases:
alloy,blackbox_exporter,tanium_node_exporter(node_exporter),victoriametrics,loki,golang/go,grafana/grafana-image-renderer. - Docker images:
ghcr.io/goauthentik/server,docker.io/library/postgres,docker.io/library/redis(all consumed byroles/authentik_service/).
A second weekly Action (.github/workflows/renovate-backlog.yml) fails
the run if more than 5 dependency-labeled PRs accumulate, so the
dashboard issue #1 doesn't get ignored silently.
- Login auth — every operator-facing UI flows through Authentik:
Proxmox WebUI (OIDC), Grafana (OAuth2), Tanium console (SAML),
VictoriaMetrics + whoami (nginx forward-auth via the Authentik
embedded outpost). Grafana also has a local
adminpassword as documented emergency-access fallback. - DNS —
dns-01(bind9) forwards upstream to AdGuard Home on the homeassistant host, which handles the encrypted DoT/DoH egress to Cloudflare and applies adblock lists. Lab queries fail closed if AdGuard is down (forward only;). - Per-CT firewalls — authentik, mail-01, proxy-01, and all 6 Tanium
CTs have allow-listed inbound rules. Other service CTs (tailscale-gw,
dns-01, lb-01, ntp-01, vm-01, graf-01) live in the trusted-SDN zone.
See
docs/network-security.mdfor the full threat model. - pve hosts — management plane (SSH, web UI, corosync) on
vmbr0LAN only. TheprivateSDN endpoint added byroles/pve_private_ip/is outbound-only via aPRIVATE-MONITORING-INiptables drop chain. - Secrets — Ansible Vault for everything;
gitleaksruns in CI for accidental commits beyond pre-commit's PEM-only check.
Manages the lifecycle of LXC containers, VMs, and cluster configuration.
- Inventory:
ansible/inventory/proxmox.ymldefines nodes and static IPs for core services. - Playbooks (selected):
deploy_dns.yml— Bind9 DNS (dns-01).deploy_authentik.yml— Authentik IDP.deploy_loadbalancer.yml— Nginx LB (lb-01).deploy_tailscale_gw.yml— Tailscale gateway.configure_sso.yml— Proxmox OIDC realm & permissions.manage_ha.yml— ZFS replication & HA resources.manage_authentik.yml— declarative Authentik config (providers, apps, groups).deploy_tanium_clients.yml— Tanium client install across mixed OS targets.deploy_monitoring.yml— VictoriaMetrics + Grafana.deploy_grafana_ack_bot.yml— Discord reaction ack/silence bot for Grafana alerts (hermes-gw-01, seeroles/grafana_ack_bot/README.md).
Home Assistant configuration deployed to the HA host. Notable subsystems:
- Water heater circulator pump — occupancy-driven, with daily runtime limits and cooldown.
- Lighting automations — cat room, front porch sconces, garage door motion, stair lighting.
- Climate control — Flair vents, Ecobee sensors.
- Shelly Gen3 fleet — 6 devices (pool pump, water-heater circulator, basement exhaust, 3 garage bays) on local MQTT to the HA Mosquitto add-on, Shelly cloud disabled. See
docs/shelly-mqtt.md. - Deployment —
homeassistant/deploy_homeassistant.shsyncs configs and reloads.
bootstrap.sh— restore terminal/shell setup on a fresh machine (macOS, Debian/Ubuntu, RHEL family, Arch).install_dev_tools.sh— install the lint/test toolchain (pre-commit + ansible-lint + collections fromansible/requirements.yml). macOS via Homebrew.yubikey_vpn_connect.sh— YubiKey-based VPN connection.patch_binary.sh— binary patching helper.grafana_auth.py+grafana_curl.sh— JWT-token extractor for browser-based Grafana SSO + curl wrapper that injects the token (seeREADME_GRAFANA_AUTH.md).tanium/— Tanium platform tooling (client API, TDS, performance testing, sensors, etc.).
ansible-playbook -i ansible/inventory/proxmox.yml ansible/<playbook_name>.ymlVerify via https://proxmox.chi.lab.amd-e.com or SSH.
# Fast path — automations / templates / sensors / inputs / apps.yaml
# (everything that can be hot-reloaded). Reloads automations via API, no
# restart, ~5 seconds.
ansible-playbook ansible/deploy_ha_automations.yml
# Slow path — same plus a full `ha core restart`. Use when
# configuration.yaml changes (anything that requires a restart to take
# effect).
ansible-playbook ansible/deploy_ha_automations.yml -e ha_restart=true./scripts/bootstrap.shLab-only convenience: adds a passwordless ("trust") pg_hba.conf entry
scoped to 172.16.0.0/24 and opens the listener port in iptables on all
four Tanium platform appliances (TS x2, TMS x2). Auto-detects whichever
postgres service is enabled per host and reloads it on change.
The play also installs /usr/local/bin/open_psql.sh and a oneshot
systemd unit pulled in by postgresql-ts.service and
postgresql-tms.service, so the rules survive reboots — iptables state
is reapplied on every postgres start (pg_hba.conf already persists).
ansible-playbook -i ansible/inventory/proxmox.yml \
ansible/apply_tanium_postgres_trust.ymlscripts/tanium/open_psql.sh is the per-host script the systemd unit
runs; you can also invoke it directly on an appliance for one-off
recovery.
- Ansible Vault is the single source of truth for ansible secrets.
Inline
!vault |blocks inansible/inventory/group_vars/all.ymland a fully-encryptedansible/group_vars/all/vault.yml. Decrypted at runtime using.vault_pass(gitignored). ansible.logis disabled inansible/ansible.cfg—ansible-inventory --hostand any debug task that prints vars dumps decrypted hostvars to log_path, so we don't write logs to disk. See the comment in ansible.cfg.- Home Assistant secrets in
/config/secrets.yamlon the HA host (gitignored locally; seehomeassistant/secrets.yaml.example).
GitHub Actions runs four lint jobs on push and PR (see .github/workflows/lint.yml):
ansible-lint(configured byansible/ansible.cfg+.ansible-lint)shellcheck(configured by.shellcheckrc)yamllint(configured by.yamllint)rufffor Python (configured bypyproject.toml)
To get the same enforcement on your laptop, run the installer once:
./scripts/install_dev_tools.shIt installs pre-commit, ansible-core, ansible-lint, the ansible
collections, and wires up the git hooks. After that, hooks run on every
git commit; to lint the whole tree manually:
pre-commit run --all-filesPersonal homelab configuration. Use at your own discretion and adapt as needed.