---
title: 'Tutorial: Deploy Access Server on GKE with the Helm Chart'
canonical: https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-gke-with-helm/
product: Access Server Docs
description: Deploy Access Server on GKE with the Helm chart — Ubuntu nodes, a regional static IP, and one load balancer for TCP 443, TCP 943, and UDP 1194.
---

# Tutorial: Deploy Access Server on GKE with the Helm Chart

## Overview

This tutorial guides you through deploying OpenVPN Access Server on a Google Kubernetes Engine (GKE) Standard cluster using the Access Server Helm chart.

The deployment uses Ubuntu nodes, persistent storage, a regional static IP, and a single mixed-protocol external load balancer that exposes:

- TCP 943 for the Admin Web UI and Client Web UI.
- TCP 443 for web services and OpenVPN TCP connections.
- UDP 1194 for OpenVPN UDP connections.

At the end of the tutorial, you'll have one Access Server pod running in GKE with persistent storage and a stable public IP for VPN and web traffic.

For other Kubernetes platforms, refer to the [Kubernetes Helm tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-kubernetes-with-helm.md). If you'd rather deploy Access Server on a Compute Engine VM instead of Kubernetes, refer to the [Google Cloud VM guide](https://openvpn.net/as-docs/v3/google-cloud.md).

> **Important**
>
> This tutorial requires **GKE Standard** mode.
>
> Access Server needs a privileged container so it can use `/dev/net/tun` and the Linux networking capabilities required for VPN traffic, including `NET_ADMIN`. GKE Autopilot doesn't permit privileged pods except for supported Google Cloud partner workloads. Use a Standard cluster for this deployment.

### How the GKE deployment works

The Helm chart creates:

- One Access Server pod.
- Persistent storage for Access Server data.
- A Kubernetes `LoadBalancer` Service.
- One public IP for TCP 943, TCP 443, and UDP 1194.

Because the Service contains both TCP and UDP ports, this is a mixed-protocol load balancer.

This tutorial requires GKE version 1.34.1-gke.2190000 or newer. Versions from 1.34.1-gke.2190000 support the external IPv4 mixed-protocol load balancer used in this tutorial.

## Prerequisites

Before you begin, ensure you have:

- A Google Cloud project with billing enabled.
- Permission to create GKE clusters, Compute Engine addresses, and load balancers in the project.
- GKE version 1.34.1-gke.2190000 or newer.
- Google Cloud Shell, or a local machine with:
  - Google Cloud CLI (`gcloud`).
  - `kubectl`.
  - Helm.
  - `gke-gcloud-auth-plugin`.

This tutorial uses Cloud Shell, which provides the required Google Cloud command-line tools.

> **Important**
>
> Several commands in this tutorial store values in shell variables. Run the procedures in the same Cloud Shell session when possible.
>
> If Cloud Shell disconnects, set the variables again before running commands that use them.

## Step 1: Create a GKE Standard cluster with Ubuntu nodes

Create a GKE Standard cluster with Ubuntu nodes.

This tutorial uses the `UBUNTU_CONTAINERD` node image. Ubuntu nodes also provide the flexibility to install the Data Channel Offload (DCO) kernel module on the node if your kernel requires it.

