Tutorial: Deploy Access Server on Kubernetes with Helm
Overview
This tutorial guides you through deploying Access Server using a Helm Chart on a Kubernetes cluster. Kubernetes (K8s) is an open-source container orchestration platform that automates deployment, scaling, and management of containerized applications.
Use cases
Audience | Use case |
|---|---|
Developers | Run and test Access Server in containers. |
IT admins and DevOps | Manage production VPN environments at scale. |
Organizations | Build reliable, scalable cloud-native platforms. |
Where Kubernetes can run
On a laptop: Minikube, Kind, or Docker Desktop.
On-premises: Your own servers.
Cloud: Google Kubernetes Engine (GKE), Amazon EKS, Azure AKS, DigitalOcean Kubernetes, IBM Cloud Kubernetes Service, and others.
Prerequisites
Before proceeding, ensure your environment meets the following requirements:
Kubernetes installed.
kubectl installed.
Helm installed.
Connect to the console and get root privileges.
Add the repository to your system:
helm repo add as-helm-chart https://openvpn.github.io/as-helm-chartVerify the Access Server Helm Chart is available:
helm search repo as-helm-chartExample output:
# helm search repo as-helm-chart NAME CHART VERSION APP VERSION DESCRIPTION as-helm-chart/openvpn-as 0.1.1 latest A Helm chart for OpenVPN Access Server
Option A: Install with default values
Install the Access Server Helm Chart with default settings:
helm install my-openvpn-as as-helm-chart/openvpn-asThe Access Server Helm Chart installs on one of your pods in your Kubernetes cluster.
Check the pod status:
kubectl get podsExample output
# kubectl get pods NAME READY STATUS RESTARTS AGE my-openvpn-as-openvpn-as-6b474c9757-jmjlc 1/1 Running 0 2m36s
Verify the installation:
helm status my-openvpn-as --show-resourcesExample output:
# helm status my-openvpn-as --show-resources NAME: my-openvpn-as LAST DEPLOYED: Wed Sep 3 17:06:26 2025 NAMESPACE: default STATUS: deployed REVISION: 1 RESOURCES: ==> v1/Secret NAME TYPE DATA AGE my-openvpn-as-openvpn-as-poststart Opaque 1 2d23h ==> v1/PersistentVolumeClaim NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE my-openvpn-as-openvpn-as-pvc Bound pvc-256c79ba-97e9-474b-8b7c-8008187bb2fb 5Gi RWO do-block-storage <unset> 2d23h ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-openvpn-as-openvpn-as LoadBalancer 10.108.92.231 203.0.113.5 943:30479/TCP,443:31010/TCP,1194:31020/UDP 2d23h ==> v1/Deployment NAME READY UP-TO-DATE AVAILABLE AGE my-openvpn-as-openvpn-as 1/1 1 1 2d23h ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE my-openvpn-as-openvpn-as-6b474c9757-jmjlc 1/1 Running 0 2d23h
Use the
Pod Nameand thesaclitool to check Access Server:kubectl exec -it <Pod Name> -- /bin/bash sacli versionReplace
<Pod Name>with your pod name. From our example:my-openvpn-as-openvpn-as-6b474c9757-jmjlc.
Example output:
# kubectl exec -it my-openvpn-as-openvpn-as-6b474c9757-jmjlc -- /bin/bash sacli version 3.2.1 (abc12345)
Option B: Install with custom values
Use this option if you need a custom image version, service type, or storage size.
Go to ArtifactHub and select Default Values.
Copy the contents of the
values.yamlfile.Create a new custom values file:
nano my-values.yamlPaste the copied content and edit as needed. Key settings:
image: repository: openvpn/openvpn-as tag: latest pullPolicy: IfNotPresent service: type: LoadBalancer ports: admin: 943 tcp: 443 udp: 1194 persistence: enabled: true size: 5Gi storageClass: accessMode: ReadWriteOnceCommon customizations:
Setting
Example
Use case
image.tag3.2.1-abc12345-Ubuntu26Pin to a specific AS version.
service.typeNodePortBare-metal environments without a load balancer.
persistence.size20GiLarger storage for high-traffic deployments.
Save and exit (
Ctrl+X, thenY).Install with your custom values file:
helm install my-openvpn-as as-helm-chart/openvpn-as -f my-values.yamlVerify the installation:
helm status my-openvpn-as --show-resources
You've installed Access Server, and the container is running. You can now sign in to the Admin Web UI, a web-based GUI for managing your VPN server, with or without Linux knowledge.
The Admin Web UI is available at https://EXTERNAL-IP:943/admin.
Retrieve the
EXTERNAL-IP:kubectl get svcExample output:
# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.108.64.1 <none> 443/TCP 24h my-openvpn-as-openvpn-as LoadBalancer 10.108.79.160 203.0.113.5 943:31851/TCP,443:31305/TCP,1194:30865/UDP 4m1sThe
EXTERNAL-IPof this Access Server is203.0.113.5.
The default admin username is openvpn. The temporary password is in the container logs.
Get the pod name:
kubectl get podsExample output:
# kubectl get pods NAME READY STATUS RESTARTS AGE my-openvpn-as-openvpn-as-6b474c9757-jmjlc 1/1 Running 0 5m53s
Retrieve the logs using the pod name:
kubectl logs <Pod Name>Replace
<Pod Name>with your name from the previous command. Examplemy-openvpn-as-openvpn-as-6b474c9757-jmjlc.
The Access Server Initial Configuration Tool output displays.
Scroll through the output to find:
Auto-generated pass = "<password>". Setting in db...Use the password with the openvpn username to sign in.
Open a web browser.
Navigate to
https://EXTERNAL-IP:943/admin.Select through the security warning. Access Server uses a self-signed SSL certificate by default. We recommend replacing it with a signed certificate. See SSL Certificates.
Enter openvpn as the username with the temporary password from Step 4.
Review and accept the EULA to proceed to the Admin Web UI.
For end-user devices to connect to your VPN server, update the hostname or public IP address in Access Server:
Sign in to the Admin Web UI.
Select Configuration → Network Settings.
Enter the
EXTERNAL-IPor your domain name in the Hostname or IP Address field.Select Save Settings and Update Running Server.
When deploying Access Server via Helm Chart in your Kubernetes Cluster, you can run custom scripts to perform custom configuration, such as creating users, configuring rules, installing Web SSL certificates, etc
Here's an example of running sacli commands to create a new user with a password and grant them admin privileges. By following the procedure for our example below, you would push the following Bash Script:
#!/bin/sh
echo "Custom setup"
echo "[INFO] Running default post-start script for OpenVPN Access Server..."
# Waiting for AS service initialization
until /usr/local/openvpn_as/scripts/sacli status 2>/dev/null |grep -q '"api": "on"'
do
sleep 2
done
sacli --user "admin" --key "type" --value "user_connect" UserPropPut
sacli --user "admin" --new_pass "secure123" SetLocalPassword
sacli --user "admin" --key "prop_superuser" --value "true" UserPropPut
sacli startAdd the Helm Chart Repository to your system:
helm repo add as-helm-chart https://openvpn.github.io/as-helm-chartGo to the ArtifactHub where the Access Server Helm Chart is allocated and click on the
DEFAULT VALUESoption on the right panel.Copy the content of the
values.yamlfile.Create a new file (example:
my-values.yaml) where you'll be deploying your Access Server Helm Chart:nano my-values.yamlPaste the copied contents from the
values.yamlfile.Edit your new file and the
postStartscript section from this:postStart: enabled: false # Optional: inline override customScript: ""To this:
postStart: enabled: true # Optional: inline override customScript: | #!/bin/sh echo "Custom setup" echo "[INFO] Running default post-start script for OpenVPN Access Server..." # Waiting for AS service initialization until /usr/local/openvpn_as/scripts/sacli status 2>/dev/null |grep -q '"api": "on"' do sleep 2 done sacli --user "admin" --key "type" --value "user_connect" UserPropPut sacli --user "admin" --new_pass "secure123" SetLocalPassword sacli --user "admin" --key "prop_superuser" --value "true" UserPropPut sacli startSave and exit (Ctrl+x, then y).
Install the Access Server Helm Chart with your new custom file. (Example:
my-values.yaml.)helm install my-openvpn-as as-helm-chart/openvpn-as -f my-values.yamlAccess Server will be successfully deployed with the custom commands pushed by the Bash script.
Verify that Access Server was installed correctly by identifying the pod name:
helm status my-openvpn-as --show-resourcesExample output:
# helm status my-openvpn-as --show-resources NAME: my-openvpn-as LAST DEPLOYED: Wed Sep 3 17:06:26 2025 NAMESPACE: default STATUS: deployed REVISION: 1 RESOURCES: ==> v1/Secret NAME TYPE DATA AGE my-openvpn-as-openvpn-as-poststart Opaque 1 2d23h ==> v1/PersistentVolumeClaim NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE my-openvpn-as-openvpn-as-pvc Bound pvc-256c79ba-97e9-474b-8b7c-8008187bb2fb 5Gi RWO do-block-storage <unset> 2d23h ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-openvpn-as-openvpn-as LoadBalancer 10.108.92.231 203.0.113.5 943:30479/TCP,443:31010/TCP,1194:31020/UDP 2d23h ==> v1/Deployment NAME READY UP-TO-DATE AVAILABLE AGE my-openvpn-as-openvpn-as 1/1 1 1 2d23h ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE my-openvpn-as-openvpn-as-6b474c9757-jmjlc 1/1 Running 0 2d23h
And using the
Pod Nameand thesaclitool to check Access Server:kubectl exec -it <Pod Name> -- /bin/bash sacli UserPropGetReplace
<Pod Name>with your pod name. From our example:my-openvpn-as-openvpn-as-6b474c9757-jmjlc.
Example output:
# kubectl exec -it my-openvpn-as-openvpn-as-6b474c9757-jmjlc -- /bin/bash sacli UserPropGet { "__DEFAULT__": { "prop_autogenerate": "true", "type": "user_default" }, "admin": { "prop_superuser": "true", "pvt_password_digest": "$P$zIggWugxu9qv/LZid+np+Q==$0+RZiz4UK6y4c1+A3aG8gBBS08sFAmX3x23U+vxqP1c=", "type": "user_compile" }, "openvpn": { "prop_superuser": "true", "pvt_password_digest": "$P$zYL2GZ3tRzUDw0/rWj6EPA==$QNQmZLQqkR0T1o4drQVkCGLDopr4tY0TIerl2yMmRo0=", "type": "user_compile", "user_auth_type": "local" } }
Run the command below to uninstall the Access Server Helm Chart:
helm delete my-openvpn-asRun the command below to remove the Access Server Helm Chart completely:
helm repo remove as-helm-chart
The following are known issues or limitations if you deploy Access Server from a Docker image:
Failover mode: This feature isn't supported.
Layer 2 (bridging): This mode isn't supported.
Fixed license keys: This license key model isn't supported. Using fixed keys can cause license invalidation because the hardware specification fingerprint isn't persistent.
DCO: DCO support depends on the Linux kernel version running on your Docker host:
Linux kernel 6.16 or newer: The latest Access Server Docker image ships with Access Server 3.2.2, which includes DCO support built into the kernel. No additional installation is needed on the Docker host as Access Server uses DCO automatically.
Linux kernel older than 6.16: To use DCO, you need to install it manually on the Docker host before running the container. See Step 1: Install DCO for instructions.
Clustering: You can use Access Servers, deployed from Docker images, to build cluster functionality with the following limitations:
You can only run one cluster node per Host system at a time.
Hosts must be available directly from the internet, not via a load balancer, proxy, or ingress controller.
Performance: The additional abstraction layer can cause performance degradation and increase latency. We don't recommend deploying highly loaded installations using Docker.
PAM authentication method: We recommend avoiding PAM as your authentication method because user credentials stored inside the container aren't persistent.
Logs: Access Server forwards logs to Docker, so it can't handle logging in the Access Server configuration. See the Docker logging documentation to set up rotation, forwarding, etc.
IPv6: We don't recommend Access Server inside a Docker container if you plan to use IPv6 for VPN clients because IPv6 support in the Docker network toolset is limited/experimental.
Submit a support ticket if you need assistance.
Check pod status:
kubectl get podsIf a pod fails to start:
kubectl describe pod <Pod Name> kubectl logs <Pod Name>This displays events, errors, and useful debugging information.
Common issues:
ErrImagePull— Docker image not found or network issue.CrashLoopBackOff— Service not starting correctly; checkpostStartscripts.