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:
- The subnet that we want to use for our VPN clients is
172.16.16.0/20
which will give us a total of 4094 different IP addresses ranging from172.16.16.1
to172.16.31.255
. - The private key of our server is
OIviMX9BPHk1w/bvsXW0Qc2/mY3+HS3iS31aEtsn+Uc=
and the corresponding public key (though not explicitly shown in the configuration) isCSB59ZuD/YVwKWRpVfRpzhirVxfAr36E5770/JDqDx4=
. - The
PostUp
andPostDown
commands are necessary to make sure the VPN host correctly forwards our packages. - We have one peer which configured via its public key
AOIzLd2C71DtY8DWgUfuMllRNa0iR1O3tO2WbFO7ICU
and assigned the internal IP172.16.16.10
to that client.
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:
- The first (and only) entry in the
initContainers
is responsible for replacing theENI
placeholder value in thePostUp
andPostDown
sections of the WireGuard configuration files with the actual name of the network interface.- The entries from the Secret are mounted as files into the directory
/etc/wireguard-secret/
- While executing the init container the
ENI
placeholder value will be replaced by the actual name of the network interface. The resulting file will be stored into/etc/wireguard/wg0.conf
which is the standard configuration file loaded by the WireGuard server.
- The entries from the Secret are mounted as files into the directory
- The
wireguard-config
volume (which is mounted as/etc/wireguard/
in both the init container and the actual container running the server) uses anemptyDir
volume, which is re-created every time the pod is restarted. It only exists so that both the init container and the application container can access the same resources (the configuration file). - Some additional tweaks are needed in the
securityContext
section so that our pod will be able to add new network interfaces and update theipconfig
firewall rules.
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:
- The UDP port
51820
is forwarded to thewireguard
Pod in the same namespace (which we have created using the Deployment shown earlier). - Setting the
type
of the Service toLoadBalancer
will make it available at the public IP address of our Kubernetes clusrter.
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:
[Interface]
section- The
PrivateKey
in the[Interface]
section corresponds to thePublicKey
that has been configured in the server configuration file as peer, as well as theAddress
. - We explicitly define the DNS server to be used as
1.1.1.1
(the free Cloudflare DNS server) as our Kubernetes server doesn’t bring its own DNS server. You’re free to chose any other DNS server as well.
- The
[Peer]
section- By setting the
AllowedIPs
to0.0.0.0/0
we force the complete traffic from the clients computer to be routed through the VPN server. If you only want the VPN to be active for certain IP addresses you can define other blocks here (as explained in the previous article).
- By setting the
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.