1. Sign in to the [Google Cloud console](https://console.cloud.google.com/).
2. Select the project where you want to deploy Access Server.
3. Select **Activate Cloud Shell**.
4. If prompted, select **Continue** and authorize Cloud Shell.
   - A terminal opens at the bottom of the page.
5. Confirm the active Google account:

   ```
   gcloud auth list
   ```
6. Confirm the active Google Cloud project:

   ```
   gcloud config get-value project
   ```
7. Set the cluster name and zone.
   - For example:

     ```
     CLUSTER_NAME=access-server-gke
     ZONE=us-west1-b
     REGION=$(echo "$ZONE" | sed -E 's/-[a-z]$//')
     ```
   - Change `access-server-gke` and `us-west1-b` for your environment as needed.
8. Enable the required Google Cloud APIs:

   ```
   gcloud services enable container.googleapis.com compute.googleapis.com
   ```
9. Create a one-node GKE Standard cluster with the `UBUNTU_CONTAINERD` node image:

   ```
   gcloud container clusters create "$CLUSTER_NAME" \
     --zone "$ZONE" \
     --num-nodes 1 \
     --machine-type e2-standard-2 \
     --image-type UBUNTU_CONTAINERD
   ```
10. Configure `kubectl` to use the new cluster:

    ```
    gcloud container clusters get-credentials "$CLUSTER_NAME" --zone "$ZONE"
    ```
11. Check the GKE version:

    ```
    gcloud container clusters describe "$CLUSTER_NAME" \
      --zone "$ZONE" \
      --format='value(currentMasterVersion)'
    ```

    - Ensure the version is 1.34.1-gke.2190000 or newer.
12. Verify that the node is ready:

    ```
    kubectl get nodes -o wide
    ```

    - Example output:

      ```
      NAME                                         STATUS   ROLES    AGE    VERSION                 INTERNAL-IP   EXTERNAL-IP    OS-IMAGE
      gke-access-server-gke-default-pool-abc123    Ready    <none>   106s   v1.36.4-gke.1391002   192.0.2.5     203.0.113.5   Ubuntu 24.04.4 LTS
      ```
13. Confirm that:
    - `STATUS` is Ready.
    - `OS-IMAGE` shows Ubuntu.

> **Important**
>
> GKE Standard lets you select an Ubuntu node image, while this Access Server deployment requires capabilities unavailable to a normal Autopilot workload.

## Step 2: Reserve a regional static IP

Reserve a regional static IP in the same region as your GKE cluster.

Using a reserved address keeps the load balancer's public IP stable if the Kubernetes Service changes.

> **Important**
>
> Use a regional address. A global address won't work with the external `LoadBalancer` Service used in this tutorial. GKE Service-based external load balancers use regional IP addresses; global static IP addresses are used with different load-balancing resources such as Ingress.

1. Confirm the region variable:

   ```
   echo "$REGION"
   ```

   - If the variable is empty because you opened a new Cloud Shell session, set it again:

     ```
     REGION=us-west1
     ```
2. Reserve a regional static IP named `as-ip` and store the reserved address in the IP variable

   ```
   gcloud compute addresses create as-ip --region "$REGION"
   IP=$(gcloud compute addresses describe as-ip --region "$REGION" --format='value(address)')
   ```
3. Display the address:

   ```
   echo "$IP"
   ```

   - Example output:

     ```
     203.0.113.15
     ```

Keep this Cloud Shell session open. Later steps use the `$IP` variable.

## Step 3: Add the Access Server Helm chart repository

Add the `as-helm-chart` repository to Helm.

1. Add the repository:

   ```
   helm repo add as-helm-chart https://openvpn.github.io/as-helm-chart
   ```
2. Verify that the Access Server Helm Chart is available:

   ```
   helm search repo as-helm-chart
   ```

   - Example output:

     ```
     NAME                         CHART VERSION   APP VERSION   DESCRIPTION
     as-helm-chart/openvpn-as     0.2.1           latest        A Helm chart for OpenVPN Access Server
     ```
   - Your chart version may differ from the example.

## Step 4: Install Access Server with the Helm Chart

Install Access Server and configure the GKE load balancer.

The deployment uses:

- `privileged=true` so Access Server can use the TUN device and required networking capabilities.
- The regional static IP from Step 2.
- `cloud.google.com/l4-rbs: "enabled"` to create a backend service-based load balancer.
- `externalTrafficPolicy: Local` to preserve the original client IP address.

The backend service-based load balancer is required for the mixed-protocol Service that handles TCP and UDP traffic on the same public IP.

1. Install Access Server with the GKE load balancer settings:

   ```
   helm install my-openvpn-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 Helm chart creates a Deployment with one Access Server pod and persistent storage.
   - The Helm value for the load balancer annotation produces:

     ```
     cloud.google.com/l4-rbs: "enabled"
     ```

     This tells GKE to use a backend service-based regional external passthrough Network Load Balancer rather than the legacy target-pool implementation.
   - `externalTrafficPolicy: Local` ensures that incoming traffic is sent only to a node with a ready Access Server pod and preserves the original client source IP.
2. Watch the pod startup:

   ```
   kubectl get pods -w
   ```

   - Example output:

     ```
     NAME                                         READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6fc84554b6-rfc2p    0/1     Running   0          41s
     ```
3. Wait until the pod shows:

   ```
   1/1   Running
   ```
4. Press `Ctrl+C` to stop watching.
5. Watch the Access Server Service:

   ```
   kubectl get svc my-openvpn-as-openvpn-as -w
   ```

   - Example output:

     ```
     NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)
     my-openvpn-as-openvpn-as     LoadBalancer   203.0.113.10   203.0.113.15  943:31313/TCP,443:30193/TCP,1194:32206/UDP
     ```
6. Confirm that:
   - `EXTERNAL-IP` displays the regional static IP from Step 2.
   - TCP 943 is exposed.
   - TCP 443 is exposed.
   - UDP 1194 is exposed.
7. Press `Ctrl+C` to stop watching.
8. Verify the Helm installation:

   ```
   helm status my-openvpn-as
   ```

   - Example output:

     ```
     NAME: my-openvpn-as
     LAST DEPLOYED: Sat Oct 10 15:29:32 2026
     NAMESPACE: default
     STATUS: deployed
     REVISION: 1
     DESCRIPTION: Install complete
     RESOURCES:
     ==> v1/PersistentVolumeClaim
     NAME                           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
     my-openvpn-as-openvpn-as-pvc   Bound    pvc-e0c635a1-2a94-47dd-8159-75e0cafb481c   5Gi        RWO            standard-rwo   <unset>                 81s

     ==> v1/Service
     NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGE
     my-openvpn-as-openvpn-as   LoadBalancer   203.0.113.10    203.0.113.15  943:32442/TCP,443:31756/TCP,1194:31721/UDP   81s

     ==> v1/Deployment
     NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
     my-openvpn-as-openvpn-as   0/1     1            0           82s

     ==> v1/Pod(related)
     NAME                                        READY   STATUS    RESTARTS   AGE
     my-openvpn-as-openvpn-as-6fc84554b6-rfc2p   0/1     Running   0          82s
     ```
9. Store the pod name in a variable:

   ```
   POD=$(kubectl get pods -o name | grep openvpn-as | head -1)
   ```
10. Use the `sacli` command-line tool to verify the Access Server version:

    ```
    kubectl exec -it "$POD" -- /bin/bash sacli version
    ```

    - Example output:

      ```
      3.2.3 (8444f22d)
      ```

For more information about `sacli`, refer to [Tutorial: An Intro to the sacli Command-Line Utility](https://openvpn.net/as-docs/v3/tutorials/tutorial--sacli-command-line-tool.md).

## Step 5: Find the Admin Web UI URL

Use the regional static IP to determine the Admin Web UI URL.

1. Use the IP variable to display the Admin Web UI URL:

   ```
   echo "https://${IP}:943/admin"
   ```

   - Example output:

     ```
     https://203.0.113.15:943/admin
     ```
   - You'll use this URL after retrieving the temporary administrative password.

## Step 6: Find the temporary administrative password

The default Access Server administrative username is `openvpn`. The initial temporary password is written to the Access Server container logs.

1. Use the pod name variable to find the generated password:

   ```
   kubectl logs "$POD" | grep -i pass
   ```

   - Example output:

     ```
     Auto-generated pass = "JaaxMWUlE53b". Setting in db...
     ```
2. Record the temporary password.

### If the password doesn't appear in the logs

Set a new password with `sacli`, replacing `<STRONG_PASSWORD>` with a strong password:

```
kubectl exec -it "$POD" -- /bin/bash sacli \
  --user openvpn \
  --new_pass '<STRONG_PASSWORD>' \
  SetLocalPassword
