Stephen Levine

Stephen Levine

Share this post

Stephen Levine
Stephen Levine
WireGuard on Kubernetes

WireGuard on Kubernetes

Road-warrior-style VPN server

Stephen Levine's avatar
Stephen Levine
Apr 27, 2020
Share

(See comments on Hacker News.)

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

This means that if your Kubernetes nodes are running Ubuntu 20.04 LTS or later, they come with WireGuard installed as a kernel module that will automatically load when needed. If your cluster permits to you to set CAP_NET_ADMIN on containers in pods, you can run a road-warrior-style WireGuard server in Kubernetes without modifying the node.

Example deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wireguard
  namespace: myvpn
  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. On production cluster, you would want to map 51820/udp to a load balancer with a Service.

You may notice that the container is running tail -f /dev/null. This is intentional -- we're only using the pod to configure the host kernel and hold open a network namespace.

Example Dockerfile (sclevine/wg):

FROM ubuntu:focal
RUN apt-get update && \
  apt-get install -y --no-install-recommends \
    iproute2 iptables wireguard-tools && \
  rm -rf /var/lib/apt/lists/*

This Dockerfile installs the WireGuard userspace utility and setup script without the kernel module and associated dependencies.

Next, generate a key pair for the server and each peer:

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

Store the server's private key in a Secret:

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

Example ConfigMap for the server config (/etc/wireguard/wg0.conf):

apiVersion: v1
kind: ConfigMap
metadata:
  name: wg0-conf
  namespace: myvpn
  labels:
    app: wireguard
data:
  wg0.conf: |
    [Interface]
    Address = 10.1.30.1/24,fdb0:5dfe:70d8:7f0b::1/64
    ListenPort = 51820
    PostUp = wg set %i private-key /etc/wireguard/wg0.key; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
    PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -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 config assumes the VPN subnets will be 10.1.30.0/24 (IPv4) and fdb0:5dfe:70d8:7f0b::/64 (IPv6). I picked the private IPv4 address arbitrarily and generated the IPv6 ULA here. You may or may not need to specify the MTU. The private key (/etc/wireguard/wg0.key) is loaded dynamically from the Secret.

Client configuration:

[Interface]
PrivateKey = <client private key>
Address = 10.1.30.3/24, fdb0:5dfe:70d8:7f0b::3/64
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

To access the server, open a shell with:

$ kubectl -n myvpn exec -it deploy/wireguard bash

Check out WireGuard 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

Confirm WireGuard is listening:

root@wireguard-6dbf689864-5cnxb:/# ss -lun 'sport = :51820'
State  Recv-Q   Send-Q   Local Address:Port   Peer Address:Port Process
UNCONN 0        0        0.0.0.0:51820        0.0.0.0:*
UNCONN 0        0        [::]:51820           [::]:*
Share

Ready for more?

© 2025 Stephen Levine
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share