A hybrid cloud / on-prem environment, provisioned entirely as code.
A single source of truth. Every host, subnet, and service is declared once
and generates everything downstream from it: Terraform resources, GitOps
manifests, DNS records, reverse-proxy config. Nothing hand-duplicated.
Architecture
EDGEOracle Cloud - public zero-trust edge
OpenZiti controller/router — internal services reachable with no open inbound ports on the home network.
CORE3× on-prem Proxmox hosts
Every VM provisioned from one declarative host definition — no manual clicking, no drift.
RUN3-node Talos cluster + 3× Docker host
Talos cluster reconciled continuously by Flux GitOps, plus Docker hosts for services that don't need an orchestrator.
SEC5× security & observability tools
Wazuh, Greenbone, an isolated attack range, LibreNMS, and Nautobot — full breakdown under Stack below.
Full topology
Every layer, from the internet down to whatever's actually running right now: not a snapshot, the same live counts and service names as the rest of this page.
Zero-trust tunnel (no open inbound ports)Physical / local network
Network segmentation
13 VLANs off one trunk, segmented by function and trust level rather than one flat network. A compromised IoT device or the isolated attack-range lab has no path to production or management traffic by design.
CORE SWITCH · 802.1Q TRUNK
Workload tiers
DMZVLAN 10
ProductionVLAN 20
DevelopmentVLAN 30
Infra & storage
ManagementVLAN 40
Storage frontVLAN 50
Storage backVLAN 60
Isolated segments
IoTVLAN 70
Security toolsVLAN 80
Security labVLAN 90
Access tiers
GuestsVLAN 100
UsersVLAN 110
AdminsVLAN 120
VPN usersVLAN 130
Network automation
Config-as-code and centralized identity, extended onto real, physical network hardware, not just VMs.
Keeping a physical switch's config honest:
Declare
Jinja template, version-controlled
Render
Golden Config renders the intended config
Commit
Output pushed back to this same repo
Compare
Diffed against the switch's live config
Visibility
Compliance report in Nautobot
The CBS350 switch's intended config is a real Jinja template in this repo, not a backup nobody looks at again
Nautobot's Golden Config app renders it, commits the output back into this same git history, and diffs it against the switch's actual running config
RADIUS/AAA and LibreNMS SNMP/LLDP monitoring run against that same physical switch — not just VMs
How it works
Authenticating a user, one real identity for every app:
Identity
Microsoft Entra ID
Federate
Authentik OAuth2 source
Gate
Access-tier group check
Protocol
OIDC · SAML · forward auth
App
Signed in, no second password
Every app traces back to the same Microsoft Entra ID account — no separate password per service
Authentik fronts everything as the single SSO broker: OIDC where supported, SAML for the one app that only speaks that, forward-auth for the rest
Each app's own <service>-read/-edit/-admin Authentik groups gate access, generated automatically, not hand-provisioned
A local emergency-fallback account exists on a handful of services as a break-glass path, not day-to-day login
Reaching an app remotely, with no open inbound ports:
Client
Ziti Desktop / Mobile Edge
Overlay
OpenZiti controller + router
Proxy
Traefik · single ingress point
SSO gate
Authentik · OIDC / SAML
App
Kubernetes or Docker
Nothing on the home network ever accepts an inbound connection
A Ziti client dials out to the public controller, which brokers an outbound-only tunnel back to Traefik
Traefik is the same reverse proxy every request goes through, local or remote, enforcing the Authentik SSO gate before most apps are reached
Shipping a change, GitOps-style:
Commit
git push to main
Reconcile
Flux polls the repo
Apply
Cluster converges to match
Publish
Wiki + this page's live stats regenerate
Every change to this repo, including the numbers on this page, flows through the same pipeline: commit, reconcile, apply
No manual kubectl apply, no clicking through a UI
Enrolling a new user, without ever hand-provisioning an account:
Intake
Public form, no login required
Review
n8n checks existing identities
Approve
Slack DM, one human decision
Commit
n8n commits the new user to git
Provision
CI applies, Terraform creates the account
Enroll
Emailed a QR code to enroll their identity
The intake form is the only thing a not-yet-provisioned person can reach — no Ziti identity yet, so it runs over a narrowly-scoped public tunnel, not the zero-trust overlay above
Approval is a single human decision that produces a git commit, not a rubber stamp on a database write
Terraform is still the only thing that ever creates the account — same GitOps pipeline as everything else, just triggered by a Slack click
A symmetric offboarding flow tears the account back down the same way
Disaster recovery
A written, dependency-ordered runbook for a total rebuild.
Rebuilding from an empty Proxmox cluster:
Clone
git clone, secrets reconnect via Bitwarden
Bare metal
Proxmox + firewall, installed by hand
Terraform
Every unit applied, one dependency order
GitOps
Flux reconciles every K8s app on its own
Docs
Wiki + this page rebuild themselves
Everything in this repo except a short, explicit list of local secrets lives in git — clone it, reconnect Bitwarden Secrets Manager, and most of the state is back
Every VM, the Kubernetes cluster, and the app layer on top come back from a documented sequence of Terraform applies, in a fixed dependency order
Installing the hypervisor OS, creating VLANs on the firewall, and pushing config to the physical network hardware are genuine manual steps — the runbook calls them out explicitly rather than glossing over them
Everything downstream of that, including this page's own live stats, rebuilds itself with zero further manual steps
Real-time monitoring
Zabbix, discovering what's actually running rather than a static list someone has to keep maintaining.
Coverage
Proxmox and Kubernetes via native HTTP-agent templates polling their own real APIs, everything else agentless (ICMP / TCP)
Discovery
Kubernetes node and pod conditions are LLD-discovered, not hand-declared per resource
Alerting
Slack, severity-colored, with threaded update / resolve messages
Built to catch a real outage
On 2026-08-08, kubelet silently stopped posting status on two Kubernetes nodes while they kept answering ICMP the whole time, invisible to a simple ping check
The Kubernetes template's node-condition trigger ("Is not in Ready state") exists specifically because of that incident, not as a generic best practice added up front
It would have caught the real failure in real time instead of after the fact
Stack
IaC
Terraform + Terragrunt, single source of truth for hosts, network, and services
GitOps
Flux CD, continuously reconciles the Kubernetes cluster from git
Zero trust
OpenZiti overlay, no open inbound ports on the home network
Identity
Authentik SSO fronting internal and Kubernetes-hosted apps
Security
Wazuh + Greenbone, plus an isolated attack range for practice
Observability
LibreNMS monitoring, Nautobot source-of-truth with Golden Config
Automation
n8n running real household/ops workflow pipelines
Backup & DR
Proxmox Backup Server + restic, with a written and tested DR runbook
How the code is organized
The real layout of this repo's terraform/ directory.
terraform/
├── blueprints/ 42 reusable modules, grouped by category
│ ├── compute/
│ ├── containers/
│ ├── kubernetes/
│ └── ...
├── environments/
│ ├── 01-globals/ the single source of truth
│ │ ├── hosts.hcl every VM and physical host, declared once
│ │ ├── network.hcl every VLAN and subnet
│ │ ├── services.hcl every app: routing, auth, hosting tier
│ │ └── variables.hcl shared type contracts every module validates against
│ ├── core/ · dev/ · prod/ one small terragrunt.hcl per deployed unit
│ └── root.hcl centralized inputs, wired into every unit above
└── modules/ shared logic reused across many blueprints
Declared once, generated everywhere
Adding a host to hosts.hcl is the only manual step. Everything below it regenerates on its own, with nothing hand-duplicated and nothing to keep in sync by hand.
hosts.hcl · one host entry
Provisioned
TerraformVM on Proxmox
Discoverable
DNSTechnitium record
NautobotDCIM / IPAM entry
Monitored
Zabbixhost + host group
Documented
Wikireference page regenerates
Live from the reporefreshes ~30min
Not a snapshot: pulled from the same generator that keeps the internal wiki honest. Counts and generic service names only.