Zero-Trust Networking in Your Homelab: Microsegmentation Strategies
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
Most homelabs start the same way: everything on one flat network, every container chatting freely with every other container, and maybe a single router firewall standing between you and the internet. That works until your Jellyfin instance somehow becomes a pivot point into your Vaultwarden data — or until you realise your IoT bulbs can reach your NAS over SMB. Zero-trust networking flips this model entirely: nothing is trusted by default, every connection is explicitly permitted, and every service is isolated until proven otherwise. In this article I'll walk through the practical tools and techniques I use to bring microsegmentation to a real homelab, covering VLANs, Docker networks, nftables rules, and per-service authentication with Authelia.
What Zero-Trust Actually Means at Home
Zero-trust isn't a product you buy — it's a philosophy with three core principles: verify every connection, grant least-privilege access, and assume breach. In an enterprise context this involves big identity platforms and expensive hardware. In a homelab, we're translating the same ideas into practical tools we already have. For me, that means:
- Network segmentation so that a compromised service can't reach unrelated services.
- Explicit firewall rules that deny by default and permit by exception.
- Per-service authentication so that being on the right network still isn't enough to access a service.
- Continuous monitoring so that unexpected traffic patterns surface quickly.
The goal isn't perfection — it's making lateral movement expensive enough that an attacker either gives up or makes enough noise to be caught.
Network Segmentation with VLANs and Docker Networks
The foundation of microsegmentation is keeping different classes of traffic on different network segments. I run a managed switch (a TP-Link TL-SG108E works fine for this) and define VLANs by trust zone. My rough breakdown:
- VLAN 10 — Management: Proxmox host, switch, router admin interfaces. No internet access, only reachable from a specific admin machine.
- VLAN 20 — Services: Docker hosts running Nextcloud, Gitea, Immich, etc.
- VLAN 30 — IoT: Smart bulbs, plugs, home automation. Internet access, but completely blocked from all other VLANs.
- VLAN 40 — Trusted clients: My desktop, laptop, phone.
Traffic between VLANs goes through my OPNsense router, where inter-VLAN routing rules live. IoT devices cannot initiate connections to anything except the internet and my Home Assistant instance on a dedicated interface. Services can be reached from Trusted clients, but only on specific ports.
Inside Docker, I extend the same principle. Instead of dumping every container on the default bridge network, I define named networks and attach containers only to the networks they need:
# docker-compose.yml excerpt — Nextcloud stack with isolated networks
networks:
nextcloud_frontend:
driver: bridge
internal: false # Caddy lives here and needs internet egress
nextcloud_backend:
driver: bridge
internal: true # Postgres and Redis — no external connectivity at all
services:
caddy:
image: caddy:2-alpine
networks:
- nextcloud_frontend
ports:
- "443:443"
- "80:80"
nextcloud:
image: nextcloud:29-apache
networks:
- nextcloud_frontend
- nextcloud_backend
depends_on:
- db
- redis
db:
image: postgres:16-alpine
networks:
- nextcloud_backend # Only reachable by nextcloud — not by Caddy directly
redis:
image: redis:7-alpine
networks:
- nextcloud_backend
With internal: true, Docker sets up a network bridge with no host routing, so Postgres genuinely cannot make or receive connections outside that Docker network. If your Nextcloud container is compromised, an attacker still can't directly query your database from their remote shell — they have to go through the Nextcloud process itself.
docker network inspect nextcloud_backend after bringing the stack up and check the "Internal": true field. If it reads false, you've misconfigured something and the network has external routing.Firewall Rules and Microsegmentation with nftables
VLANs and Docker networks handle broad segmentation, but nftables lets me express fine-grained policy at the host level. I prefer nftables over iptables for new setups — the syntax is more readable, rule sets are atomic (no half-applied state during reload), and sets/maps make managing lists of IPs and ports far cleaner.
Here's a baseline nftables configuration I apply to every Docker host. It starts with a deny-all posture and opens only what's needed:
#!/usr/sbin/nft -f
# /etc/nftables.conf — baseline Docker host policy
flush ruleset
table inet filter {
# Trusted management sources (replace with your actual admin VLAN subnet)
set mgmt_sources {
type ipv4_addr
flags interval
elements = { 10.0.10.0/24 }
}
# Services this host is allowed to expose to the trusted client VLAN
set allowed_service_ports {
type inet_service
elements = { 443, 80 }
}
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow loopback
iifname "lo" accept
# ICMP — allow ping but rate-limit
ip protocol icmp icmp type { echo-request } limit rate 5/second accept
ip6 nexthdr icmpv6 accept
# SSH only from management VLAN
ip saddr @mgmt_sources tcp dport 22 accept
# HTTPS/HTTP from trusted clients VLAN
ip saddr 10.0.40.0/24 tcp dport @allowed_service_ports accept
# Docker internal bridge traffic — needed for container comms
iifname "docker0" accept
iifname { "br-*" } accept
# Log and drop everything else
log prefix "nft-input-drop: " level warn
drop
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
# Allow Docker networks to forward between their own bridges
iifname { "br-*" } oifname { "br-*" } accept
# Allow Docker containers to reach internet via host (if needed)
iifname { "br-*" } oifname "eth0" accept
log prefix "nft-forward-drop: " level warn
drop
}
chain output {
type filter hook output priority 0; policy accept;
# Permissive output — tighten per-service as needed
}
}
Apply and enable it with:
sudo nft -f /etc/nftables.conf
sudo systemctl enable nftables
sudo systemctl restart nftables
# Verify the ruleset loaded cleanly
sudo nft list ruleset
nft -c -f /etc/nftables.conf (check mode) before committing. Getting your SSH rule wrong will lock you out. Always have out-of-band access (iDRAC, Proxmox console, or physical keyboard) before tightening firewall rules on a remote host.Per-Service Authentication with Authelia and mTLS
Being on the right VLAN and passing the firewall still isn't enough in a zero-trust model. I front every internal web service with Authelia, which adds SSO and two-factor authentication as a Caddy forward-auth middleware. Even if someone gets onto my trusted VLAN, they still need valid credentials to reach Gitea, Immich, or anything else.
My Caddy config for a protected service looks like this:
# /etc/caddy/Caddyfile — forward-auth with Authelia
(authelia_forward_auth) {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.home.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
}
gitea.home.example.com {
import authelia_forward_auth
reverse_proxy gitea:3000
}
immich.home.example.com {
import authelia_forward_auth
reverse_proxy immich-server:2283
}
# Authelia itself — no forward-auth wrapper obviously
auth.home.example.com {
reverse_proxy authelia:9091
}
For service-to-service communication (where a user isn't involved), I use mutual TLS (mTLS). Each service gets a client certificate signed by an internal CA I manage with step-ca. When Service A calls Service B, both sides present certificates — neither will communicate with an unrecognised peer. This is what "verify every connection" looks like at the API level.
# Bootstrap a local CA with step-ca
sudo apt install step-cli step-ca
step ca init \
--name "Homelab Internal CA" \
--dns "ca.home.example.com" \
--address ":8443" \
--provisioner "[email protected]"
# Issue a cert for a service
step ca certificate "immich-server.home.example.com" \
immich.crt immich.key \
--ca-url https://ca.home.example.com:8443 \
--root /etc/step-ca/certs/root_ca.crt
Practical Implementation Checklist
When I set up a new service in my homelab, I run through this checklist before considering it "done":
- [ ] Service is in a dedicated Docker Compose stack with its own named networks
- [ ] Database and cache containers use
internal: truenetworks - [ ] Service is on the correct VLAN (or VM NIC bound to that VLAN)
- [ ] nftables INPUT chain has an explicit rule for the service's port, scoped to the correct source subnet
- [ ] Service is behind Caddy + Authelia forward-auth
- [ ] Service has no ports published to
0.0.0.0— only to specific host IPs if needed - [ ] Outbound internet access for the service container is explicitly allowed or blocked depending on whether it's needed
Monitoring and Anomaly Detection
Zero-trust without visibility is just security theatre. The log prefix lines in my nftables config feed into journald, which I ship to a local Loki instance (via Promtail) and query in Grafana. Any nft-input-drop or nft-forward-drop log entries that spike in volume get an alert.
I also run Suricata in IDS mode on my OPNsense router, watching inter-VLAN traffic specifically. Unusual port scans from an IoT device, a container suddenly trying to reach port 5432 on a database it shouldn't know about, or DNS queries going to non-approved resolvers all show up here. Uptime Kuma handles basic availability monitoring, but it's the anomaly detection that catches actual security events.
# Tail nftables drop logs in real time
sudo journalctl -f -k | grep "nft-.*-drop"
# Count drops by source IP in the last hour (useful for spotting scanners)
sudo journalctl -k --since "1 hour ago" \
| grep "nft-input-drop" \
| grep -oP 'SRC=\S+' \
| sort | uniq -c | sort -rn | head -20
Pair this with fail2ban watching the nftables log output for SSH brute-force attempts, and you have a reasonably complete detection layer without spending anything on commercial tooling.
Conclusion
Zero-trust networking in a homelab isn't about paranoia — it's about building good habits that make your environment meaningfully more resilient without destroying usability. The combination of VLAN segmentation, Docker internal networks, nftables deny-by-default rules, and Authelia forward-auth covers the vast majority of realistic threat scenarios a homelab faces. Start with one piece: even just moving your database containers to internal Docker networks this weekend is a real improvement.
My recommended next steps: set up the VLAN structure first (it forces you to think about what talks to what), then harden Docker networking, then layer Authelia on top. Once those three are in place, the nftables work becomes a matter of codifying the policy you've already mentally defined. After that, look into Tailscale ACLs if you want zero-trust access from outside your home network — it's surprisingly well-suited to the same microsegmentation approach described here.
Discussion