Back to work

Project Bastion

2026

Zero-Trust Homelab Infrastructure

OPNsenseProxmox VEDockerCloudflare Zero TrustOpenWrtNginx Proxy Manager
Project Bastion
Status
Core platform running; security tooling next
Role
Network design, build and operations
Hardware
Sophos X15 firewall, Proxmox host (Intel i5, 16 GB), Netgear R7800 on OpenWrt

A zero-trust homelab built on a repurposed Sophos X15 firewall running OPNsense, with a Proxmox host for virtual machines and containers. Nothing on the network accepts connections from the internet: remote access runs through an outbound Cloudflare Zero Trust tunnel, and every self-hosted service sits behind a TLS reverse proxy. The goal is private, self-hosted replacements for cloud services, built and run the way a small production network would be.

Interactive network map. Drag to pan; zoom with the scroll wheel or the + and − buttons. Orange dashed lines are the outbound Zero Trust tunnel; the purple zone is the planned isolated lab.

[Where it stands]

  • Firewall and routing. OPNsense on the Sophos X15 is the only router on the network. It handles NAT, firewall rules, DHCP and DNS.
  • Hypervisor. Proxmox VE runs a Debian VM as the Docker host, plus a Windows VM that starts only when needed. Snapshot backups run weekly.
  • Remote access. The Cloudflare Zero Trust connector runs on the firewall and only dials out. My laptop and phone join the private network through the WARP client, with no ports open on the WAN.
  • HTTPS for every service. Nginx Proxy Manager terminates TLS with a wildcard Let's Encrypt certificate issued through a DNS challenge, and routes each service by hostname.
  • Wi-Fi. A Netgear R7800 flashed with OpenWrt works as an access point behind the firewall.
  • Self-hosted apps. Vaultwarden for passwords, Immich for photo backup, immich-swipe for sorting photos, and AdGuard Home, each deployed as a Portainer stack.
  • Power. The Proxmox host can sleep, and the firewall wakes it remotely with Wake-on-LAN.

[How a request travels]

DNS on the firewall resolves every service name to the reverse proxy. The proxy reads the hostname, decrypts the request and forwards it to that service's port on the Docker host. Containers sit on private Docker networks, so the proxy is the front door. Away from home, WARP replaces the first hop: the request enters Cloudflare, leaves the tunnel connector on the firewall, and follows the same path.

[Problems I hit and how I solved them]

  • The ISP gateway was bypassing the firewall. Devices were picking up public IPv6 addresses and the ISP's domain, and IPv6 traces skipped OPNsense entirely. Using ipconfig, tracert and the IPv6 neighbor table, I traced it to a cable running from the ISP gateway straight into the LAN switch. Removing it left one path to the internet, through the firewall.
  • Wi-Fi was outside the firewall. Phones and laptops used the ISP gateway's Wi-Fi, so none of my firewall rules or DNS applied to them. I flashed a used Netgear R7800 with OpenWrt, set it up as an access point with routing and DHCP turned off, moved every device over, and switched off the ISP Wi-Fi.
  • The tunnel plugin corrupted its token. The OPNsense Cloudflare plugin kept failing with "Invalid tunnel secret", even with a fresh token. I compared only the length and the first and last characters of the saved and original tokens, so the secret was never exposed, and showed the plugin was altering it on save. I disabled the plugin and run the connector as a supervised daemon that reads its token from a root-only file, with a restart script and a written runbook.
  • Rotating an exposed credential. A tunnel token ended up in a support conversation while I was debugging. I deleted that tunnel, created a new one, and changed my process so tokens are only ever written to the root-only file.
  • Proxmox couldn't resolve names. Updates failed with "Temporary failure resolving" even though pings worked. A leftover DHCP client was overwriting the host's resolver config with an empty file. Disabling it and setting DNS through Proxmox fixed updates. The Docker VM hit the same symptom after I gave it a static IP, and the same fix applied.
  • DNS that depended on the lab being awake. Using AdGuard on the Docker VM as the network's DNS would have taken the whole house offline whenever that VM slept or rebooted. I moved the service-name overrides to Unbound on the always-on firewall and pointed the tunnel's local-domain lookups there too, which also fixed name resolution for remote devices.
  • An open-source app broke on Immich v3. immich-swipe stopped loading photos after Immich's v3 release. I found the removed and renamed API endpoints, patched the client on a fork (the new random-photo search, a renamed album parameter, a visibility filter, and clearer validation errors), documented it in the README, and prepared a pull request for the upstream project. Portainer now builds the patched fork.
  • Fitting the stack into 16 GB of RAM. One Docker VM carries the lightweight services, the Windows VM runs only on demand, and heavier tools like Wazuh wait for a RAM upgrade. Before trusting the 2 TB drive with photos, I checked its SMART health data.

[Security decisions]

  • No inbound ports on the WAN. Every remote path starts as an outbound connection from inside the network.
  • The password manager and photo library are reachable only at home or through the tunnel, never from the public internet.
  • The Cloudflare API token used for certificates can edit DNS records for one domain and nothing else.
  • Secrets live in root-only files, never in scripts, repositories or chat.
  • Third-party app stacks don't update themselves. I review upstream changes before redeploying.

[Next up]

  • VLANs for management, servers, IoT and guests, using the OpenWrt router as the managed switch
  • An isolated Kali lab segment with deliberately vulnerable targets
  • Suricata, CrowdSec and Wazuh for detection, tested against that lab
  • Uptime monitoring with alerts to my phone
  • Off-site encrypted backups
  • Gitea and Woodpecker CI to deploy the stack from code

[Tech stack]

OPNsense · Unbound DNS · Proxmox VE · Debian · Docker · Portainer · Nginx Proxy Manager · Let's Encrypt · Cloudflare Zero Trust · WARP · OpenWrt · Vaultwarden · Immich · AdGuard Home