WireGuard first appears in Linux kernel 5.6, but Ubuntu 20.04 LTS includes a backport in its 5.4 kernel.

So if your K8s nodes are running Ubuntu 20.04 LTS, they come with WireGuard installed as a kernel module that will automatically load when needed. This means that if you can set CAP_NET_ADMIN on containers, you can run a road-warrior-style WireGuard server in K8s without making changes to the node.

Here's my deployment:

apiVersion : apps/v1 kind : Deployment metadata : name : wireguard labels : app : wireguard spec : replicas : 1 strategy : type : Recreate selector : matchLabels : app : wireguard template : metadata : labels : app : wireguard spec : restartPolicy : Always volumes : - name : wg0-key secret : secretName : wg0-key - name : wg0-conf configMap : name : wg0-conf containers : - name : wireguard image : sclevine/wg imagePullPolicy : Always lifecycle : postStart : exec : command : [ " wg-quick" , " up" , " wg0" ] preStop : exec : command : [ " wg-quick" , " down" , " wg0" ] command : [ " tail" , " -f" , " /dev/null" ] volumeMounts : - name : wg0-key mountPath : /etc/wireguard/wg0.key subPath : wg0.key readOnly : true - name : wg0-conf mountPath : /etc/wireguard/wg0.conf subPath : wg0.conf readOnly : true ports : - containerPort : 51820 hostPort : 51820 protocol : UDP securityContext : capabilities : add : - NET_ADMIN

I set this up on a single-node K3s cluster, so I used hostPort and the Recreate strategy. In a real cluster, you would want to map 51820/udp to wherever you need with a Service .

Also, you might have noticed that the container is just running tail -f /dev/null . This is intentional -- we're just using the pod to configure the host kernel and hold open a network namespace. Is this a K8s anti-pattern? Maybe. But I think the alternative is to not use K8s.

Here's the Dockerfile for sclevine/wg :

FROM ubuntu:focal as builder RUN apt-get update && \ apt-get install -y wireguard && \ rm -rf /var/lib/apt/lists/* FROM ubuntu:focal RUN apt-get update && \ apt-get install -y iproute2 iptables && \ rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/bin/wg /usr/bin/wg-quick /usr/bin/

This gets you the WireGuard userspace utility and setup script without the kernel module and associated dependencies (like build-essentials ). You could build a smaller image with Alpine, but I decided to use the same build of wg as the Ubuntu Focal host node.

You'll need to generate a key pair for the server and each peer:

$ wg genkey | tee private.key | wg pubkey > public.key

To store the server's private key in a Secret :

$ kubectl -n my-vpn create secret generic wg0-key --from-file = wg0.key = ./path/to/private.key

(This assumes you created the Deployment in namespace my-vpn .)

Here's an example ConfigMap for the server config ( /etc/wireguard/wg0.conf ):

apiVersion : v1 kind : ConfigMap metadata : name : wg0-conf labels : app : wireguard data : wg0.conf : | [Interface] Address = 10.1.30.1/24,fdb0:5dfe:70d8:7f0b::1/48 ListenPort = 51820 PostUp = wg set %i private-key /etc/wireguard/wg0.key; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE MTU = 1500 SaveConfig = false [Peer] # first peer PublicKey = <client #1 public key> AllowedIPs = 10.1.30.3/32,fdb0:5dfe:70d8:7f0b::3/128 [Peer] # second peer PublicKey = <client #2 public key> AllowedIPs = 10.1.30.4/32,fdb0:5dfe:70d8:7f0b::4/128

This assumes the VPN subnets will be 10.1.30.0/24 (ipv4) and fdb0:5dfe:70d8:7f0b::/48 (ipv6). I picked these arbitrarily. You may not need to manually set the MTU. The private key ( /etc/wireguard/wg0.key ) is loaded dynamically from a Secret .

For WireGuard clients, I use this config:

Address = 10.1.30.3/24, fdb0:5dfe:70d8:7f0b::3/48 DNS = 1.1.1.1, 1.0.0.1 [Peer] PublicKey = <server public key> AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = <k8s node/LB ip>:51820 PersistentKeepalive = 25

If you want to debug the server, you can get shell access with:

$ kubectl -n my-vpn exec -it $( kubectl -n my-vpn get pods|tail -n 1|cut -f1 -d ' ' ) bash

Then check out the server stats:

root@wireguard-6dbf689864-5cnxb:/# wg interface: wg0 public key: <server public key> private key: ( hidden ) listening port: 51820 peer: <peer 1 public key> endpoint: <peer 1 IP>:24189 allowed ips: 10.1.30.3/32, fdb0:70d8:7f0b:5dfe::3/128 latest handshake: Now transfer: 4.64 MiB received, 53.40 MiB sent

Or confirm WireGuard is listening: