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