---
title: Run OpenVPN Access Server on Kubernetes with Helm
description: "Run OpenVPN Access Server on Kubernetes with the official Helm chart: values, persistence, sacli postStart config, and the limits to know first."
image: https://blog.openvpn.net/hubfs/ba9509c7-acdd-4882-a950-336985827166.png
---

- [Blog](https://blog.openvpn.net)
- [Cybersecurity](https://blog.openvpn.net/tag/cybersecurity)
- [Access Server](https://blog.openvpn.net/tag/access-server)

# Run OpenVPN Access Server on Kubernetes with Helm

Nov 12, 2026 •  9 min read

![Illustration of a laptop connected to a stack of servers](https://blog.openvpn.net/hubfs/ba9509c7-acdd-4882-a950-336985827166.png)

Share

- <https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fhelm-kubernetes-access-server&title=Run%20OpenVPN%20Access%20Server%20on%20Kubernetes%20with%20Helm&summary=Run+OpenVPN+Access+Server+on+Kubernetes+with+the+official+Helm+chart%3A+values%2C+persistence%2C+sacli+postStart+config%2C+and+the+limits+to+know+first.&source=>
- <https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fhelm-kubernetes-access-server>
- <https://twitter.com/intent/tweet?url=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fhelm-kubernetes-access-server&text=Run+OpenVPN+Access+Server+on+Kubernetes+with+Helm>
- <https://blog.openvpn.net/vpn-as-code/helm-kubernetes-access-server>

Run OpenVPN Access Server on Kubernetes with Helm

12:18

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.

![OpenVPN Access Server banner](https://blog.openvpn.net/hs-fs/hubfs/banner-preview-desktop.png?width=1624&height=326&name=banner-preview-desktop.png)

## 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](https://blog.openvpn.net/vpn-as-code/terraform-access-server?hsLang=en), 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

- `kubectl` and 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-chart`

Start 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: ReadWriteOnce`

 

`helm install platform-dev-as as-helm-chart/openvpn-as -f values.yaml helm status platform-dev-as --show-resources`

### Settings worth knowing

 

 

| Setting | Example | Purpose |
| --- | --- | --- |
| `image.tag` | `3.2.1-abc12345-Ubuntu26` | Pin a specific Access Server version (the tutorial's example format) |
| `service.type` | `NodePort` | Bare-metal clusters without a load balancer |
| `persistence.size` | `20Gi` | More storage for high-traffic deployments |
| `nodeSelector`, `tolerations`, `affinity` | your node labels | Control where the pod runs |
| `persistence.pvcRefName` | your PVC name | Use a PVC you created yourself. It survives `helm delete`; a chart-created PVC does not |
| `service.annotations`, `service.externalTrafficPolicy`, `service.loadBalancerIP` | provider-specific | Tune how the service is exposed |
| `env`, `envFrom`, `initContainers`, `serviceAccountName` | 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 start`

Create 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 UserPropGet`

The 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](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-gke-with-helm) 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=Local`

 

The `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 only `NET_ADMIN`. Set `securityContext.privileged=true`, as the GKE tutorial does, and consider a device plugin as an alternative.
- **Script changes do not roll pods.** `postStart` runs only at container start. Editing the Secret changes nothing until you run `kubectl rollout restart deployment/platform-dev-as-openvpn-as`.
- **The default rollout causes downtime, and that is correct for one instance.** `updateStrategy` defaults to `Recreate`. For a single instance on a `ReadWriteOnce` PVC, keep it. `RollingUpdate` does 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 `/openvpn` data at once. The chart's own `values.yaml` scopes `RollingUpdate` to multi-replica cluster mode. Use it, with `maxUnavailable: 0`, only in cluster mode with persistence disabled.
- **Draining.** Add a `preStop` hook with a sleep and a matching `terminationGracePeriodSeconds` so 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 delete` removes 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 set `persistence.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-chart`

If 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](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-kubernetes-with-helm.html) or the [GKE tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-gke-with-helm), browse the [chart repository](https://github.com/OpenVPN/as-helm-chart), or [try it with our free plan](https://openvpn.net/access-server/).

 

## 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](https://openvpn.net/access-server/)

## Related posts from OpenVPN

### [![Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example](https://blog.openvpn.net/hubfs/f954367b-e204-43cc-92e6-5711d68ade8d.png) Cybersecurity Nov 10, 2026 Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example](https://blog.openvpn.net/vpn-as-code/terraform-access-server?hsLang=en)

### [![Unlock the Full Potential of Your Access Server](https://blog.openvpn.net/hubfs/Blog%20Single%20Large%20(1).png) Announcements May 29, 2025 Unlock the Full Potential of Your Access Server](https://blog.openvpn.net/unlock-the-full-potential-of-your-access-server?hsLang=en)

### [![VPN as Code: Deploy and Manage OpenVPN Access Server with Terraform, Helm and Puppet](https://blog.openvpn.net/hubfs/ba9509c7-acdd-4882-a950-336985827166.png) Cybersecurity Nov 9, 2026 VPN as Code: Deploy and Manage OpenVPN Access Server with Terraform, Helm and Puppet](https://blog.openvpn.net/vpn-as-code/?hsLang=en)

### Subscribe for Blog Updates

```json
{
  "@context" : "https://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Rohit Kalbag",
    "url" : "https://blog.openvpn.net/author/rohit-kalbag"
  },
  "dateModified" : "2026-11-12T15:14:35.668Z",
  "datePublished" : "2026-11-12T15:14:35.000Z",
  "headline" : "Run OpenVPN Access Server on Kubernetes with Helm",
  "image" : [ "https://blog.openvpn.net/hubfs/ba9509c7-acdd-4882-a950-336985827166.png" ],
  "mainEntityOfPage" : {
    "@id" : "https://blog.openvpn.net/vpn-as-code/helm-kubernetes-access-server",
    "@type" : "WebPage"
  },
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "url" : "https://blog.openvpn.net/hubfs/Dark=True%20Medium.png"
    },
    "name" : "OpenVPN"
  }
}
```