Exposing a home lab to the internet sounds easy, right?
The easiest option is to expose LAN directly from the router, which does the job but imposes a huge security risk. The second option is to set up a VPN on the router and share the client configuration, so LAN would only be accessible via VPN. While secure, it makes it harder to share with other people. The third option is Tailscale or Cloudflare Tunnel. They do the heavy lifting by providing servers and software for remote connections. The latter allows global public access, so anyone can connect. And the fourth option is to rent a VPS, set up a firewall, reverse proxy, separate Docker network, harden the system, and deploy a host-based intrusion detection system (HIDS)...
In the previous articles I've already covered firewall, reverse proxy setups, and system hardening. I'll be using WireGuard, but any other VPN solution would mostly have the same setup.
Install iproute2
sudo pacman -S iproute2
sudo mkdir -p /etc/iproute2
sudo touch /etc/iproute2/rt_tables
sudo chmod 644 /etc/iproute2/rt_tables
Create a new route table
echo "200 vpn_docker" | sudo tee -a /etc/iproute2/rt_tables
Next we need to pick an IP range for the network.
In my case I went for 172.30.0.0/16.
Create a new Docker network in that range
docker network create \
--driver bridge \
--opt com.docker.network.driver.mtu=1420 \
--subnet=172.30.0.0/16 \
vpn_docker_net
Add a routing rule
sudo ip rule add from 172.30.0.0/16 table vpn_docker
Add a NAT rule
sudo iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
sudo ip route add 10.66.66.0/24 dev wg0
WireGuard config
[Interface]
PrivateKey =
Address = 10.66.66.2/32
DNS = 9.9.9.11,9.9.9.9
# Uncomment the next line to set a custom MTU
# This might impact performance, so use it only if you know what you are doing
# See https://github.com/nitred/nr-wg-mtu-finder to find your optimal MTU
MTU = 1320
Table = vpn_docker
[Peer]
PublicKey =
PresharedKey =
Endpoint =
# Since the table has been altered it won't proxy the whole system's traffic
AllowedIPs = 0.0.0.0/0, ::/0
Make it persistent
tee -a /usr/local/bin/vpn-docker-setup.sh << EOF
#!/bin/bash
# 1. Create the routing table if it doesn't exist
if ! grep -q "vpn_docker" /etc/iproute2/rt_tables; then
echo "200 vpn_docker" | tee -a /etc/iproute2/rt_tables
fi
# 2. Add the IP rule if it doesn't exist
if ! ip rule show | grep -q "from 172.30.0.0/16 table vpn_docker"; then
ip rule add from 172.30.0.0/16 table vpn_docker
fi
# 3. Add the NAT masquerade if it doesn't exist
if ! iptables -t nat -C POSTROUTING -o wg0 -j MASQUERADE >/dev/null 2>&1; then
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
fi
# 4. Manage the routes for the vpn_docker table
# We delete the old default in that table to prevent conflicts, then add the new one
ip route del default dev wg0 table vpn_docker 2>/dev/null
ip route add default via 10.66.66.1 dev wg0 table vpn_docker
# 5. Ensure the local subnet is reachable via wg0
if ! ip route show | grep -q "10.66.66.0/24 dev wg0"; then
ip route add 10.66.66.0/24 dev wg0
fi
EOF
chmod +x /usr/local/bin/vpn-docker-setup.sh
Systemd
tee -a /etc/systemd/system/docker-wg-routing.service << EOF
[Unit]
Description=Configure Docker VPN Routing Table
# Wait for the network and the WireGuard interface to be up
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vpn-docker-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable vpn-docker-setup.service
Update the Docker service
/usr/lib/systemd/system/docker.service
After=network-online.target nss-lookup.target docker.socket firewalld.service containerd.service time-set.target docker-wg-routing.service
Also add net.ipv4.ip_forward=1 to sysctl to allow traffic forwarding.
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sysctl --system
Add this network to the containers and recreate them:
networks:
vpn_docker_net:
external: true