Running OpenVPN in Kubernetes
I condemn Russian invasion to Ukraine. I hope the war ends soon. I wish Ukrainians can take their country back and start rebuilding. I wish there was no more suffering.
There is another, less bloody but still important war happening right now. The informational war between Putin and regular residents of Russia. It's been going on for a really long time but escalated dramatically in the last few weeks. Thousands of people were arrested for participating in anti-war protests. Many, if not all independent news sources were blocked so that Putin can continue spreading lies through the government-controlled channels without facing any criticism. And the sad part is that Putin appears to be winning this war. Many Russians are now brainwashed. I've been watching for years how many of my acquaintances become angrier and more and more radicalized. Thanfully my close friends (those few who are still in Russia) were spared and still have their critical thinking intact. But for how long?
So I've decided to help them and set up a VPN service which will help them to get access to free information.
OpenVPN
I've decided to do it using OpenVPN. Primarily because I've used it before and was somewhat familiar with it. But also because it can work over https on tcp port 443 which should hopefully make it a bit harder to detect & block.
And, since I already had a hammer Kubernetes cluster, I've decided to run it there.
Docker image
I could not find an up-to-date and supported image for OpenVPN so I've built my own here. It is based on Gentoo and the Dockerfile is in my sundry repo.
Server setup
CA and keys
While OpenVPN supports several authorization/authentication methods, I've decided to use a fairly simple way of setting up PKI (public key infrastructure). It is possible to set PKI via unsecure channel, but I had a secure one so I've decided to just generate certificate authority along with server and client keys in one place and then just distribute generated files as appropriate.
I've used easy-rsa for this:
alias easyrsa=/usr/share/easy-rsa/easyrsa easyrsa init-pki easyrsa build-ca nopass # Put your server name instead of example.com easyrsa build-server-full example.com nopass # Place distinguishable client name instead of 'username1' easyrsa build-client-full username1 nopass easyrsa build-client-full username2 nopass easyrsa gen-dh
The set of command created CA (with public and private keys), server key and two pairs of private/public keys for two clients. OpenVPN server would need server key, clients will need their private/public pair, and everyone will need CA public certificate. All of these are placed into not-so-trivial folder hierarchy under pki/
.
The following script took care of packaging necessary for server files in a flat directory and storing it as Kubernetes secret:
#!/bin/bash mkdir -p etc-openvpn cp pki/ca.crt etc-openvpn cp pki/issued/example.com.crt etc-openvpn cp pki/private/example.com.key etc-openvpn cp pki/dh.pem etc-openvpn kubectl -n vpn create secret generic etc-openvpn --dry-run=client --from-file=etc-openvpn -o yaml > openvpn-config.yaml kubectl apply -f openvpn-config.yaml kubectl -n vpn rollout restart deployment openvpn-tcp
Server config
For now I've just set OpenVPN via TCP on port 30749. It had to be in range 30000-31000 so I can use NodePort
service later on. It does not use TLS auth so may be vulnerable to DOS, and does not provide adequate protection from MITM. The goal of the service is to circumvent censorship, not to provide perfect security. I expect my clients to use https and other secure protocols, though DNS may be still vulnerable.
I've used following server config:
proto tcp port 30749 dev tun ca /etc/openvpn/ca.crt cert /etc/openvpn/example.com.crt key /etc/openvpn/example.com.key dh /etc/openvpn/dh.pem topology subnet # Give clients IP in 10.185.162.0/24 server 10.185.162.0 255.255.255.0 # Set default gateway through VPN push "redirect-gateway def1 bypass-dhcp" push "dhcp-option DNS 8.8.8.8" # Allows same client to connect more than once. duplicate-cn keepalive 10 120 max-clients 10 # Enable compression compress lz4-v2 push "compress lz4-v2" allow-compression yes user openvpn group openvpn persist-key persist-tun log /dev/stdout # Logging settings verb 4 mute 20 # run /etc/openvpn/up.sh when the connection is set up. This will set up NAT. script-security 2 up "/bin/sh /etc/openvpn/up.sh"
up.sh
script sets up NAT so that clients have Internet connectivity:
#!/bin/sh set -e echo "1" > /proc/sys/net/ipv4/ip_forward iptables -t nat -A POSTROUTING -s 10.185.162.0/24 -o eth0 -j MASQUERADE
The config and up.sh
are put into etc-openvpn
folder and therefore put into etc-openvpn
Secret by the above script.
In order for iptables
command in up.sh
to work, ip_tables
module needs to be loaded on the host. I did this by running modprobe ip_tables
on all of my Kubernetes hosts, and by putting "ip_tables" to /etc/modules-load.d/iptables.conf
.
Kubernetes deployment
Here's how my deployment currently looks like:
apiVersion: apps/v1 kind: Deployment metadata: name: openvpn-tcp namespace: vpn labels: app: openvpn-tcp spec: replicas: 1 selector: matchLabels: app: openvpn-tcp strategy: type: Recreate template: metadata: labels: app: openvpn-tcp spec: containers: - name: openvpn-tcp image: vrusinov/openvpn:2.5.2-r1 command: ["/bin/openvpn"] args: - "/etc/openvpn/openvpn.conf" ports: - name: ovpn containerPort: 30749 protocol: TCP resources: requests: cpu: "0.01" memory: "16Mi" limits: cpu: "1" memory: "32Mi" volumeMounts: - name: openvpn-config-volume mountPath: /etc/openvpn/ securityContext: capabilities: add: - NET_ADMIN - SYS_ADMIN privileged: true securityContext: runAsUser: 0 runAsGroup: 0 # 394 is the ID of the `openvpn` user in my image. fsGroup: 394 volumes: - name: openvpn-config-volume secret: defaultMode: 420 secretName: etc-openvpn
The main downside is that since OpenVPN needs to create new network interfaces, it had to run in privileged mode with some dangerous capabilities. In theory it should have been possible to run in non-privileged mode just with capabilities but I didn't manage to figure it out yet.
Kubernetes service
I've used NodePort
service so that clients can connect to the server:
apiVersion: v1 kind: Service metadata: name: openvpn-tcp namespace: vpn spec: selector: app: openvpn-tcp ports: - name: ovpn-tcp protocol: TCP port: 30749 targetPort: 30749 nodePort: 30749 type: NodePort
Client setup
I've created following template for the client config:
client dev tun proto tcp4 nobind resolv-retry infinite remote example.com 30749 # Try to preserve some state across restarts. persist-key persist-tun comp-lzo verb 3
It's only the template because it's missing certificates. I've created make-ovpn.sh
script to create per-user .ovpn file which can be then shared with clients and used by either Linux or Windows clients:
#!/bin/bash # Creates ovpn file with bundled keys client_name=$1 F="${client_name}.ovpn" cp openvpn-client.conf $F if [[ ! -f pki/private/${client_name}.key ]] then easyrsa build-client-full $client_name nopass fi echo "<ca>" >> $F cat pki/ca.crt | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >> $F echo "</ca>" >> $F echo "<cert>" >> $F cat pki/issued/${client_name}.crt | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >> $F echo "</cert>" >> $F echo "<key>" >> $F cat pki/private/${client_name}.key >> $F echo "</key>" >> $F
Now, I could create all necessary keys and client-specific config by running ./make-ovpn test
. This would create test.ovpn
file which can be later used by clients like so: openvpn test.ovpn
.
Future work
This was set up in a relatively quick and dirty way. There are many possible incremental improvements, especially for security and hardening:
- Set up server certificate verification to prevent MITM.
- Set up tls-auth.
- Limit egress from OpenVPN using
NetworkPolicy
. - Set up UDP server.
- Set up server working through Ingress on port 443.
- Remove dangerous privileges from OpenVPN container and try to run it as a regular user.
- Set up IPv6.
- Create
down.sh
script to clean up NAT properly. - Tighten up OpenVPN deployment - set up probes, run more than one instance, etc.
Comments