Zero-Trust Networking for Homelabs

Zero-Trust Networking for Homelabs: Network Isolation and Access Control

Most homelab builders think about security as an afterthought—a firewall rule here, a password there. I made that mistake for years. Then I compromised my entire network because a single Docker container got exploited, and I'd never set up proper network isolation. Zero-trust networking changed everything for me. Instead of trusting anything inside my LAN, I now verify every single connection, segment my network aggressively, and enforce granular access controls. If you're running sensitive services at home—Vaultwarden, Nextcloud, Immich, or anything with personal data—you need this approach.

What Zero-Trust Actually Means in a Homelab

Zero-trust is simple in concept but demanding in practice: never trust, always verify. In enterprise terms, it means no implicit trust based on network location. For us running homelabs, this translates to a few core principles:

I prefer zero-trust over traditional firewalls alone because a firewall only stops external threats. It won't help if your Jellyfin container gets compromised and an attacker pivots to your file server. With proper segmentation, that pivot fails immediately.

Layer 1: Network Segmentation with VLANs

The foundation of zero-trust networking is splitting your network into security zones. I use VLANs on my managed switch to create logical boundaries. Here's my setup:

If you don't have a managed switch, don't skip this—you can still segment using Docker networks and iptables rules on your Linux host. It's messier, but it works.

Tip: Start with just two VLANs: "trusted" (workstations, management) and "untrusted" (everything else). You can expand later once you understand the routing.

Layer 2: Docker Network Isolation

For containerized services, Docker networking is your first line of defense. By default, Docker containers on the same bridge network can talk to each other freely. I break that assumption immediately.

version: '3.9'
services:
  nextcloud:
    image: nextcloud:latest
    networks:
      - nextcloud-internal
    environment:
      - MYSQL_HOST=db
    ports:
      - "8080:80"
  
  db:
    image: mariadb:latest
    networks:
      - nextcloud-internal
    environment:
      - MYSQL_ROOT_PASSWORD=change_me
    volumes:
      - db_data:/var/lib/mysql

  vaultwarden:
    image: vaultwarden/server:latest
    networks:
      - vaultwarden-internal
    ports:
      - "8081:80"
    volumes:
      - vw_data:/data

networks:
  nextcloud-internal:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-nextcloud
  vaultwarden-internal:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-vaultwarden

volumes:
  db_data:
  vw_data:

Notice: Nextcloud and Vaultwarden run on separate Docker networks. They can't communicate with each other at all. The database only connects to Nextcloud. This is critical—if one service is compromised, the attacker can't immediately pivot to steal credentials from the others.

I also define explicit networks per application stack rather than throwing everything on one "default" bridge. This forces me to be intentional about what talks to what.

Layer 3: Firewall Rules with UFW

Even with network segmentation, I use UFW on my Docker host to enforce additional boundaries. UFW sits above Docker iptables rules and acts as a safety net.

#!/bin/bash
# Enable UFW
sudo ufw --force enable
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH only from management network (192.168.10.0/24)
sudo ufw allow from 192.168.10.0/24 to any port 22 proto tcp

# Allow Nextcloud only from trusted network
sudo ufw allow from 192.168.10.0/24 to any port 8080 proto tcp

# Allow Jellyfin from services and trusted networks
sudo ufw allow from 192.168.20.0/24 to any port 8096 proto tcp
sudo ufw allow from 192.168.10.0/24 to any port 8096 proto tcp

# Block everything between service networks
# (handled by Docker network isolation, but belt-and-suspenders)
sudo ufw deny from 192.168.20.0/24 to 192.168.30.0/24

# Show rules
sudo ufw status numbered

The magic here is the source-specific rules. Port 8080 is open, but only from my management VLAN. If someone plugs in a laptop on the guest network and scans for open ports, they'll get nothing.

Watch out: UFW rules in Docker can be tricky. Docker modifies iptables directly and can bypass UFW rules. Use --iptables=false in your Docker daemon config if you want UFW to be the sole firewall, then restart the daemon.

Layer 4: Mutual TLS and Application-Level Authentication

