djuntgen@juntgen.com
← all posts

Building a Homelab with AI · part 6

Security Review: Hardening a Homelab IaC Repo


This is Part 6 of the Building a Homelab with AI series. Previously: Caddy DMZ Migration | Next: Lessons Learned

Why Review Your Own Homelab?

“It’s just a homelab” is the most dangerous sentence in self-hosting. Your homelab sits on the same network as your family’s devices. It has services exposed to the internet. It stores credentials and API tokens. A compromised homelab isn’t a minor inconvenience — it’s a foothold into your home network.

After building out the IaC repo, bootstrapping servers, setting up GitOps, and migrating Caddy to the DMZ, I asked Claude Code to do a full security review of everything we’d built. The results were a mix of real findings, false positives, and useful validation.

Finding 1: Vault Password Not in .gitignore

Severity: High (potential)

The Ansible Vault password file (<vault-password-file>) wasn’t listed in .gitignore. The file lives outside the repo directory, so it wouldn’t be committed accidentally under normal circumstances. But .gitignore is defense in depth — one misplaced cp or a future directory restructure, and the vault password could end up in the repo. One git push later and every encrypted secret in the repo is compromised.

The fix: Add .vault_pass to .gitignore. Thirty seconds of work, meaningful risk reduction.

Finding 2: Open WebUI Bound to 0.0.0.0

Severity: Medium

The Open WebUI compose file had:

ports:
  - "3000:8080"

This binds to 0.0.0.0:3000, meaning Open WebUI was accessible directly on the LAN at http://docker-host:3000, completely bypassing the Caddy reverse proxy and all its access controls. Anyone on the network could hit it directly.

The fix: Two changes. First, bind to localhost only:

ports:
  - "127.0.0.1:3000:8080"

Second, add openwebui.example.com to the Caddyfile with the local_only snippet, so it’s only accessible through the reverse proxy from RFC1918 addresses. Now there’s a single ingress point with consistent access controls.

Investigated but Acceptable

Not every finding from the review was actionable. Some were investigated and determined to be acceptable risk or false positives:

host_key_checking = false in ansible.cfg. Yes, this disables SSH host key verification, which in theory allows MITM attacks. In practice, on a flat home network where I control every device and the Ansible control node talks to hosts by static IP, the risk is negligible. The alternative — manually accepting host keys for every new container and dealing with key rotation — adds real operational friction for theoretical protection against an attacker who already has layer-2 access to my network.

tls_insecure_skip_verify in the Caddyfile. This tells Caddy not to verify TLS certificates when proxying to backends like Portainer (https://10.0.0.12:9443) and Proxmox (https://10.0.0.4:8006). These services use self-signed certs. The traffic is on the LAN, between the DMZ proxy and known backends at hardcoded IPs. The alternative is deploying a private CA and managing internal certificates — a worthwhile project someday, but not a critical vulnerability today.

Command injection in pct exec playbook. The bootstrap-via-pve playbook uses command: pct exec {{ item.vmid }} which could theoretically be a command injection vector. But the VMIDs are hardcoded integers in the playbook vars, not user input. No injection risk with static, controlled data.

Defense in Depth

The security review reinforced something I already knew but don’t always practice: defense in depth isn’t about any single control being perfect. It’s about layering imperfect controls so an attacker has to beat all of them.

My layers now:

  1. Cloudflare — DDoS protection, hides origin IP for proxied records
  2. Edge firewall — only ports 80/443 forwarded, and only to the DMZ
  3. DMZ isolation — Caddy runs on a dedicated subnet, separate from LAN services
  4. Caddy access controlslocal_only snippet blocks external access to internal services
  5. Service binding — services bind to localhost, only reachable through the reverse proxy
  6. Ansible Vault — secrets encrypted at rest in the Git repo
  7. SSH hardening — no root login, key-only auth, deployed consistently via Ansible

No single layer is unbreakable. Together, they make the homelab dramatically harder to compromise than “everything on a flat network with services on 0.0.0.0.”

The Biggest Win

The DMZ migration was the single largest security improvement. Moving the internet-facing reverse proxy to an isolated VM on a dedicated subnet means a compromise of Caddy doesn’t immediately grant access to the LAN. That architectural decision is worth more than any individual configuration tweak.

Security reviews aren’t one-and-done. They’re a practice. But having your infrastructure in code makes them dramatically easier — you can review a Git repo instead of SSH’ing into twelve machines.

Next post: what I learned about AI-assisted infrastructure work.