Run OpenVPN Access Server on Kubernetes with Helm
By Rohit Kalbag
Your team already ships everything else through Git, review and a rollout. The VPN is often the exception.
Short answer: To run OpenVPN Access Server on Kubernetes, add OpenVPN's Helm repository, install the openvpn-as chart with a values file, expose ports 943/TCP, 443/TCP and 1194/UDP through a LoadBalancer, and keep state on a persistent volume. The chart needs Kubernetes 1.20 or later and Helm 3.0 or later. A postStart script can apply sacli configuration automatically.

Why run Access Server on Kubernetes
On Kubernetes, the VPN becomes one more release in the pipeline you already trust: versioned in Git, reviewed in a pull request and rolled out like any other workload. OpenVPN's official Helm tutorial names three audiences: developers who want to run and test Access Server in containers, IT and DevOps teams running production VPNs, and organizations building cloud-native platforms. The cluster can be a laptop (Minikube, Kind or Docker Desktop), on-premises servers, or a managed service such as GKE, EKS or AKS.
It is not the right shape for every deployment, and we would rather say so up front. Containers bring a set of specific limits, listed below. The low-risk first step is a laptop cluster: install the chart, try it, and decide with evidence.
Where this fits
If you followed post 1, you have a VM-based server. If your platform already runs on Kubernetes, you can skip the VM and run the same spec as a workload. The two are alternatives, not layers. Choose Helm when you already operate a cluster and want Access Server managed like any other workload. The spec is unchanged: DNS name vpn-dev.example.com, superuser admin1, test user eng-test, and vpn.tls_refresh.interval set to 100.
Prerequisites
kubectland Helm 3 installed, and a running cluster on Kubernetes 1.20 or later (a laptop cluster, on-premises, or a managed service)- A way to expose the service: a LoadBalancer, or NodePort on bare metal
- Read the Kubernetes limits below before committing
Install the chart
helm repo add as-helm-chart https://openvpn.github.io/as-helm-chart helm search repo as-helm-chartStart from the chart's default values.yaml and keep a file like this in Git:
image: repository: openvpn/openvpn-as tag: latest # pin a specific Access Server version in production service: type: LoadBalancer ports: admin: 943 tcp: 443 udp: 1194 persistence: enabled: true size: 5Gi accessMode: ReadWriteOncehelm install platform-dev-as as-helm-chart/openvpn-as -f values.yaml helm status platform-dev-as --show-resourcesSettings worth knowing
|
Setting |
Example |
Purpose |
|---|---|---|
|
|
|
Pin a specific Access Server version (the tutorial's example format) |
|
|
|
Bare-metal clusters without a load balancer |
|
|
|
More storage for high-traffic deployments |
|
|
your node labels |
Control where the pod runs |
|
|
your PVC name |
Use a PVC you created yourself. It survives |
|
|
provider-specific |
Tune how the service is exposed |
|
|
your values |
Pod setup and secret injection |
The scheduling, networking and pod-setup options arrived in chart version 0.2 and later.
Apply the spec with a postStart script
With postStart.enabled=true, the chart runs a script after the container starts. Keep the script and the password in Kubernetes Secrets, not in values.yaml.
postStart: enabled: true secretRefName: platform-dev-as-config # Secret that contains configure.sh envFrom: - secretRef: name: platform-dev-as-credentials # provides ADMIN_PASSWORD#!/bin/bash # configure.sh set -euo pipefail # sacli only works once the service is up, so wait for the API until sacli status | grep -q '"api": "on"'; do sleep 2; done sacli --user admin1 --key type --value user_connect UserPropPut sacli --user admin1 --key prop_superuser --value true UserPropPut sacli --user admin1 --new_pass "$ADMIN_PASSWORD" SetLocalPassword sacli --user eng-test --key type --value user_connect UserPropPut sacli --key host.name --value vpn-dev.example.com ConfigPut sacli --key vpn.tls_refresh.interval --value 100 ConfigPut sacli startCreate both Secrets before installing, for example kubectl create secret generic platform-dev-as-config --from-file=configure.sh, and source the credentials Secret from your secret manager. The chart also accepts an inline script in postStart.customScript.
Sign in and verify
kubectl get svc # find EXTERNAL-IP for platform-dev-as-openvpn-as kubectl get pods kubectl exec -it <pod-name> -- /bin/bash sacli UserPropGetThe Admin UI is at https://EXTERNAL-IP:943/admin, and Access Server serves a self-signed certificate by default, so replace it with a signed one. Without a postStart script, the default admin user is openvpn and the generated password appears in the pod log on the line containing "Auto-generated pass". Point vpn-dev.example.com at the external address.
Reference: GKE
OpenVPN publishes a platform-specific GKE tutorial for the same chart. The differences from the generic install above:
- GKE Standard only. Autopilot does not permit privileged pods. Use GKE 1.34.1-gke.2190000 or newer, which the tutorial gives as the minimum for the mixed TCP and UDP load balancer, and Ubuntu nodes (
--image-type UBUNTU_CONTAINERD). - A regional static IP. Reserve it with
gcloud compute addresses create as-ip --region <region>. A global address will not work with this Service. - Four install flags instead of a values file, shown here with this series' release name:
helm install platform-dev-as as-helm-chart/openvpn-as \ --set securityContext.privileged=true \ --set service.loadBalancerIP="$IP" \ --set-string 'service.annotations.cloud\.google\.com/l4-rbs=enabled' \ --set service.externalTrafficPolicy=LocalThe l4-rbs annotation selects the backend-service-based regional external passthrough load balancer, and externalTrafficPolicy: Local preserves the client source IP. The tutorial then sets the public address with sacli --key host.name --value "$IP" ConfigPut followed by sacli start. Its cleanup deletes the static address and the cluster, and the tutorial warns that deleting the cluster permanently deletes the persistent volume, so back up first.
Production caveats and fixes
- Privileged mode. On Kubernetes 1.31.3 and later, the pod must run privileged to create
/dev/net/tun. Earlier versions needed onlyNET_ADMIN. SetsecurityContext.privileged=true, as the GKE tutorial does, and consider a device plugin as an alternative. - Script changes do not roll pods.
postStartruns only at container start. Editing the Secret changes nothing until you runkubectl rollout restart deployment/platform-dev-as-openvpn-as. - The default rollout causes downtime, and that is correct for one instance.
updateStrategydefaults toRecreate. For a single instance on aReadWriteOncePVC, keep it.RollingUpdatedoes not fit here: if the surge pod lands on another node it hangs on a Multi-Attach error and the rollout stalls, and if it lands on the same node two Access Server instances write to the same/openvpndata at once. The chart's ownvalues.yamlscopesRollingUpdateto multi-replica cluster mode. Use it, withmaxUnavailable: 0, only in cluster mode with persistence disabled. - Draining. Add a
preStophook with a sleep and a matchingterminationGracePeriodSecondsso connections can drain. - Unsupported in container deployments: failover mode, Layer 2 bridging, and fixed license keys, which can be invalidated because the hardware fingerprint is not persistent.
- Clustering. The tutorial limits clustering to one node per host, with hosts reachable directly from the internet rather than through a load balancer, proxy or ingress. That describes classic Access Server clustering, which does not fit Kubernetes. Choose one of two shapes: a single instance, as in this post, or cluster mode inside Kubernetes, where a LoadBalancer Service balances clients across replicas and the pods are not reachable from the internet directly. Cluster mode needs persistence disabled and state kept in an external MySQL database, and this post does not cover it.
- Performance. The container layer can add latency and reduce throughput, so the tutorial does not recommend it for heavily loaded installations. Data Channel Offload is built into Linux 6.16 and later; on older kernels, install it on the host first.
- Authentication, logs, IPv6. PAM authentication is not recommended because credentials in the container are not persistent. Logs go to Docker, so set rotation and forwarding there. IPv6 for VPN clients is not recommended.
- Cleanup.
helm deleteremoves the chart-created PVC and all data on it, because the chart does not mark the PVC to be kept. To keep data across reinstalls, create the PVC yourself and setpersistence.pvcRefName. Back up before you delete the release.
Troubleshoot and uninstall
kubectl get pods kubectl describe pod <pod-name> kubectl logs <pod-name>ErrImagePull means the image was not found or there is a network problem. CrashLoopBackOff means the service is not starting correctly, so check any postStart script first.
To remove everything, including stored data, delete the release. helm delete removes the chart-created PVC and all data on it:
helm delete platform-dev-as # also removes the chart-created PVC and its data helm repo remove as-helm-chartIf you created the PVC yourself and referenced it with persistence.pvcRefName, helm delete leaves it in place. Delete it with kubectl delete pvc <your-pvc-name> only when you want the data gone.
FAQ
Does Access Server on Kubernetes support failover or clustering through a load balancer?
Failover mode is unsupported for container deployments, and classic clustering, where each node is reachable directly from the internet, does not fit Kubernetes. Run a single instance, or use cluster mode inside Kubernetes, where a LoadBalancer balances clients across replicas and state is kept in MySQL with persistence disabled.
Does the pod need to run privileged?
On Kubernetes 1.31.3 and later, yes, to create /dev/net/tun. Earlier versions needed only NET_ADMIN.
How do I change configuration after deployment?
Edit the Secret that holds configure.sh, then run kubectl rollout restart on the deployment, because the script runs only at container start.
Which ports does the chart expose?
943/TCP for the Admin and Client UI, 443/TCP for client connections over TCP, and 1194/UDP for client connections over UDP.
Can I try it without a production cluster?
Yes. The chart runs on a laptop cluster such as Minikube, Kind or Docker Desktop, so you can install it, test the behavior and decide before touching production.
Is running a VPN on Kubernetes worth it?
It is when your team already operates a cluster and wants the VPN managed like any other workload. It is a weaker fit for heavily loaded installations, since the container layer can add latency, and for features that container deployments do not support. Try it on a laptop cluster first.
Why run Access Server on Kubernetes instead of a VM?
If your platform already runs on Kubernetes, the VPN joins the same Git, review and rollout flow as your other workloads, with no separate VM to build and maintain. If you run VMs, the Terraform post is the better fit.
Next in the series
The cluster runs the spec once. In the final post, you keep a VM-based server consistent over time with Puppet.
Try it: Follow the official Helm tutorial or the GKE tutorial, browse the chart repository, or try it with our free plan.
Get started
Pick the stage that matches your stack and start there. When you are ready, try Access Server with our free plan or read the official tutorials.
Start Access Server Free