```

## Step 7: Sign in to the Admin Web UI

Sign in and complete the initial Access Server setup.

1. Open a web browser.
2. Navigate to `https://EXTERNAL-IP:943/admin`.

   > **Important**
   >
   > Ensure you use **https** in the URL.
3. Continue through the browser's certificate warning. Access Server initially uses a self-signed web certificate. Replace it with a trusted certificate for normal use. Refer to [SSL Certificates](https://openvpn.net/as-docs/v3/ssl-certificates.md).

## Step 8: Configure the public hostname or IP address

> **Note**
>
> The Access Server configuration may initially contain a private address. VPN clients connecting from outside your Google Cloud network need a publicly reachable IP address or a hostname whose DNS record points to the regional static IP.
>
> We recommend using a hostname for production deployments.

Configure the address that VPN clients use to reach Access Server.

We recommend using a fully qualified domain name (FQDN) with a DNS A record that resolves to your regional static IP. You can also use the static public IP directly.

### Configure the address in the Admin Web UI

1. Sign in to the Admin Web UI.
2. Select **VPN Server.**
3. Under **Server Address**, enter the `EXTERNAL-IP` or your domain name in **DNS hostname (or IP address)**.
4. Select **Save** and **Restart**.

### Configure the address with sacli

You can also configure the static IP from Cloud Shell.

