www.perdian.de Home Blog Conference talks Publications

Setting up a WireGuard VPN using Kubernetes

In a previous article I described how to set up a VPN using WireGuard on a dedicated EC2 instance at AWS.

If you happen to run a Kubernetes cluster then the configuration becomes even simpler, as we don’t have to set up a dedicated EC2 instance but can build upon the infrastructure provided by Kubernetes.

Server installation

Our goal for this article is to run a WireGuard server as “just another pod” inside a Kubernetes cluster.

Luckily for us the team of LinuxServer.io has provided a Docker image with all the installation details already prepared to configure and deploy a WireGuard pod into a Kubernetes cluster.

While the Docker container can work out of the box without much additional configuration (all we need is a preconfigured wg0.conf configuration file), I prefer to manually configure the server keys as well as the clients in a dedicated configuration file, so the result will look a bit different from the basic configuration at LinuxServer.io.

WireGuard configuration file

The WireGuard configuration file looks very similar (if not identical) to the one from the example using a dedicated EC2 instance:

[Interface]
Address = 172.16.16.0/20
ListenPort = 51820
PrivateKey = OIviMX9BPHk1w/bvsXW0Qc2/mY3+HS3iS31aEtsn+Uc=
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ENI -j MASQUERADE
PostUp = sysctl -w -q net.ipv4.ip_forward=1
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ENI -j MASQUERADE
PostDown = sysctl -w -q net.ipv4.ip_forward=0

[Peer]
# Example Peer 1
PublicKey = AOIzLd2C71DtY8DWgUfuMllRNa0iR1O3tO2WbFO7ICU=
AllowedIPs = 172.16.16.10

Let’s break this down a bit:

Kubernetes deployment descriptors

Now we’re ready to deploy our WireGuard VPN server as a Kubernetes pod.

We’ll define the WireGuard configuration within a Kubernetes Secret which we’ll later mount as files into the pod:

---
apiVersion: v1
kind: Secret
metadata:
  name: wireguard
  namespace: example
type: Opaque
stringData:
  wg0.conf.template: |
    [Interface]
    Address = 172.16.16.0/20
    ListenPort = 51820
    PrivateKey = OIviMX9BPHk1w/bvsXW0Qc2/mY3+HS3iS31aEtsn+Uc=
    PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ENI -j MASQUERADE
    PostUp = sysctl -w -q net.ipv4.ip_forward=1
    PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ENI -j MASQUERADE
    PostDown = sysctl -w -q net.ipv4.ip_forward=0

    [Peer]
    # Example Peer 1
    PublicKey = AOIzLd2C71DtY8DWgUfuMllRNa0iR1O3tO2WbFO7ICU=
    AllowedIPs = 172.16.16.10

Note that we have named the entry wg0.conf.template and not wg0.conf as the name of the network interface through which our traffic needs to be routed to the rest of the network is not yet known to us. The ENI placeholder value in the PostUp and PostDown section needs to be replaced with the actual network interface name. We’ll do that in an init container defined in the actual Deployment.

The Deployment will now set up the actual pod that is running the WireGuard server:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wireguard
  namespace: example
spec:
  selector:
    matchLabels:
      name: wireguard
  template:
    metadata:
      labels:
        name: wireguard
    spec:
      initContainers:
        # The exact name of the network interface needs to be stored in the
        # wg0.conf WireGuard configuration file, so that the routes can be
        # created correctly.
        # The template file only contains the "ENI" placeholder, so when
        # bootstrapping the application we'll need to replace the placeholder
        # and create the actual wg0.conf configuration file.
        - name: "wireguard-template-replacement"
          image: "busybox"
          command: ["sh", "-c", "ENI=$(ip route get 8.8.8.8 | grep 8.8.8.8 | awk '{print $5}'); sed \"s/ENI/$ENI/g\" /etc/wireguard-secret/wg0.conf.template > /etc/wireguard/wg0.conf; chmod 400 /etc/wireguard/wg0.conf"]
          volumeMounts:
            - name: wireguard-config
              mountPath: /etc/wireguard/
            - name: wireguard-secret
              mountPath: /etc/wireguard-secret/

      containers:
        - name: "wireguard"
          image: "linuxserver/wireguard:latest"
          ports:
            - containerPort: 51820
          env:
            - name: "TZ"
              value: "Europe/Berlin"
            # Keep the PEERS environment variable to force server mode
            - name: "PEERS"
              value: "example"
            - name: "PEERDNS"
              value: "8.8.8.8"
          volumeMounts:
            - name: wireguard-config
              mountPath: /config/
              readOnly: true
          securityContext:
            privileged: true
            capabilities:
              add:
                - NET_ADMIN

      volumes:
        - name: wireguard-config
          emptyDir: {}
        - name: wireguard-secret
          secret:
            secretName: wireguard

