Setting Up a Wireguard Vpn Using Kubernetes



I’ve been playing with a home Kubernetes setup and slowly moving my docker images across. So far it’s been fairly straight-forward but wireguard presented a challenge… The post from Christian Seifert @ https://www.perdian.de/blog/2022/02/21/setting-up-a-wireguard-vpn-using-kubernetes/ finally got me up and running so posting here for my records…

The other piece of really useful information was this post on stackoverflow: What’s the difference between ClusterIP, NodePort and LoadBalancer service types in Kubernetes?

One more tip - the AllowedIPs parameter is different for the server and the clients.

Christian’s post:

In another 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

We want to fire up a WireGuard server as “just another Pod” inside Kubernetes.

Luckily for us the team of LinuxServer.io has provided a Docker image with all the basic installation already prepared, that we just need to configure and deploy as a Pod inside Kubernetes.

While the Docker container can work out of the box without much additional configuration (which will automatically create the wg0.conf file), I prefer to manually configure the server keys as well as the clients in a dedicated configuration file, so this will look a little bit different that the basic configuration from 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 into Kubernetes.

We’ll make the WireGuard configuration file available within a Kubernetes Secret which we’ll later mount as files into the Deployment:

---
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 value in the PostUp and PostDown section needs to be replaced vith the actual network interface name. We’ll do that in the actual Deployment.

The Deployment will now setup 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"
          volumeMounts:
            - name: wireguard-config
              mountPath: /etc/wireguard/
              readOnly: true
          securityContext:
            privileged: true
            capabilities:
              add:
                - NET_ADMIN

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

      imagePullSecrets:
        - name: docker-registry

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 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.