Featured projectlive

The Homelab

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
EDGE Oracle Cloud - public zero-trust edge

OpenZiti controller/router — internal services reachable with no open inbound ports on the home network.

CORE 3× on-prem Proxmox hosts

Every VM provisioned from one declarative host definition — no manual clicking, no drift.

RUN 3-node Talos cluster + 3× Docker host

Talos cluster reconciled continuously by Flux GitOps, plus Docker hosts for services that don't need an orchestrator.

SEC 5× 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
Internet
OpenZiti overlay
Core Switch · 802.1Q trunk
Proxmox Cluster 3× physical hosts
Kubernetes 3 nodes · 50 pods
actualbudgetai_gatewaydocsdonetickgrafanagrocyhomarrn8nnautobotvikunjawifi_portalzabbix
Docker 3× hosts
authentiktechnitiumtraefikwifi_scepziti_console
Dedicated VMs 17× services
bazarrcheckrrclab_apiclab_webgns3greenbonehome_assistantjellyfinlibrenmsmdmprowlarrqbittorrentradarrseerrsonarrunifiwazuh
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
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

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

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

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
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
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.

Hosts managed
24 Terraform-provisioned
Kubernetes
3 nodes · 50 pods running
Services deployed
34 across the stack
Infrastructure as code
22,906 lines across 42 Terraform blueprints
Commits
5,324 to this repo
Deployed right now
actualbudgetai_gatewayauthentikbazarrcheckrrclab_apiclab_webdocsdonetickgns3grafanagreenbonegrocyhomarrhome_assistantjellyfinlibrenmsmdmn8nnautobotprowlarrqbittorrentradarrseerrsonarrtechnitiumtraefikunifivikunjawazuhwifi_portalwifi_scepzabbixziti_console

Last synced: 2026-11-01T07:44:37Z

GNS3 labs Pulled directly from what's actually running on the lab server, not a writeup after the fact.
MTU

MTU Lab 01

Complete

Learning to configure and troubleshoot MTU

4× Router · Cisco IOSv 15.9(3)M6
1× Switch · Cisco IOSvL2 (high_iron_20190423)

Drag nodes to rearrange. Click one to open its config.

Lab Instructions
  • IP Info
  • * R1-ISP: 203.0.111.0/24 (ISP = .100, R1 = .1)
  • * R2-ISP: 203.0.112.0/24 (ISP = .100, R2 = .2)
  • * R2-R3: 192.168.23.0/24 (R2 = .2, R3 = .3)
  • Instructions
  • 1. Do an extended ping from R1 to R2
  • * Set the DF-bit
  • * Do a sweep of sizes from 1496 bytes to 1505 bytes
  • * Which pings succeed? Which fail?
  • 2. Configure a GRE tunnel between R1 and R2
  • * Subnet: 192.168.12.0/30 (R1 = .1, R2 = .2)
  • *What is the IP MTU of the tunnel interface?
  • 3. Configure the IP MTU of each router's tunnel interface to 1500
  • * Ping R2's tunnel interface from R1, size 1500, no DF-bit
  • * Ping R2's tunnel interface from R1, size 1500, set DF-bit
  • * Verify that R1's pings were fragmented
  • * Return the IP MTU to the default
  • 4. Send the following pings and troubleshoot the problem:
  • * Ping from R1 to R3, size 1476, set DF-bit
  • * Ping from R1 to R3, size 1476, no DF-bit
  • * What is preventing these pings from succeeding? Troubleshoot and fix the problem.