Skip to content

Run the VPN on a Docker gateway

A gateway run with Docker carries VPN sessions the same way an appliance does, as long as its host gives it what it needs. Gateways on Kubernetes, Docker Desktop, rootless Docker, and Podman can’t run the VPN yet.

  • The container run as Witina shows it. The commands under Gateways → Add gateway → Run it as a container use the host’s network and the NET_ADMIN, NET_RAW, and NET_BIND_SERVICE capabilities. The VPN needs the host’s network and NET_ADMIN.
  • WireGuard in the host’s kernel. Current Linux distributions have it. We test on Ubuntu 24.04, Debian 12, and Rocky Linux 9.
  • IP forwarding on. Docker turns it on itself. With Docker’s nftables firewall backend, Docker asks you to turn it on (net.ipv4.ip_forward = 1).
  • The VPN’s addresses free. Sessions use 100.64.0.0/16 for tunnels and 100.96.0.0/12 for aliases. A host that already has addresses or routes in those ranges, or runs Tailscale, can’t run the VPN.

If something is missing, the gateway’s VPN tab says what, and so does the site’s Gateways list.

While the VPN is on at the gateway’s site, the gateway keeps:

  • a WireGuard interface, wtn0, and a route to the tunnel addresses;
  • an nftables table, inet witina_vpn, that holds each session to its routes and sends its traffic out from the host’s address;
  • two rules in Docker’s DOCKER-USER chain, so Docker’s firewall lets the VPN’s traffic through;
  • where the host’s firewall filters incoming traffic, as ufw does, two rules that let technicians’ DNS queries reach the gateway.

Its iptables rules all carry the comment witina-vpn.

Technicians reach the site’s network through the host, but not the host itself or the host’s other containers: the gateway forwards their traffic only out of interfaces that lead to a network, never into Docker’s bridges.

Turning the VPN off at the site removes all of this. One kernel setting stays on until the host restarts: connection byte counting (net.netfilter.nf_conntrack_acct), which flow records need.

firewalld, the default firewall on Red Hat Enterprise Linux, Rocky Linux, and AlmaLinux, doesn’t let other programs change its rules. Until wtn0 is in one of its zones, firewalld turns away technicians’ DNS queries to the gateway, so names in the site’s domains don’t resolve. The gateway’s VPN tab shows a warning with the commands. Run them once on the host:

Terminal window
sudo firewall-cmd --permanent --zone=trusted --add-interface=wtn0
sudo firewall-cmd --reload

The trusted zone only stops firewalld dropping what arrives on wtn0. The gateway’s own rules still let technicians reach nothing on the host except its DNS. The warning goes away within a minute.

Restarting the container, or the gateway updating itself, doesn’t drop anyone. The kernel keeps the tunnels up while the gateway restarts, and the new gateway process takes them over. A session’s traffic stops when the session ends, even if the gateway isn’t running.

Removing the container leaves the interface, the table, and the rules in place, for the same reason: the host can’t tell a removal from a restart. On a host that’s done with Witina, restart the host, or run:

Terminal window
sudo ip link delete wtn0
sudo nft delete table inet witina_vpn
sudo ip route delete local 100.64.0.0/24 dev lo table local
for chain in DOCKER-USER INPUT; do
sudo iptables -S "$chain" | grep -- '--comment witina-vpn' | sed 's/^-A /-D /' |
while read -r rule; do sudo iptables $rule; done
done