This CloudFormation template (+ optional Terraform module) helps you set up a complete AWS-managed VPN in about 10 minutes and operate it for as little as $1.45 per work day.
How the template minimizes costs:
-
Split-tunneling. Only AWS private network (VPC) traffic uses the VPN.
-
Reduced redundancy. Access all availability zones in the region through one.
-
Optional night and weekend shutdowns with github.com/sqlxpert/lights-off-aws .
Savings...
Price Hours Hours Cost ↓ Usage / Period → 1 hour 7 days 365 days 365 days Always on: 1 VPC subnet associated 10.0¢ 168 8,760 $876 1 VPN user connected 5.0¢ 40 2,080 $104 1 public IPv4 address 0.5¢ 40 2,080 $10 Total $990 Work hours only: 1 VPC subnet associated 10.0¢ 50 2,607 $261 1 VPN user connected 5.0¢ 40 2,080 $104 1 public IPv4 address 0.5¢ 40 2,080 $10 Total $375 $990 − $375 = $615 ≈ $600 saved per year.
$375 ÷ (52 weeks × 5 work days) = $375 ÷ 260 work days ≈ $1.45 spent per work day.
For each additional VPN user who connects full-time, add approximately $115 per year or $115 ÷ 260 work days = 45¢ per work day.
AWS Client VPN prices in the
us-east-1region were checked in May, 2026 but can change at any time. Public IPv4 address charges also apply as of February, 2024. If a VPC is shared, some charges are billed to the AWS account that owns the VPC. NAT gateway, data transfer, CloudWatch, and other charges may also apply.
Jump to: Installation • Scheduling • Terraform
Rationale for connecting to AWS with a VPN
"Zero-trust" proponents correctly discourage relying on the strength of the perimeter around your private network, but sometimes, perimeter security is the available defense, and a virtual private network connection is necessary. For example, to access an AWS Elastic File System (EFS) volume from your local computer, you must use a VPN, so that the Network File System (NFS) client connection originates inside your AWS Virtual Private Cloud (VPC). NFS server software was not designed for exposure to the public Internet.
Transit Gateway alternative
Since late April, 2026, it's been possible to associate an AWS Client VPN with a Transit Gateway for easy access to multiple private networks. That's an exciting announcement, but my 10-minute VPN continues to support direct association with a VPC. Not only is one VPN, one VPC the right level of complexity for most people, but a Transit Gateway association and its routes would be long-lived properties, making nightly VPN shutdowns impractical.
🔒 Software supply chain security is on everyone's mind. This solution contains no executable code. I made GitHub releases immutable as of
v4.1.2. For security awareness, I provide links to release notes for the software you'll use to generate certificates and connect.The VPN lets clients with the certificate you specify access the private network you specify. The included security group pair demonstrates a critical AWS network security practice: allowing traffic from a specific, named security group rather than from arbitrary private IP addresses. You can supply custom security groups. For additional security options, you can create the VPN endpoint separately and integrate it with this solution.
Certificate creation is faster than it looks. To avoid errors, read each step completely before doing it. You will have to switch between this ReadMe file and AWS's documentation.
AWS CloudShell works well for setup, but move your certificate authority (and your Terraform state file, if applicable), due to the 120-day retention limit.
-
Create the VPN certificate(s) by following AWS's mutual authentication steps.
-
⚠ Check release notes for the version of github.com/OpenVPN/easy-rsa that you will use. As of 2026-05-12, the latest release was
v3.2.6(2026-03-13) and OpenVPN had not enabled immutable releases; a careful release integrity check is necessary. Also check industry security bulletins. -
Copy the individual Linux/macOS commands and execute them verbatim.
-
Copy and edit the block of commands before executing them together. If only AWS's technical writers had chosen a plausible folder name instead of a placeholder! Keep
custom_folderfor now, but after themkdirline, insert:chmod go= ~/custom_folder -
After uploading the first (server) certificate, copy the ARN returned by AWS Certificate Manager.
-
Uploading the second (client) certificate is completely optional.
-
-
⚠ Tag the VPN certificate(s) if you are using Terraform. If you are not using a separate client certificate, apply both tags to the server certificate.
aws acm add-tags-to-certificate --tags 'Key=CVpnServer,Value=' --certificate-arn 'SERVER_CERT_ARN' aws acm add-tags-to-certificate --tags 'Key=CVpnClientRootChain,Value=' --certificate-arn 'CLIENT_CERT_ARN'
-
Install the Client VPN CloudFormation stack using CloudFormation or Terraform.
-
CloudFormation
Easy ✓Create a CloudFormation stack.
Select "Upload a template file", then select "Choose file" and navigate to a locally-saved copy of cloudformation/10-minute-aws-client-vpn.yaml [right-click to save as...].
-
Name the stack
CVpn. -
The parameters are thoroughly documented. Set all "Required" ones. For reference, find your VPC in the list of VPCs.
-
Under "Additional settings" → "Stack policy - optional", you can "Upload a file" and select a locally-saved copy of cloudformation/10-minute-aws-client-vpn-policy.json [right-click to save as...]. The stack policy prevents replacement or deletion of certain resources during stack updates, producing an error if you attempt parameter updates that are not supported.
-
-
Terraform
Check that you have at least:
Add the following child module to your existing root module:
module "cvpn" { source = "git::https://github.com/sqlxpert/10-minute-aws-client-vpn.git//terraform?ref=v5.0.0" # Reference a specific version from github.com/sqlxpert/10-minute-aws-client-vpn/releases # Check that the release is immutable! cvpn_params = { TargetSubnetId = "subnet-10123456789abcdef" } }
Just specify the ID of a subnet in the desired VPC.
Have Terraform download the module's source code. Review the plan before typing
yesto allow Terraform to proceed with applying the changes.terraform init terraform apply
⚠ Turn on the VPN by changing the
Enableparameter of theCVpnCloudFormation stack totrue. The Terraform module leaves the VPN off at first and then ignores changes tocvpn_params["Enable"]so that CloudFormation can manage it, potentially unattended and with limited permissions.
-
-
Follow Step 8 of AWS's Getting Started document.
-
Find your VPN in the list of Client VPN endpoints in the AWS Console and download the configuration file from there.
-
cdto the directory where you downloaded the file and:chmod go= downloaded-client-config.ovpn
-
Open the file in your preferred editor, copy the skeleton from AWS's instructions and paste it at the end of the file, then replace the text between the tags with the contents of the
~/custom_folder/client1.domain.tld.crtcertificate and~/custom_folder/client1.domain.tld.keykey files. -
Rename
~/custom_folderand note that you must also continue to protecteasy-rsa/easyrsa3/pkianddownloaded-client-config.ovpn. All three contain copies of your key.
-
-
Download either the OpenVPN client (Resources → Download OpenVPN) or the AWS client.
⚠ Check OpenVPN Connect release notes or AWS client Linux, macOS, or Windows release notes, plus relevant industry security bulletins.
-
Import your edited configuration file to the client.
-
Use the client to connect to the VPN.
-
Add
FromClientSampleSecGrpto an EC2 instance.If you do not use SSH, create and add a security group that accepts traffic from VPN clients on the port of your choice.
-
Test. On your local computer, run:
ssh -i PRIVATE_KEY_FILE ec2-user@IP_ADDRESS
where PRIVATE_KEY_FILE is the path to the private key for the instance's SSH key pair, and IP_ADDRESS is the instance's private IPv4 address.
Different operating system images have different default usernames;
ec2-useris not always correct!If you do not use SSH, run a different command to test VPN connectivity.
-
Remove
FromClientSampleSecGrp(or equivalent) from you EC2 instance.
Turning the VPN off at night and on weekends and back on at the start of each work day saves $600 per year. See the table in Item 3 of the goals. For a VPN needed strictly on-demand, you could schedule a daily end-of-day shutdown or a weekly end-of-week shutdown but no automatic startup.
To turn the VPN on and off on a schedule...
-
If you used Terraform above, skip to Automatic Scheduling Step 2.
If you used CloudFormation...
-
Create a stack from a locally-saved copy of cloudformation/10-minute-aws-client-vpn-prereq.yaml [right-click to save as...].
-
Name this stack
CVpnPrereq. -
Under "Additional settings" → "Stack policy - optional", you can "Upload a file" and select a locally-saved copy of cloudformation/10-minute-aws-client-vpn-prereq-policy.json [right-click to save as...]. The stack policy prevents inadvertent replacement or deletion of the deployment roles during stack updates, but it cannot prevent deletion of the entire
CVpnPrereqstack. -
Update your
CVpnCloudFormation stack, changing nothing until you reach the "Configure stack options" page, on which you will set "IAM role - optional" toCVpnPrereq-DeploymentRole. You are delegating privileges with a CloudFormation service role.If your own privileges are limited, you might need explicit permission to pass the role to CloudFormation. See the
CVpnPrereq-SampleDeploymentRolePassRolePolsample IAM policy.
-
-
Update your
CVpnCloudFormation stack, adding the following stack-level tags:sched-set-Enable-true:u=1 u=2 u=3 u=4 u=5 H:M=11:00sched-set-Enable-false:u=2 u=3 u=4 u=5 u=6 H:M=01:00
In Terraform, set the following variable inside your
moduleblock:cvpn_schedule_tags = { sched-set-Enable-true = "u=1 u=2 u=3 u=4 u=5 H:M=11:00" sched-set-Enable-false = "u=2 u=3 u=4 u=5 u=6 H:M=01:00" }
Adjust the weekdays and the times based on your work schedule. The example is for the mainland portions of the United States and Canada.
u=1is Monday andu=5is Friday, per ISO 8601.- Times are in Universal Coordinated Time (UTC), which matches the local time in London, England during the winter. The timeanddate.com converter is helpful.
- UTC has no provision for Daylight Saving Time/Summer Time. Leave a buffer at the end of your work day to avoid having to switch schedules.
-
Find your VPN in the list of Client VPN endpoints in the AWS Console and check that its "Target network associations" are being created and deleted as scheduled. Check actual costs after a few days, and set up alerts with AWS Budgets. Keep in mind that if a VPC is shared, some charges are billed to the AWS account that owns the VPC.
You can toggle the Enable parameter (always in
CloudFormation,
never from Terraform) to turn the VPN on and off. This has no effect if
VpnEndpointAndOrVpcSubnetAssociation is VpnEndpointOnly .
You can switch from generic to custom VPN client security groups, change (but
not empty) the list of custom security group IDs, and change the connection log
retention period. These settings have no effect if
VpnEndpointAndOrVpcSubnetAssociation is VpcSubnetAssociationOnly .
Do not try to change the VPC, the IP address ranges, the name paths, or any
other parameters after the CVpn stack has been created. Instead, create a
CVpn2 stack (in Terraform, create a new module instance with
cvpn_stack_name_suffix = "2" ), then update the remote line of
your client configuration file and re-import the configuration file to your VPN
client utility.
You can use the Terraform wrapper module and/or the CloudFormation template to create a complete AWS Client VPN from scratch, but it's also possible to create separate Terraform module instances and/or CloudFormation stacks for the VPN endpoint and each VPC subnet association. Separation increases flexibility and security.
Separation details...
Set VpnEndpointAndOrVpcSubnetAssociation to VpnEndpointOnly for
the first CloudFormation stack or Terraform module instance. The CloudFormation
stack is named CVpn . This stack should not have sched-set-Enable-true
and sched-set-Enable-false tags.
Or, create the VPN endpoint using any CloudFormation template, Terraform module or other system that you like! Creating your own VPN endpoint gives you the freedom to customize IPv6 support, authentication, network authorization rules, the banner message, and other properties.
AWS's quick start, which was introduced in January, 2026, is quite helpful for configuring Client VPN, though it can't guide you through certificate creation.
Set VpnEndpointAndOrVpcSubnetAssociation to VpcSubnetAssociationOnly for
each additional CloudFormation stack or Terraform module instance. These stacks
are named CVpn plus distinguishing suffixes. These stacks may have
sched-set-Enable-true and sched-set-Enable-false tags. Unless you set
ExistingEndpointId directly, each VPC subnet association stack automatically
references the AWS Systems Manager (SSM) Parameter Store parameter created by
the VPN endpoint stack. Change ExistingEndpointStackName if that stack's name
is other than CVpn . In Terraform, make sure that all module instances
share the same cvpn_stack_name_suffix value. There is no stack policy, and
no need for one.
If you configure
automatic scheduling,
a very-low-privilege CloudFormation service role is provided for
VpcSubnetAssociationOnly stacks. CloudFormation can use this role only to
create and delete associations between a VPN endpoint and VPC subnets. The role
cannot be used to create, tag, modify, or delete the VPN endpoint, security
groups, or any other resource types. In CloudFormation, set
"IAM role - optional" to CVpnPrereq-OperationRole instead of
CVpnPrereq-DeploymentRole if VpnEndpointAndOrVpcSubnetAssociation is
VpcSubnetAssociationOnly . The Terraform module selects the appropriate
role automatically.
Keep in mind that one VPC subnet association grants access to network resources in all of the VPC's availability zones. Additional subnet associations, each of which must cover a different availability zone, provide network redundancy at an extra cost. See the table in Item 3 of the goals.
See Separate Terraform Module Instances, below.
| Output | Original Resource and Attribute |
|---|---|
| Matching Data Source and Argument | |
module.cvpn.cvpn_endpoint_id |
aws_ec2_client_vpn_endpoint.id |
data.aws_ec2_client_vpn_endpoint.client_vpn_endpoint_id |
|
module.cvpn.cvpn_client_sec_grp_id |
aws_security_group.id |
data.aws_security_group.id |
Neither output is available if
cvpn_params["VpnEndpointAndOrVpcSubnetAssociation"] is
VpcSubnetAssociationOnly .
The VPN client security group is not available if
cvpn_params["CustomClientSecGrpIds"] is set.
To accept traffic from VPN clients, reference
module.cvpn.cvpn_client_sec_grp_id in:
aws_vpc_security_group..ingress.security_groupsaws_vpc_security_group_ingress_rule.referenced_security_group_id
Separate VPN endpoint and VPC subnet association module instances...
As explained above in Separating the VPN Endpoint from the VPC Subnet Associations, creating separate module instances for the VPN endpoint and each VPC subnet association increases flexibility and security.
locals {
cvpn_stack_name_suffix = "" # Set to "2" for a blue/green VPN deployment
}
module "cvpn" {
source = "git::https://github.com/sqlxpert/10-minute-aws-client-vpn.git//terraform?ref=v5.0.0"
# Reference a specific version from github.com/sqlxpert/10-minute-aws-client-vpn/releases
# Check that the release is immutable!
cvpn_stack_name_suffix = local.cvpn_stack_name_suffix
cvpn_params = {
VpnEndpointAndOrVpcSubnetAssociation = "VpnEndpointOnly"
VpcId = "vpc-00123456789abcdef"
# Specify VpcId for VpnEndpointOnly, TargetSubnetId otherwise!
}
}
module "cvpn_subnets" {
source = "git::https://github.com/sqlxpert/10-minute-aws-client-vpn.git//terraform?ref=v5.0.0"
# Reference a specific version from github.com/sqlxpert/10-minute-aws-client-vpn/releases
# Check that the release is immutable!
depends_on = [module.cvpn]
for_each = {
# Key: availability zone ID (same physical zone, across all AWS accounts)
usw2-az1 = {
TargetSubnetId = "subnet-10123456789abcdef"
}
usw2-az2 = {
TargetSubnetId = "subnet-20123456789abcdef"
# Optional:
sched-set-Enable-true = "u=1 u=2 u=3 u=4 u=5 H:M=11:00"
sched-set-Enable-false = "u=2 u=3 u=4 u=5 u=6 H:M=01:00"
}
}
cvpn_stack_name_suffix = local.cvpn_stack_name_suffix
cvpn_params = {
VpnEndpointAndOrVpcSubnetAssociation = "VpcSubnetAssociationOnly"
TargetSubnetId = each.value["TargetSubnetId"]
}
cvpn_schedule_tags = {
sched-set-Enable-true = lookup(each.value, "sched-set-Enable-true", null)
sched-set-Enable-false = lookup(each.value, "sched-set-Enable-false", null)
} # Optional, and schedules need not be the same for all subnet associations
}To automate certificate creation, consider third-party modules such as:
If you run Terraform with least-privilege permissions...
If you do not give Terraform full AWS administrative permissions, you must give it permission to:
-
List, describe, get tags for, create, tag, update, untag and delete IAM roles, update the "assume role" (role trust or "resource-based") policy, and put and delete in-line policies
-
List, describe, create, tag, update, untag, and delete CloudFormation stacks
-
Set and get CloudFormation stack policies
-
Pass
CVpnPrereq-DeploymentRole-*andCVpnPrereq-OperationRole-*to CloudFormation -
List, describe, and get tags for, all
datasources. For a list, run:grep 'data "' terraform*/*.tf | cut --delimiter=' ' --fields='1,2'
Open the AWS Service Authorization Reference, go through the list of services on the left, and consult the "Actions" table for each of:
AWS Identity and Access Management (IAM)CloudFormationAWS Security Token ServiceAmazon EC2AWS Certificate ManagerAWS Systems ManagerAWS Key Management Service(if you encrypt the CloudWatch log group with a KMS key)
In most cases, you can scope Terraform's permissions to one workload by regulating resource naming and tagging, and then by using:
- ARN patterns in
Resourcelists - ARN patterns in
Conditionentries - Request tag and then resource tag
Conditionentries
Check Service and Resource Control Policies (SCPs and RCPs), as well as resource policies (such as KMS key policies).
The deployment roles defined in the CVpnPrereq stack give CloudFormation the
permissions it needs to create the CVpn or CVpnSubnet stack. Terraform
itself does not need a deployment role's permissions.
Getting the best of the two leading IaC systems for AWS...
As an individual open-source developer I get mere seconds of a potential new user's attention. To reach everybody, I initially distributed my open-source AWS tools as CloudFormation templates. All AWS users have immediate access, with no infrastructure-as-code software to install, no Terraform state files to store, and no extra permissions (see below) and credentials to worry about.
Because most of my software is multi-region and multi-account, writing
templates that also work as
CloudFormation StackSets
makes deployment from a central region and AWS account to many regions and AWS
accounts easy for my users. Terraform gained practical
multi-region support
only in v6 of the Terraform AWS provider, released mid-2025. It will be years
before clumsy provider aliases disappear and existing code and third-party
modules are updated to propagate the region attribute so that Terraform users
can deploy a module with for_each over a straightforward set of regions.
As of mid-2026, Terraform still lacks a standard mechanism for deploying to multiple AWS accounts. People have been talking about this since at least 2019. I could not well expect my users to install Terraform plus third-party Terraform tooling, and to adopt a complex file layout, just to install my lightweight software in multiple AWS accounts! Instead, I accomplished it with CloudFormation StackSets right after the feature was released, in 2017!
Last but not least, CloudFormation service roles allow for delegation of fine-grained permissions. A property of each CloudFormation stack, the service role is recognized by CloudFormation. In this VPN solution, a CloudFormation service role makes it possible for cost-bearing VPC subnet associations to be deleted and recreated unattended, with no infrastructure-as-code software installed.
I added Terraform support to all of my open-source software tools from 2025 to 2026, to increase adoption. I usually wrap either a CloudFormation stack or StackSet in HashiCorp Configuration Language. Where the basis is a third language, such as the IAM policy language, I can provide native Terraform alongside CloudFormation without too much duplication of effort.
Where Terraform shines is dynamic resource lookups. My CloudFormation wrapper modules aren't passive; the Terraform code isn't an afterthought. I add data sources to reduce the number of required inputs and to validate the inputs, in ways that are not possible out-of-the-box with CloudFormation. (Defining custom CloudFormation resources should be a last resort. It's a gravy train for unscrupulous consultants. I avoid even CloudFormation language transforms, which are AWS's cheap way of grafting-on features that belong in the core language. Because they work like pre-processors, CloudFormation language transforms would wreck the unattended, low-privilege, low-code, "use existing template" CloudFormation stack updates that turn the VPN off and on.)
The Terraform module for the VPN finds certificates by tag and requires only a subnet ID, whereas the underlying CloudFormation stack requires many inputs, and some of the inputs contain overlapping details. If we put necessary CloudFormation parameters into a database, it would be thoroughly de-normalized!
My eventual goal is to call a third-party open-source Terraform module to generate self-signed VPN certificates with the correct options -- and perhaps to supply the complete VPN client configuration file. (The VPN self-service portal isn't available, because I use mutual TLS authentication.) Generating VPN certificates would never be possible in CloudFormation, without recourse to an AWS Lambda function.
But Terraform is a mismatch for labor-saving AWS capabilities like gradual updates in Lambda and Elastic Container Service (ECS), and RDS/Aurora blue/green database upgrades. Supporting these kinds of native AWS features "would require multiple iterations of editing the Terraform configuration, applying the configuration, or importing resources. A separate tool [emphasis added] that can manage the orchestration steps would be a better fit" (AWS Aurora Blue/Green Update, November 24, 2023). Why make other people solve the same problem over and over again?
No single tool does everything well. AWS infrastructure-as-code enthusiasts should learn both CloudFormation and Terraform/OpenTofu. Take advantage of the best of what each of these leading IaC systems has to offer!
To help improve the 10-minute AWS Client VPN template, please report bugs.
| Scope | Link | Included Copy |
|---|---|---|
| Source code files, and source code embedded in documentation files | GNU General Public License (GPL) 3.0 | LICENSE-CODE.md |
| Documentation files (including this readme file) | GNU Free Documentation License (FDL) 1.3 | LICENSE-DOC.md |
Copyright Paul Marcelin
Contact: marcelin at cmu.edu (replace "at" with @)