Zero-Trust Networking in Your Homelab: Microsegmentation Strategies

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:

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:

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.

Tip: Run 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
Watch out: If you're applying this remotely over SSH, test the rules with 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":

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