1. Ensure the `REGION` variable is set.
2. Retrieve the regional static IP again. This ensures `$IP` is available even if Cloud Shell disconnected after Step 2:

   ```
   IP=$(gcloud compute addresses describe as-ip \
     --region "$REGION" \
     --format='value(address)')
   ```
3. Store the pod name:

   ```
   POD=$(kubectl get pods -o name | grep openvpn-as | head -1)
   ```
4. Set the Access Server hostname/IP value:

   ```
   kubectl exec -it "$POD" -- /bin/bash sacli --key "host.name" --value "$IP" ConfigPut
   ```
5. Restart the Access Server services:

   ```
   kubectl exec -it "$POD" -- /bin/bash sacli start
   ```

## Uninstall Access Server and remove the GKE resources

Use this procedure when you no longer need the deployment.

> **Important**
>
> Removing the Helm release and deleting the cluster permanently removes the deployed Access Server environment and deletes the persistent volume. Preserve any Access Server data you need before continuing. Refer to [Tutorial: How to Back Up Access Server Configuration](https://openvpn.net/as-docs/v3/tutorials/tutorial--configuration-backup.md).

1. Uninstall the Access Server Helm release:

   ```
   helm uninstall my-openvpn-as
   ```
2. Check the regional static IP:

   ```
   gcloud compute addresses describe as-ip --region "$REGION" --format='value(address,status,users)'
   ```
3. After the load balancer releases the address, its status changes from `IN_USE` to `RESERVED`.
4. If you no longer need the regional static IP, delete it:

   ```
   gcloud compute addresses delete as-ip --region "$REGION"
   ```
5. Delete the GKE cluster:

   ```
   gcloud container clusters delete "$CLUSTER_NAME" --zone "$ZONE"
   ```
6. Confirm the deletion when prompted.

## Limitations and known issues

The following limitations apply when you deploy Access Server from a container image on GKE:

- **Failover mode:** This feature isn't supported.
- **Layer 2 bridging:** This mode isn't supported.
- **Fixed license keys:** This license model isn't supported. Fixed keys can become invalid because a container deployment doesn't provide a persistent hardware fingerprint.
- **Data Channel Offload (DCO):** DCO depends on the Linux kernel running on the GKE node. Containers use the host kernel, so DCO must be available there.
  - **Linux kernel 6.16 or newer:** Access Server 3.2.0 and newer uses the DCO implementation based on upstream Linux kernel 6.16+. If the GKE node kernel includes DCO support, Access Server can use it without an additional module.
  - **Linux kernel older than 6.16:** Install the DCO module on the Ubuntu GKE node before Access Server can use it. Refer to the [DCO tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--turn-on-openvpn-dco.md) and [DCO overview](https://openvpn.net/as-docs/v3/openvpn-data-channel-offload.md).
- **Performance:** The additional container and Kubernetes networking layers can add latency and overhead. Evaluate performance requirements before using this architecture for heavily loaded deployments.
- **PAM authentication:** Avoid PAM authentication for this deployment because user credentials stored inside the container aren't persistent.
- **Logs:** Access Server sends logs to the container output. Retrieve them with:

  ```
  kubectl logs <POD_NAME>
  ```

For more information about container deployment behavior, refer to the [Docker guide](https://openvpn.net/as-docs/v3/docker.md).

## Troubleshooting

[Submit a support ticket](https://support.openvpn.com/) if you need assistance.

Start by checking the pod and Service status:

1. Check the Access Server pod:

   ```
   kubectl get pods
   ```
2. If the pod doesn't start or become ready, describe it:

   ```
   kubectl describe pod <POD_NAME>
   ```
3. Review its logs:

   ```
   kubectl logs <POD_NAME>
   ```
4. For load balancer problems, inspect the Service and its events:

   ```
   kubectl describe svc my-openvpn-as-openvpn-as
   ```

Use the following common issues to identify the next step.

| Issue | Cause or resolution |
| --- | --- |
| `ErrImagePull` | Kubernetes couldn't retrieve the Access Server container image. Verify the image is available and that outbound network access works. |
| `CrashLoopBackOff` | Verify the pod is running with `privileged` access and can use `NET_ADMIN` and `/dev/net/tun`. |
| `LoadBalancerMixedProtocolNotSupported` | Ensure the cluster is running GKE `1.34.1-gke.2190000` or newer and that the Service includes `cloud.google.com/l4-rbs: "enabled"`. |
| Service `EXTERNAL-IP` stays `<pending>` | Describe the Service and review its Events section. Verify the regional static IP, GKE version, and backend service-based load balancer configuration. |
| `kubectl get nodes` returns `No resources found` | Verify that you created a GKE Standard cluster rather than an Autopilot cluster and that `kubectl` is using the intended cluster context. |
| Pod remains `Pending` | Verify that the node has sufficient CPU and memory and that the persistent volume can attach. |
| Pod stays `0/1` Running for more than five minutes | Check the pod events and logs. Verify `privileged`, `NET_ADMIN`, and `/dev/net/tun` access. |
| Client connects, but the connection profile contains a private IP | Configure the public hostname or regional static IP in Step 8, then issue an updated connection profile as needed. |

> **Note**
>
> If you create the Service with `cloud.google.com/l4-rbs: "enabled"`, GKE documentation notes that the legacy Service controller can sometimes emit a warning that mixed protocol isn't supported even though the newer controller successfully provisions the mixed-protocol load balancer. Verify whether the external IP and forwarding rules were created before treating that warning alone as a failure.

For additional Google Cloud troubleshooting, refer to the [GCP FAQ](https://openvpn.net/as-docs/faq-gcp.md).

## Related resources

- [Tutorial: Deploy Access Server on Kubernetes with Helm](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-on-kubernetes-with-helm.md)
- [Google Cloud VM guide](https://openvpn.net/as-docs/v3/google-cloud.md)
- [Google Cloud VPN solution](https://openvpn.net/google-cloud-vpn/)
- [Docker guide](https://openvpn.net/as-docs/v3/docker.md)
- [DCO tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--turn-on-openvpn-dco.md)
- [DCO overview](https://openvpn.net/as-docs/v3/openvpn-data-channel-offload.md)
- [GCP FAQ](https://openvpn.net/as-docs/faq-gcp.md)

---

Prev: [Tutorial: Connect Your AI Agent to Access Server Using the MCP Server](https://openvpn.net/as-docs/v3/tutorials/tutorial--access-server-mcp-server.md)
Next: [Key Terms](https://openvpn.net/as-docs/v3/tutorials/glossary.md)
Index: https://openvpn.net/as-docs/llms.txt