Network-level isolation is great, but it's not enough. I layer in mutual TLS (mTLS) for service-to-service communication and require API keys or certificates for data access.

For example, my Nextcloud instance communicates with its database over mTLS. The database server verifies that the client certificate matches what it expects. Even if an attacker gains RCE in the Nextcloud container, they can't connect to the database without the correct certificate—which is mounted read-only and separate from the application code.

I manage certificates using Vault for production deployments and self-signed certificates for homelab services:

# Generate a root CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/CN=Homelab-CA"

# Generate server certificate for database
openssl genrsa -out db.key 4096
openssl req -new -key db.key -out db.csr \
  -subj "/CN=db.internal"
openssl x509 -req -days 365 -in db.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial -out db.crt

# Generate client certificate for Nextcloud
openssl genrsa -out nextcloud.key 4096
openssl req -new -key nextcloud.key -out nextcloud.csr \
  -subj "/CN=nextcloud.internal"
openssl x509 -req -days 365 -in nextcloud.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial -out nextcloud.crt

# Mount in Docker with proper permissions
docker run --rm -v /certs:/certs \
  -e MYSQL_SSL_CA=/certs/ca.crt \
  -e MYSQL_SSL_CERT=/certs/nextcloud.crt \
  -e MYSQL_SSL_KEY=/certs/nextcloud.key \
  nextcloud:latest

This way, even if an attacker breaks into my Nextcloud container and finds database credentials in environment variables, they can't connect to the database without the certificate. I store certificates separately from secrets in Docker, and they're read-only within the container.

Layer 5: Monitoring and Anomaly Detection

Zero-trust also means detecting when someone tries to break the rules. I use a combination of tools:

I don't need enterprise-grade SIEM at home, but I do regularly review logs and set up simple alerts. A container trying to reach a database it's not supposed to know about is an immediate red flag.

Practical Implementation: A Step-by-Step Checklist

Here's how I'd implement zero-trust in an existing homelab without starting from scratch:

  1. Inventory everything: List all your services, devices, and data flows. What talks to what?
  2. Create network zones: Set up VLANs (or at least Docker networks) for management, services, storage, IoT, and guest.
  3. Start with deny-all: Set firewall to drop all traffic, then explicitly allow only what you need.
  4. Isolate critical data: Your database, file server, and password manager are your crown jewels. Restrict access ruthlessly.
  5. Use secrets management: Never hardcode credentials. Use Docker secrets, environment variables (stored externally), or Vault.
  6. Enable logging and monitoring: You can't defend what you can't see.
  7. Test breakage: Try to ping your storage network from a service container. It should fail. If it succeeds, your segmentation is broken.

A Real-World Example: Protecting Vaultwarden

Vaultwarden holds all my passwords. If it's compromised, everything falls. Here's how I secure it:

Even if an attacker gets RCE in Vaultwarden, they can't steal the database password (it's only available to the database server's certificate), can't pivot to other services (wrong network), and can't exfiltrate data without tripping monitoring alerts.

Addressing the Complexity Trade-Off

Zero-trust is more work than open networks. You'll debug networking issues. Services won't start because they can't reach dependencies. I've spent hours troubleshooting why my backup service couldn't reach the NAS—turns out I'd forgotten to allow traffic between those VLANs.

But that pain is the point. Each failure teaches me something about my network. And the peace of mind knowing that a compromised container can't instantly pivot to my file server is worth every hour spent.

For a homelab, start small. Segment into 2–3 zones, set up Docker network isolation, and use UFW rules. Once that's stable, add mTLS and deeper monitoring. Don't try to deploy full zero-trust overnight.

Next Steps

If you're running services on a VPS and want similar isolation, I'd recommend exploring Tailscale for zero-trust access (replaces traditional VPN), Authelia for centralized authentication, and a reverse proxy like Caddy or Traefik with mTLS support. For scaling beyond a single homelab, platforms like RackNerd's KVM VPS give you the isolation of a full virtual machine—perfect for compartmentalizing environments.

Start your zero-trust journey today. Review your network topology. What's talking to what? What data could leak if one service is compromised? Once you see those attack paths, you'll understand why trust, once given, is hard to revoke.