OpenVPN server in a Docker container running on Kubernetes.
Pre-requisites
We are using our Kubernetes homelab in this article.
Configuration files used in this article can be found on GitHub. Clone the following repositories:
$ git clone https://github.com/lisenet/kubernetes-homelab.git $ git clone https://github.com/lisenet/docker-openvpn.git
The Goal
We want to be able to access Kubernetes homelab subnet 10.11.1.0/24
from the Internet using a VPN connection on an Android device. This would allow us to access internally hosted services like:
- https://grafana.apps.hl.test
- https://prometheus.apps.hl.test
We also want to route all traffic through the VPN server (push default gateway).
The Plan
- Build a container image for the latest version of OpenVPN.
- Generate OpenVPN configuration files and certificates.
- Deploy OpenVPN on Kubernetes.
- Configure a destination NAT rule on Mikrotik router.
- Test VPN access from an Android client.
Build OpenVPN Docker Image (Optional)
This step is optional.
TL;DR: use lisenet/openvpn:latest
docker image.
At the time of writing, OpenVPN image provided by kylemanna/openvpn:2.5
has not been updated since 2020, which makes it slightly out of date if you ask me.
We will build a new image and push it to Docker Hub.
$ git clone https://github.com/lisenet/docker-openvpn.git $ cd ./docker-openvpn $ docker build -t openvpn:latest . $ docker tag openvpn:latest lisenet/openvpn:latest $ docker push lisenet/openvpn:latest
Generate OpenVPN Configuration Files and Certificates
Pull scripts from GitHub:
$ git clone https://github.com/lisenet/docker-openvpn.git $ cd ./docker-openvpn/
Create a Kubernetes namespace:
$ kubectl create namespace openvpn
Set environment variables to be used to generate OpenVPN config:
$ export VPN_HOSTNAME="vpn.example.com" $ export DNS_SERVER="10.11.1.2"
Generate basic OpenVPN config:
$ ./bin/generate-config.sh
Change ownership of ovpn0
folder so that we can write to it:
$ sudo chown -R "${USER}:${USER}" ./ovpn0/
Generate a client config (can be repeated for any new client):
$ export CLIENT_NAME=android $ ./bin/add-client.sh
Set the Kubernetes secrets. Prepend with REPLACE=true
to update the existing ones:
$ ./bin/set-secrets.sh
Note: VPN config, certificates and keys are stored in the ovpn0
directory on the machine that was used to run the commands.
$ kubectl get cm -n openvpn NAME DATA AGE ccd0 0 1h istio-ca-root-cert 1 1h kube-root-ca.crt 1 1h ovpn0-conf 2 1h
$ kubectl get secrets -n openvpn NAME TYPE DATA AGE default-token-wd9q8 kubernetes.io/service-account-token 3 1h ovpn0-cert Opaque 1 1h ovpn0-key Opaque 1 1h ovpn0-pki Opaque 3 1h
Deploy OpenVPN on Kubernetes
Content of the file openvpn-deployment.yaml
can be seen below.
Note a couple of things:
- We define
initContainers
to configure IP forwarding on the pod. - We use a service type of
LoadBalancer
to receive an IP address from MetalLB.
--- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: "This priority class should be used for OpenVPN service pods only." --- apiVersion: v1 kind: Service metadata: name: openvpn namespace: openvpn labels: app: openvpn spec: selector: app: openvpn ports: - name: openvpn port: 31194 protocol: TCP targetPort: openvpn type: LoadBalancer loadBalancerIP: 10.11.1.53 --- apiVersion: apps/v1 kind: Deployment metadata: name: openvpn namespace: openvpn labels: app: openvpn spec: replicas: 1 selector: matchLabels: app: openvpn template: metadata: name: openvpn labels: app: openvpn spec: initContainers: - image: busybox:1.36 imagePullPolicy: IfNotPresent name: init-busybox command: - sh - -c - sysctl -w net.ipv4.ip_forward=1 && sysctl -w net.ipv4.conf.all.forwarding=1 securityContext: capabilities: add: - NET_ADMIN privileged: true containers: - image: lisenet/openvpn:latest imagePullPolicy: Always name: openvpn ports: - containerPort: 1194 name: openvpn protocol: TCP # The kubelet will send the first readiness probe 5 seconds after the container starts. # This will attempt to connect to the openvpn container on port 1194. If the probe succeeds, # the Pod will be marked as ready. The kubelet will continue to run this check every 30 seconds. readinessProbe: tcpSocket: port: 1194 initialDelaySeconds: 5 periodSeconds: 30 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 # In addition to the readiness probe, this configuration includes a liveness probe. # The kubelet will run the first liveness probe 15 seconds after the container starts. # Similar to the readiness probe, this will attempt to connect to the container on port 1194. # If the liveness probe fails, the container will be restarted. livenessProbe: tcpSocket: port: 1194 initialDelaySeconds: 15 periodSeconds: 60 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 2 resources: limits: cpu: 1000m memory: 128Mi requests: cpu: 10m memory: 16Mi securityContext: capabilities: add: - NET_ADMIN volumeMounts: - mountPath: /etc/openvpn/pki/private name: ovpn0-key - mountPath: /etc/openvpn/pki/issued name: ovpn0-cert - mountPath: /etc/openvpn/pki name: ovpn0-pki - mountPath: /etc/openvpn name: ovpn0-conf - mountPath: /etc/openvpn/ccd name: ccd0 priorityClassName: high-priority restartPolicy: Always terminationGracePeriodSeconds: 30 volumes: - name: ovpn0-key secret: defaultMode: 384 secretName: ovpn0-key - name: ovpn0-cert secret: defaultMode: 420 secretName: ovpn0-cert - name: ovpn0-pki secret: defaultMode: 384 secretName: ovpn0-pki - configMap: defaultMode: 420 name: ovpn0-conf name: ovpn0-conf - configMap: defaultMode: 420 name: ccd0 name: ccd0 ...
Deploy OpenVPN:
$ kubectl apply -f openvpn-deployment.yaml
List pods and services to verify.
$ kubectl get pods -n openvpn NAME READY STATUS RESTARTS AGE openvpn-8f548449f-cbqxm 1/1 Running 0 43m
$ kubectl get svc -n openvpn NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE openvpn LoadBalancer 10.107.175.151 10.11.1.53 31194:31844/TCP 43m
Configure Mikrotik Router
We want to allow connections from the internet to the OpenVPN service IP 10.11.1.53
. In this case, we have to configure a destination address translation rule on our Mikrotik router.
Create a destination NAT rule:
/ip firewall nat add chain=dstnat \ action=dst-nat \ in-interface=ether1_isp \ dst-port=31194 \ to-addresses=10.11.1.53 \ to-ports=31194 \ protocol=tcp \ comment="Allow public to OpenVPN"
At this point, assuming that the DNS record for VPN_HOSTNAME
points to the IP address of the router, we should be able to access the OpenVPN server on vpn.example.com:31194 from the Internet.
Test VPN Access from Android Device
Download OpenVPN Connect client from Google Play.
Import ./ovpn0/android.ovpn
client configuration file and connect to the VPN server.
Check pod logs.
$ kubectl -n openvpn logs $(kubectl -n openvpn get pods -o name) Checking IPv6 Forwarding Sysctl error for disable_ipv6, please run docker with '--sysctl net.ipv6.conf.all.disable_ipv6=0' Sysctl error for default forwarding, please run docker with '--sysctl net.ipv6.conf.default.forwarding=1' Sysctl error for all forwarding, please run docker with '--sysctl net.ipv6.conf.all.forwarding=1' Running 'openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd ' 2022-02-16 01:36:44 DEPRECATED OPTION: --cipher set to 'AES-256-CBC' but missing in --data-ciphers (AES-256-GCM:AES-128-GCM). Future OpenVPN version will ignore --cipher for cipher negotiations. Add 'AES-256-CBC' to --data-ciphers or change --cipher 'AES-256-CBC' to --data-ciphers-fallback 'AES-256-CBC' to silence this warning. 2022-02-16 01:36:44 OpenVPN 2.5.4 x86_64-alpine-linux-musl [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD] built on Nov 15 2021 2022-02-16 01:36:44 library versions: OpenSSL 1.1.1l 24 Aug 2021, LZO 2.10 2022-02-16 01:36:44 Diffie-Hellman initialized with 2048 bit key 2022-02-16 01:36:44 Outgoing Control Channel Authentication: Using 384 bit message hash 'SHA384' for HMAC authentication 2022-02-16 01:36:44 Incoming Control Channel Authentication: Using 384 bit message hash 'SHA384' for HMAC authentication 2022-02-16 01:36:44 TUN/TAP device tun0 opened 2022-02-16 01:36:44 /sbin/ip link set dev tun0 up mtu 1500 2022-02-16 01:36:44 /sbin/ip link set dev tun0 up 2022-02-16 01:36:44 /sbin/ip addr add dev tun0 10.8.0.1/24 2022-02-16 01:36:44 Could not determine IPv4/IPv6 protocol. Using AF_INET 2022-02-16 01:36:44 Socket Buffers: R=[87380->87380] S=[16384->16384] 2022-02-16 01:36:44 Listening for incoming TCP connection on [AF_INET][undef]:1194 2022-02-16 01:36:44 TCPv4_SERVER link local (bound): [AF_INET][undef]:1194 2022-02-16 01:36:44 TCPv4_SERVER link remote: [AF_UNSPEC] 2022-02-16 01:36:44 GID set to nogroup 2022-02-16 01:36:44 UID set to nobody 2022-02-16 01:36:44 MULTI: multi_init called, r=256 v=256 2022-02-16 01:36:44 IFCONFIG POOL IPv4: base=10.8.0.2 size=252 2022-02-16 01:36:44 MULTI: TCP INIT maxclients=1024 maxevents=1028 2022-02-16 01:36:44 Initialization Sequence Completed 2022-02-16 01:37:15 Outgoing Control Channel Authentication: Using 384 bit message hash 'SHA384' for HMAC authentication 2022-02-16 01:37:15 Incoming Control Channel Authentication: Using 384 bit message hash 'SHA384' for HMAC authentication 2022-02-16 01:37:15 TCP connection established with [AF_INET]10.11.1.35:15520 2022-02-16 01:37:15 10.11.1.35:15520 TLS: Initial packet from [AF_INET]10.11.1.35:15520, sid=27615aef 0df1177d 2022-02-16 01:37:15 10.11.1.35:15520 VERIFY OK: depth=1, CN=vpn.example.com 2022-02-16 01:37:15 10.11.1.35:15520 VERIFY OK: depth=0, CN=android 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_VER=3.git::d3f8b18b:Release 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_PLAT=android 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_NCP=2 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_TCPNL=1 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_PROTO=30 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_CIPHERS=AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_IPv6=0 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_AUTO_SESS=1 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_GUI_VER=net.openvpn.connect.android_3.2.6-7729 2022-02-16 01:37:15 10.11.1.35:15520 peer info: IV_SSO=webauth,openurl 2022-02-16 01:37:15 10.11.1.35:15520 WARNING: 'link-mtu' is used inconsistently, local='link-mtu 1588', remote='link-mtu 1587' 2022-02-16 01:37:15 10.11.1.35:15520 WARNING: 'comp-lzo' is present in local config but missing in remote config, local='comp-lzo' 2022-02-16 01:37:15 10.11.1.35:15520 Control Channel: TLSv1.3, cipher TLSv1.3 TLS_AES_256_GCM_SHA384, peer certificate: 2048 bit RSA, signature: RSA-SHA256 2022-02-16 01:37:15 10.11.1.35:15520 [android] Peer Connection Initiated with [AF_INET]10.11.1.35:15520 2022-02-16 01:37:15 android/10.11.1.35:15520 MULTI_sva: pool returned IPv4=10.8.0.2, IPv6=(Not enabled) 2022-02-16 01:37:15 android/10.11.1.35:15520 MULTI: Learn: 10.8.0.2 -> android/10.11.1.35:15520 2022-02-16 01:37:15 android/10.11.1.35:15520 MULTI: primary virtual IP for android/10.11.1.35:15520: 10.8.0.2 2022-02-16 01:37:15 android/10.11.1.35:15520 Data Channel: using negotiated cipher 'AES-256-GCM' 2022-02-16 01:37:15 android/10.11.1.35:15520 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key 2022-02-16 01:37:15 android/10.11.1.35:15520 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key 2022-02-16 01:37:15 android/10.11.1.35:15520 SENT CONTROL [android]: 'PUSH_REPLY,route 10.11.1.0 255.255.255.0,dhcp-option DNS 10.11.1.2,dhcp-option DNS 10.11.1.3,comp-lzo no,route-gateway 10.8.0.1,topology subnet,ping 10,ping-restart 60,ifconfig 10.8.0.2 255.255.255.0,peer-id 0,cipher AES-256-GCM' (status=1) 2022-02-16 01:37:15 android/10.11.1.35:15520 PUSH: Received control message: 'PUSH_REQUEST' 2022-02-16 01:38:04 android/10.11.1.35:15520 Connection reset, restarting [0] 2022-02-16 01:38:04 android/10.11.1.35:15520 SIGUSR1[soft,connection-reset] received, client-instance restarting
Credits
Docker image lisenet/openvpn:latest
is based on kylemanna/docker-openvpn
Dockerfile.
OpenVPN Kubernetes configuration is based on a Helm chart provided by suda/k8s-ovpn-chart
.
References
https://github.com/kylemanna/docker-openvpn
https://github.com/suda/k8s-ovpn-chart
Hello,
thank you for article,if we want to scale open vpn pod,How it works? I mean is it possible to know which pod will host.Then pods has connection between of them