---
title: "Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example"
description: "Deploy OpenVPN Access Server with Terraform: a portable five-part pattern, plus a worked AWS example with cloud-init, Let's Encrypt and caveats."
image: https://blog.openvpn.net/hubfs/f954367b-e204-43cc-92e6-5711d68ade8d.png
---

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

# Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example

Nov 10, 2026 •  12 min read

![cloud around a server](https://blog.openvpn.net/hubfs/f954367b-e204-43cc-92e6-5711d68ade8d.png)

Share

- <https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fterraform-access-server&title=Deploy%20OpenVPN%20Access%20Server%20with%20Terraform%3A%20A%20Portable%20Pattern%20with%20an%20AWS%20Example&summary=Deploy+OpenVPN+Access+Server+with+Terraform%3A+a+portable+five-part+pattern%2C+plus+a+worked+AWS+example+with+cloud-init%2C+Let%27s+Encrypt+and+caveats.&source=>
- <https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fterraform-access-server>
- <https://twitter.com/intent/tweet?url=https%3A%2F%2Fblog.openvpn.net%2Fvpn-as-code%2Fterraform-access-server&text=Deploy+OpenVPN+Access+Server+with+Terraform%3A+A+Portable+Pattern+with+an+AWS+Example>
- <https://blog.openvpn.net/vpn-as-code/terraform-access-server>

Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example

15:31

By Rohit Kalbag

## The VPN is the one server everyone depends on and nobody wants to rebuild from memory. Terraform turns it into a file you can read, review and rerun.

**Short answer:** To deploy OpenVPN Access Server with Terraform, provision one Linux VM, open four ports, give it a stable public address, and let cloud-init install and configure Access Server on first boot. OpenVPN publishes official templates for AWS, Google Cloud and IBM Cloud. The AWS template does this with nine variables and three commands: `terraform init`, `terraform plan` and `terraform apply`.

![banner-preview-desktophttps://marketplace.microsoft.com/en-us/product/openvpn.openvpn-as-app-tiered](https://blog.openvpn.net/hs-fs/hubfs/banner-preview-desktop.png?width=1624&height=326&name=banner-preview-desktop.png)

## Why provision an Access Server with Terraform

A hand-built VPN server holds its history in someone's head. The port rules, the key pair, the address and the first-boot settings live in a console history or in one person's memory, so the server is hard to reproduce and harder to hand over. Expressed as Terraform, the same server becomes reviewable and repeatable. `terraform plan` shows exactly what will change before anything does, a rebuild after a failure is one command, and a second environment such as staging is a different variables file rather than a different procedure. For a gateway that every engineer depends on, that repeatability matters more than the minutes saved at creation time.

## What you get

- A server you can rebuild with one command instead of a runbook.
- Changes you can review with `terraform plan` before they happen.
- A staging copy that differs by a variables file, not by a procedure.
- A starting point published by OpenVPN as official templates, with the limits listed in this post rather than discovered in production.

## The reference environment for these posts

All three posts in this series build the same environment, called **platform-dev access**, from one desired-state spec. Terraform is Day 0: it creates the server and its first admin account. Post two runs the same spec on Kubernetes, and post three keeps it from drifting.

 

 

| Spec item | Value |
| --- | --- |
| DNS name | `vpn-dev.example.com` |
| Admin account | `admin1`, superuser |
| Test user | `eng-test` |
| One server setting | `vpn.tls_refresh.interval` = `100` |

 

 

 

The Terraform template provisions the server and a static address, requests a TLS certificate, and takes the admin username and password as inputs. It does not set `vpn.tls_refresh.interval`, create `eng-test` or configure the DNS name. You apply those after the server is up, with `sacli` and your DNS provider, and post three turns them into enforced state.

## The portable pattern

OpenVPN publishes official templates for AWS, Google Cloud and IBM Cloud, and every provider needs the same five building blocks. The right-hand column shows where each lives in the AWS repository, which is the worked example in this post.

 

 

| Building block | What it must do | In the AWS template |
| --- | --- | --- |
| Network placement | Put the VM in an existing network with a route to the internet | `vpc_id`, `subnet_id` (you supply both; the template creates no network) |
| Image and compute | Boot a Linux VM sized to your user count | `ami.tf`, `aws_instance_type` (default `t3.small`, Ubuntu 26.04 LTS) |
| First-boot bootstrap | Install and initialize Access Server unattended | `user_data.tf` (cloud-init) |
| Address and firewall | Stable public IP; open 943/TCP, 443/TCP, 1194/UDP and 22/TCP | `eip.tf`, `security_groups.tf` |
| Inputs, secrets, outputs | Take credentials and an SSH key pair safely; print the admin and client URLs | `variables.tf` (including `key_name`, the existing EC2 key pair used for SSH), `outputs.tf` |

 

 

 

The GCP and IBM Cloud templates implement the same five blocks, as the comparison below shows. For any other cloud, reproduce these pieces with that provider's resources.

### Also available: GCP and IBM Cloud templates

If you run on Google Cloud or IBM Cloud, start from the official [GCP template](https://github.com/OpenVPN/openvpn-as-gcp-terraform-deploy) or [IBM Cloud template](https://github.com/OpenVPN/openvpn-as-ibm-terraform-deploy) instead of porting the AWS one. All three follow the same flow: one VM, a static public address, first-boot install with the `openvpn-as` package held, and optional Let's Encrypt. They differ in the details that matter before you apply:

 

 

| Item | AWS | Google Cloud | IBM Cloud |
| --- | --- | --- | --- |
| Default size | `t3.small` | `n2d-standard-2` | `cx2-2x4` |
| Default image | Ubuntu 26.04 LTS | Ubuntu 26.04 LTS | Ubuntu 26.04 minimal |
| Network | Existing VPC and subnet required | Creates a VPC and subnet, or uses an existing VPC if `existing_vpc_name` is set | Creates a VPC, subnet and public gateway, or uses an existing VPC if `vpc_existing_name` is set |
| Static address | Elastic IP, optional | Static IP, on by default (`enable_static_ip`) | Floating IP, always created |
| Firewall | Security group; SSH open to `0.0.0.0/0` by default | Four rules, each with an enable flag and a source-range variable. The ranges default to empty, which opens all four ports to `0.0.0.0/0` | Security group with fixed rules open to `0.0.0.0/0` (22, 443, 943 TCP; 1194 UDP) |
| Admin password | You supply it as a variable | Generated; admin user fixed as `openvpn`; shown as a sensitive output | Generated; admin user fixed as `openvpn`; printed in plain text on every apply and `terraform output`; embedded in instance user data |
| Official tutorial | Yes | Repository README only | Repository README only |

 

 

 

Read each repository's variables file before you apply, because defaults such as the Access Server version installed differ between templates. The numbered steps below use AWS; the Terraform workflow of `init`, `plan` and `apply` is the same on all three.

### What happens on first boot

Cloud-init runs a custom script when the instance first starts. It installs and configures Access Server, applying your admin credentials and the Let's Encrypt setting, and it holds the `openvpn-as` package with `apt-mark hold`. The admin credentials are passed as instance user data, not as shell commands: `ovpn-init` reads `admin_user` and `admin_pw` from the EC2 user data at first boot, which is also why the password is visible there. The template creates a security group named `AccessServerSecurityGroup` with these rules, and all outbound traffic is allowed:

 

 

| Port | Protocol | Purpose |
| --- | --- | --- |
| 943 | TCP | Admin Web UI and Client Web UI |
| 443 | TCP | Web interfaces and OpenVPN over TCP |
| 1194 | UDP | OpenVPN over UDP |
| 22 | TCP | SSH administration |

 

 

 

## Prerequisites

- An AWS account with permissions for EC2, security groups and Elastic IPs, plus read access to VPCs, subnets and key pairs
- Terraform, the AWS CLI and Git installed, with credentials configured
- An existing VPC and a public subnet whose route table sends `0.0.0.0/0` to an internet gateway
- An EC2 key pair in the target Region

## Deploy on AWS

**1. Clone the repository and verify your credentials.**

 

`git clone https://github.com/OpenVPN/openvpn-as-aws-terraform-deploy.git cd openvpn-as-aws-terraform-deploy aws sts get-caller-identity`

 

**2. Set the variables.** The tutorial edits `variables.tf`. A `terraform.tfvars` file overrides the same variables without touching the repository's files.

 

`aws_region = "us-east-1" aws_instance_type = "t3.small" vpc_id = "vpc-0123456789abcdef0" subnet_id = "subnet-0123456789abcdef0" key_name = "platform-dev" admin_username = "admin1" enable_lets_encrypt = true use_static_public_ip = true`

 

 

**3. Pass the password outside any file.** The default password is a placeholder that must be replaced before you deploy.

 

`# macOS or Linux export TF_VAR_admin_password='use-a-long-random-value' # Windows PowerShell $env:TF_VAR_admin_password = 'use-a-long-random-value'`

 

 

**4. Restrict SSH** in `security_groups.tf` before applying. The default allows `0.0.0.0/0` on port 22.

 

`ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["203.0.113.10/32"] }`

 

 

**5. Apply.**

 

`terraform init terraform plan terraform apply`

 

 

**6. Wait, then sign in.** First-boot initialization (`ovpn-init`) can take up to five minutes, and sign-in may fail until it finishes. Run `terraform output` to get `admin_ui_url`, sign in, and accept the EULA. Point `vpn-dev.example.com` at the Elastic IP and set it as the hostname in the Admin UI. The Let's Encrypt certificate the template requests covers the public IP only, not that name, so browsers show a certificate name mismatch at `https://vpn-dev.example.com:943`. For a dev environment you can accept that, or request a certificate for the hostname once DNS resolves. The template has already registered the ACME account, so run `sacli --key "acme.domains.0" --value "vpn-dev.example.com" ConfigPut`, then `sacli start`, then `sacli AcmeRequestCertificate`, as described in the [ACME certificate tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--acme-web-ssl-certificates/). Access Server requests one certificate covering both the configured domain and the IP, so the new certificate covers both.

## Verify the deployment

 

`terraform output # admin_ui_url, client_ui_url, openvpnas_user ssh -i /path/platform-dev.pem ubuntu@<instance-public-ip>`

 

Confirm three things. The Admin UI loads at `https://<address>:943/admin`, where `<address>` is the public IP, with a trusted certificate (or a self-signed one if you disabled Let's Encrypt); a hostname shows a name mismatch until you request a hostname certificate. The instance uses the Elastic IP when `use_static_public_ip` is `true`. And the Client UI at `https://<address>:943` lets a user sign in and download OpenVPN Connect with their profile. Then continue with the [Get Started with Access Server](https://openvpn.net/as-docs/v3/get-started-with-access-server.html) tutorial to configure VPN connections.

## Production caveats and fixes

- **The admin password is stored in plaintext** in Terraform state and in EC2 user data, and the output is not marked sensitive. Use `TF_VAR_` variables and keep state in an encrypted remote backend with tight access. Mark the `admin_password` variable `sensitive = true`, and add `sensitive = true` to the `openvpnas_user` output in `outputs.tf`, which hides it from plan and apply output. `terraform output -json` still shows it, and it stays in state, so change the password in the Admin UI after first sign-in.
- **SSH is open by default.** Restrict it to your admin range, as in step 4.
- **There is one instance and no high availability.** The template has no load balancer or scaling group. Treat the server as a pet with backups, not as cattle.
- **Upgrades are manual.** Cloud-init runs `apt-mark hold openvpn-as`, so routine package updates will not change Access Server. The install also pins a version (`--as-version=3.2.3` in `user_data.tf` at the time of writing), so a fresh deploy gets that release until the template is updated, not the latest one. Only `openvpn-as` is held: the same user data enables unattended-upgrades, so the rest of the operating system keeps patching itself.
- **Let's Encrypt needs reachability.** The certificate is requested at first boot for the public IP using the `shortlived` ACME profile. OpenVPN's [ACME tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--acme-web-ssl-certificates/) describes these IP address certificates as lasting 160 hours (just over six days), renewing at three days, so the instance must stay reachable from the internet for every renewal, not just at first boot. With Let's Encrypt disabled, Access Server uses a self-signed certificate. Keep `use_static_public_ip = true` whenever Let's Encrypt is on: with `false`, the certificate is tied to the auto-assigned IP, which can change on stop and start, and the certificate then stops matching.
- `terraform destroy` **deletes users and settings** with the instance. Back up first. To keep the Elastic IP, run `terraform state rm aws_eip.AccessServerEIP` before destroying.
- **GCP and IBM Cloud have their own defaults to review.** The IBM template always opens 22, 443, 943 and 1194 to `0.0.0.0/0` with no variables to narrow them, so restrict the security group rules in the code before applying. The GCP template exposes `tcp_22_source_ranges` and the other source-range variables, but leaving them empty opens all four ports to the internet, so set them to your admin range. Both generate the admin password and use a fixed `openvpn` admin user. IBM Cloud prints the password in plain text on every apply, so protect state, user data and logs, and change the password after first sign-in.

## Adapting the pattern to another provider

If your cloud is not covered by an official template, treat the AWS, GCP and IBM Cloud templates as a checklist rather than code to port:

1. Pick a current Ubuntu LTS image. The AWS, GCP and IBM Cloud templates all use Ubuntu 26.04 LTS.
2. Place the VM in a network with a route to the internet, and allocate a static public address, because the certificate is requested for that address at first boot.
3. Open 943/TCP, 443/TCP and 1194/UDP to your users, and 22/TCP only to your admin range.
4. Deliver the install script through the provider's cloud-init or user-data mechanism, and hold the `openvpn-as` package so upgrades stay deliberate.
5. Take the admin password from a variable or secret store, mark credential outputs sensitive, and store state remotely with encryption.

The details differ by provider, and OpenVPN does not publish templates for clouds beyond AWS, Google Cloud, and IBM Cloud, so have an engineer review the result before you rely on it.

## FAQ

#### **Can I deploy Access Server with Terraform on a cloud other than AWS?**

Yes. OpenVPN publishes official Terraform templates for AWS, Google Cloud and IBM Cloud. For any other provider, the pattern is portable: reproduce the five building blocks above with your provider's network, VM, address, firewall, and secret-handling resources.

#### **How long does an Access Server Terraform deployment take?**

`terraform apply` returns quickly, but initialization on the instance can take up to five minutes before you can sign in.

#### **Is the Terraform template highly available?**

No. It provisions a single EC2 instance. Plan backups and a rebuild procedure instead.

#### **Where should the admin password live?**

Not in a committed file. Pass it with `TF_VAR_admin_password`, and remember it still lands in state and user data, so protect both.

#### **Is it worth it for a single server?**

Usually yes, because the value is the rebuild and the review, not the scale. A single server is exactly the one that is painful to recreate by hand. If you will never rebuild it or change it, the benefit is smaller.

#### **What are the benefits of OpenVPN's official Terraform templates?**

They are published by OpenVPN for AWS, Google Cloud and IBM Cloud, so you start from a working pattern instead of a blank file. You still own the caveats listed in this post, such as the single instance and the plaintext admin password in state.

#### **Why use Terraform instead of installing Access Server by hand?**

A hand install lives in someone's memory. Terraform makes the server reviewable, repeatable and rebuildable with one command, and shows changes with `terraform plan` before they happen.

## Next in the series

The server is up with one admin account. Next, run the same spec on Kubernetes with Helm.

**Try it:** Follow the [official Terraform tutorial](https://openvpn.net/as-docs/v3/tutorials/tutorial--deploy-access-server-aws-terraform.html), browse the [AWS](https://github.com/OpenVPN/openvpn-as-aws-terraform-deploy), [GCP](https://github.com/OpenVPN/openvpn-as-gcp-terraform-deploy) or [IBM Cloud](https://github.com/OpenVPN/openvpn-as-ibm-terraform-deploy) repository, 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

### 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-10T20:21:26.463Z",
  "datePublished" : "2026-11-10T20:21:26.000Z",
  "headline" : "Deploy OpenVPN Access Server with Terraform: A Portable Pattern with an AWS Example",
  "image" : [ "https://blog.openvpn.net/hubfs/f954367b-e204-43cc-92e6-5711d68ade8d.png" ],
  "mainEntityOfPage" : {
    "@id" : "https://blog.openvpn.net/vpn-as-code/terraform-access-server",
    "@type" : "WebPage"
  },
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "url" : "https://blog.openvpn.net/hubfs/Dark=True%20Medium.png"
    },
    "name" : "OpenVPN"
  }
}
```