Let’s drill down a bit here:

Now we can apply both the Secret and the Deployment and Kubernetes will launch the WireGuard server for us. We can verify this by logging in directly into the WireGuard container:

$ kubectl exec -n example -it deployment/wireguard -- bash
root@wireguard-b6bccf9b6-b2lbs:/# wg
interface: wg0
public key: CSB59ZuD/YVwKWRpVfRpzhirVxfAr36E5770/JDqDx4=
private key: (hidden)
listening port: 51820

peer: AOIzLd2C71DtY8DWgUfuMllRNa0iR1O3tO2WbFO7ICU=
allowed ips: 172.16.16.10/32

We can see that the server is correctly running on port 51820 and that our peer is prepared.

In order to expose the post 51820 outside the Kubernetes cluster so that our clients are able to access them we need to add a Service resource:

---
apiVersion: v1
kind: Service
metadata:
  name: wireguard
  namespace: example
spec:
  type: LoadBalancer
  ports:
    - name: wireguard
      port: 51820
      protocol: UDP
      targetPort: 51820
  selector:
    name: wireguard

The setup is pretty straightforward:

Let’s check that our service is up and running:

$ kubectl get services -n example
NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
...
wireguard            LoadBalancer   10.11.12.13     1.2.3.4       51820:31099/UDP   2m
...

The public IP 1.2.3.4 is now listening to the WireGuard port ``51820` forwarding it to the actual WireGuard server.

Our server setup is now complete and we have a running WireGuard VPN server.

Client installation

Now we need to prepare our WireGuard client so that it can connect to our server.

The following WireGuard client configuration will do the trick:

[Interface]
PrivateKey = oG2sa0u9qJGWGC8+vtXRsLtI0IxXKtaYzGlpPzqD91k=
Address = 172.16.16.10/20
DNS = 1.1.1.1

[Peer]
PublicKey = CSB59ZuD/YVwKWRpVfRpzhirVxfAr36E5770/JDqDx4=
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
PersistentKeepalive = 25

Let’s look at the details:

After establishing the connection through the WireGuard client we can again connect to the WireGuard server and can see the WireGuard status:

$ kubectl exec -n example -it deployment/wireguard -- bash
root@wireguard-b6bccf9b6-b2lbs:/# wg
interface: wg0
public key: CSB59ZuD/YVwKWRpVfRpzhirVxfAr36E5770/JDqDx4=
private key: (hidden)
listening port: 51820

peer: AOIzLd2C71DtY8DWgUfuMllRNa0iR1O3tO2WbFO7ICU=
  endpoint: 10.42.0.1:27008
  allowed ips: 172.16.16.10/32
  latest handshake: 53 seconds ago
  transfer: 93.27 KiB received, 149.15 KiB sent

The peer has successfully connected and the VPN connection is established.

Conclusion

Moving the WireGuard VPN endpoint from an EC2 instance into a Kubernetes cluster simplifies the setup even more in case a Kubernetes cluster is already existing. WireGuard simply becomes another service to be hosted inside the cluster and the overhead of installing (and maintaining!) yet another EC2 instance is gone. Furthermore, we’re now free to install WireGuard in other Kubernetes scenarios that may not even be hosted at AWS.

Source: https://www.perdian.de/blog/2022/02/21/setting-up-a-wireguard-vpn-using-kubernetes/