After I published my blog on How to Set Up an AWS Client VPN, someone asked if I could share a CloudFormation template to automate the setup. In response, I’ve put together this follow-up post to walk you through deploying an AWS Client VPN endpoint using CloudFormation and AWS CLI. You’ll learn how to associate the VPN with your VPC, configure access rules, and handle the critical manual step of updating security group rules. I’ve also included a handy shell script to simplify and automate the entire deployment process.
Table of Contents
- What is AWS Client VPN?
- Prerequisites
- Step-by-Step Deployment with CloudFormation
- Deploying with AWS CLI Alternate Method
- Deployment Script
- Cleanup
- Conclusion
- Referrals
What is AWS Client VPN?
AWS Client VPN is a managed client-based VPN service that enables you to securely access your AWS resources and on-premises networks from any location. It provides a highly available and scalable solution for remote access, allowing your users to connect to your private networks using a VPN client.
Prerequisites
Before you begin, ensure you have the following:
- An AWS account.
- AWS CLI installed and configured with appropriate permissions.
- OpenSSL installed for certificate generation (if not using existing certificates).
- A VPC where you want to deploy the Client VPN endpoint. The provided CloudFormation template assumes a new VPC will be created.
Step-by-Step Deployment with CloudFormation
AWS CloudFormation allows you to define your AWS infrastructure as code, making it easy to deploy and manage resources.
1. Generate Client and Server Certificates
AWS Client VPN requires mutual authentication, which means both the server and client need certificates. You can generate these using OpenSSL. The provided generate_certs.sh
script in the attached zip file can help you with this, as detailed in the original blog post: How to Set Up an AWS Client VPN. Make sure to upload these certificates to AWS Certificate Manager (ACM).
# Example command to generate certificates (from generate_certs.sh)
# This is a simplified example, refer to the actual script for full details.
# You will need to import these into ACM.
# Generate server certificate and key
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -signkey server.key -out server.crt
# Generate client certificate and key
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr
openssl x509 -req -in client.csr -signkey client.key -out client.crt
# Import server certificate to ACM
aws acm import-certificate --certificate fileb://server.crt --private-key fileb://server.key --certificate-chain fileb://ca.crt
# Import client certificate to ACM
aws acm import-certificate --certificate fileb://client.crt --private-key fileb://client.key --certificate-chain fileb://ca.crt
Important: Note down the ARNs of the imported server and client certificates. You will need them for the CloudFormation deployment.
2. Deploy VPC and EC2 Instance (Optional)
If you don't have an existing VPC and an EC2 instance in a private subnet, you can use the vpc-ec2.yaml
CloudFormation template (provided in the attached zip file) to set up a basic environment. This template creates a VPC, a private subnet, a route table, a security group, and an EC2 instance within that private subnet.
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC, subnet, security group, and EC2 instance for AWS Client VPN demo.
Parameters:
KeyName:
Type: String
Description: Name of an existing EC2 KeyPair to enable SSH access
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Description: SSM parameter for the latest Amazon Linux 2 AMI
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: MyClientVPNVPC
MyPrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MyVPC
CidrBlock: 192.168.1.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: MyPrivateSubnet
MyRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: MyPrivateRouteTable
MySubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref MyPrivateSubnet
RouteTableId: !Ref MyRouteTable
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow SSH from Client VPN
VpcId: !Ref MyVPC
SecurityGroupIngress: []
Tags:
- Key: Name
Value: MyPrivateSG
MyInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [!Ref MyInstanceRole]
MyInstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Path: '/'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: t3.micro
KeyName: !Ref KeyName
SubnetId: !Ref MyPrivateSubnet
SecurityGroupIds: [!Ref MySecurityGroup]
IamInstanceProfile: !Ref MyInstanceProfile
ImageId: !Ref LatestAmiId
Tags:
- Key: Name
Value: MyPrivateEC2
Outputs:
VpcId:
Description: VPC ID
Value: !Ref MyVPC
SubnetId:
Description: Private Subnet ID
Value: !Ref MyPrivateSubnet
SecurityGroupId:
Description: Security Group ID
Value: !Ref MySecurityGroup
InstanceId:
Description: EC2 Instance ID
Value: !Ref MyEC2Instance
To deploy this stack, you can use the AWS CLI:
aws cloudformation deploy \
--stack-name MyVPCAndEC2Stack \
--template-file vpc-ec2.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
KeyName=YOUR_KEY_PAIR_NAME \
LatestAmiId=/aws/service/ami-amazon-linux-2/latest/amzn2-ami-hvm-x86_64-gp2
Note: Replace YOUR_KEY_PAIR_NAME
with your actual key pair name. If you deploy this stack, you will need to retrieve the VpcId
, SubnetId
, and SecurityGroupId
from its outputs to use in the next step.
3. Deploy the Client VPN CloudFormation Stack
The aws-client-vpn.yaml
CloudFormation template (provided in the attached zip file) defines the necessary AWS resources for the Client VPN endpoint. This template assumes you have an existing VPC and subnet, or have deployed them using the previous step. It sets up the Client VPN endpoint itself, and includes a security group for the EC2 instance.
AWSTemplateFormatVersion: '2010-09-09'
Description: >
AWS Client VPN and EC2 setup.
- VPC: 192.168.0.0/16
- Private Subnet: 192.168.1.0/24
- EC2 Instance in private subnet
- Security group for EC2
- Client VPN endpoint (client CIDR: 10.0.0.0/22, does not overlap with VPC)
Parameters:
KeyName:
Type: String
Description: Name of an existing EC2 KeyPair to enable SSH access
ServerCertificateArn:
Type: String
Description: ARN of the ACM server certificate for the Client VPN endpoint
ClientCertificateArn:
Type: String
Description: ARN of the ACM client certificate for mutual authentication
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Description: SSM parameter for the latest Amazon Linux 2 AMI
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: MyClientVPNVPC
MyPrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MyVPC
CidrBlock: 192.168.1.0/24
MapPublicIpOnLaunch: false
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: MyPrivateSubnet
MyRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: MyPrivateRouteTable
MySubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref MyPrivateSubnet
RouteTableId: !Ref MyRouteTable
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow SSH from Client VPN
VpcId: !Ref MyVPC
SecurityGroupIngress: []
Tags:
- Key: Name
Value: MyPrivateSG
MyInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [!Ref MyInstanceRole]
MyInstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Path: '/'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: t3.micro
KeyName: !Ref KeyName
SubnetId: !Ref MyPrivateSubnet
SecurityGroupIds: [!Ref MySecurityGroup]
IamInstanceProfile: !Ref MyInstanceProfile
ImageId: !Ref LatestAmiId
Tags:
- Key: Name
Value: MyPrivateEC2
MyClientVPNEndpoint:
Type: AWS::EC2::ClientVpnEndpoint
Properties:
AuthenticationOptions:
- Type: certificate-authentication
MutualAuthentication:
ClientRootCertificateChainArn: !Ref ClientCertificateArn
ConnectionLogOptions:
Enabled: false
ServerCertificateArn: !Ref ServerCertificateArn
ClientCidrBlock: 10.0.0.0/22
DnsServers:
- 8.8.8.8
- 8.8.4.4
SplitTunnel: true
TagSpecifications:
- ResourceType: client-vpn-endpoint
Tags:
- Key: Name
Value: MyClientVPNEndpoint
MyClientVPNAuthorizationRule:
Type: AWS::EC2::ClientVpnAuthorizationRule
Properties:
ClientVpnEndpointId: !Ref MyClientVPNEndpoint
TargetNetworkCidr: 192.168.1.0/24 # Your VPC subnet
AuthorizeAllGroups: true
Description: Allow access to VPC subnet
Outputs:
VpcId:
Description: VPC ID
Value: !Ref MyVPC
SubnetId:
Description: Private Subnet ID
Value: !Ref MyPrivateSubnet
SecurityGroupId:
Description: Security Group ID
Value: !Ref MySecurityGroup
InstanceId:
Description: EC2 Instance ID
Value: !Ref MyEC2Instance
ClientVpnEndpointId:
Description: Client VPN Endpoint ID
Value: !Ref MyClientVPNEndpoint
To deploy this stack, you can use the AWS CLI:
aws cloudformation deploy \
--stack-name MyClientVPNStack \
--template-file aws-client-vpn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
KeyName=YOUR_KEY_PAIR_NAME \
ServerCertificateArn=YOUR_SERVER_CERT_ARN \
ClientCertificateArn=YOUR_CLIENT_CERT_ARN \
LatestAmiId=/aws/service/ami-amazon-linux-2/latest/amzn2-ami-hvm-x86_64-gp2
Note: Replace YOUR_KEY_PAIR_NAME
, YOUR_SERVER_CERT_ARN
, and YOUR_CLIENT_CERT_ARN
with your actual values.
4. Manual Step: Update Security Group Rule
After the CloudFormation stack is deployed, you need to manually update the security group associated with your EC2 instance to allow ingress from the Client VPN. This step is crucial because the Client VPN endpoint assigns IP addresses from its own CIDR block (e.g., 10.0.0.0/22 in our template), and your EC2 instance's security group needs to explicitly allow traffic from this range.
- Navigate to the EC2 console and find the security group created by the CloudFormation stack (e.g.,
MyPrivateSG
). - Go to the Inbound Rules tab and click
Edit inbound rules
. - Add a new rule:
- Type: All TCP (or specific ports like SSH if preferred)
- Source: Custom, and enter the Client VPN CIDR block (e.g.,
10.0.0.0/22
). - Description: Allow traffic from Client VPN.
- Save the rules.
This manual step is necessary because CloudFormation does not inherently know the dynamically assigned Client VPN CIDR block at the time of stack creation, and it's a best practice to explicitly define ingress rules for security groups.
5. Authorize Clients to Access the Network
6. (Optional) Add Route to Enable Traffic from VPN to VPC
To allow VPN clients to route traffic to your VPC, you need to add a route:
aws ec2 create-client-vpn-route \
--client-vpn-endpoint-id <VPN_ENDPOINT_ID> \
--destination-cidr-block 192.168.1.0/24 \
--target-vpc-subnet-id <SUBNET_ID> \
--region ap-south-1
Replace <VPN_ENDPOINT_ID>
and <SUBNET_ID>
with your actual values. This is essential for traffic to flow from VPN clients to private resources in the VPC.
Even after associating the Client VPN endpoint with a subnet, clients cannot access resources until authorization rules are defined. This rule specifies which network resources clients can access.
aws ec2 authorize-client-vpn-ingress \
--client-vpn-endpoint-id <VPN_ENDPOINT_ID> \
--target-network-cidr 192.168.1.0/24 \
--authorize-all-groups \
--description "Allow access to VPC subnet" \
--region ap-south-1
Replace <VPN_ENDPOINT_ID>
with the actual ID of your Client VPN endpoint, which you can get from the CloudFormation stack outputs or the AWS console. The target-network-cidr
should be the CIDR block of the subnet you want your VPN clients to access (e.g., 192.168.1.0/24
for the private subnet in our CloudFormation template).
Deploying with AWS CLI (Alternate Method)
For those who prefer a more granular approach or need to integrate into existing scripts, here are the AWS CLI commands to set up an AWS Client VPN. This section assumes you have already generated and imported your server and client certificates into ACM.
1. Create Client VPN Endpoint
aws ec2 create-client-vpn-endpoint \
--client-cidr-block "10.0.0.0/22" \
--server-certificate-arn "arn:aws:acm:ap-south-1:123456789012:certificate/your-server-cert-id" \
--authentication-options Type=certificate-authentication,MutualAuthentication={ClientRootCertificateChainArn="arn:aws:acm:ap-south-1:123456789012:certificate/your-client-cert-id"} \
--connection-log-options Enabled=false \
--dns-servers "8.8.8.8" "8.8.4.4" \
--split-tunnel \
--tag-specifications 'ResourceType=client-vpn-endpoint,Tags=[{Key=Name,Value=MyClientVPNEndpointCLI}]'
Note: Replace the ARN values with your actual certificate ARNs.
2. Associate Client VPN Endpoint with a Subnet
aws ec2 associate-client-vpn-target-network \
--client-vpn-endpoint-id <VPN_ENDPOINT_ID> \
--subnet-id <SUBNET_ID>
Replace <VPN_ENDPOINT_ID>
and <SUBNET_ID>
with your respective IDs.
3. Authorize Client VPN Ingress
aws ec2 authorize-client-vpn-ingress \
--client-vpn-endpoint-id <VPN_ENDPOINT_ID> \
--target-network-cidr 192.168.1.0/24 \
--authorize-all-groups \
--description "Allow access to VPC subnet" \
--region ap-south-1
4. Revoke Client VPN Ingress (Optional)
aws ec2 revoke-client-vpn-ingress \
--client-vpn-endpoint-id <VPN_ENDPOINT_ID> \
--target-network-cidr 192.168.1.0/24 \
--revoke-all-groups \
--region ap-south-1
Deployment Script
To automate the deployment process, you can use a shell script. This script provides options to deploy an optional VPC and EC2 instance, deploy the Client VPN CloudFormation stack, automatically add the necessary security group rules, and includes a cleanup function to tear down the deployed resources. Make sure you have the aws-client-vpn.yaml
, vpc-ec2.yaml
, and generate_certs.sh
files in the same directory as this script.
#!/bin/bash
# Variables (customize these)
STACK_NAME="MyClientVPNStack"
KEY_PAIR_NAME="YOUR_KEY_PAIR_NAME"
SERVER_CERT_ARN="YOUR_SERVER_CERT_ARN"
CLIENT_CERT_ARN="YOUR_CLIENT_CERT_ARN"
REGION="ap-south-1"
TARGET_NETWORK_CIDR="192.168.1.0/24"
# 1. Deploy CloudFormation Stack
echo "Deploying CloudFormation stack..."
aws cloudformation deploy \
--stack-name ${STACK_NAME} \
--template-file aws-client-vpn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
KeyName=${KEY_PAIR_NAME} \
ServerCertificateArn=${SERVER_CERT_ARN} \
ClientCertificateArn=${CLIENT_CERT_ARN} \
LatestAmiId=/aws/service/ami-amazon-linux-2/latest/amzn2-ami-hvm-x86_64-gp2 \
--region ${REGION}
if [ $? -ne 0 ]; then
echo "CloudFormation deployment failed. Exiting."
exit 1
fi
echo "CloudFormation stack deployed successfully. Waiting for resources..."
aws cloudformation wait stack-create-complete --stack-name ${STACK_NAME} --region ${REGION}
# Ensure Client VPN endpoint is in available state
echo "Waiting for VPN endpoint to become available..."
sleep 15
# Get Client VPN Endpoint ID from stack outputs
VPN_ENDPOINT_ID=$(aws cloudformation describe-stacks \
--stack-name ${STACK_NAME} \
--query "Stacks[0].Outputs[?OutputKey==\'ClientVpnEndpointId\'].OutputValue" \
--output text \
--region ${REGION})
if [ -z "${VPN_ENDPOINT_ID}" ]; then
echo "Could not retrieve Client VPN Endpoint ID. Exiting."
exit 1
fi
echo "Client VPN Endpoint ID: ${VPN_ENDPOINT_ID}"
# 2. Authorize Client VPN Ingress Rule
echo "Authorizing Client VPN ingress rule..."
aws ec2 authorize-client-vpn-ingress \
--client-vpn-endpoint-id ${VPN_ENDPOINT_ID} \
--target-network-cidr ${TARGET_NETWORK_CIDR} \
--authorize-all-groups \
--description "Allow access to VPC subnet" \
--region ${REGION}
if [ $? -ne 0 ]; then
echo "Failed to authorize client VPN ingress. Exiting."
exit 1
fi
echo "Client VPN ingress rule authorized successfully."
# 3. Get Security Group ID from stack outputs
SECURITY_GROUP_ID=$(aws cloudformation describe-stacks \
--stack-name ${STACK_NAME} \
--query "Stacks[0].Outputs[?OutputKey==\'SecurityGroupId\'].OutputValue" \
--output text \
--region ${REGION})
if [ -z "${SECURITY_GROUP_ID}" ]; then
echo "Could not retrieve Security Group ID. Exiting."
exit 1
fi
echo "Security Group ID: ${SECURITY_GROUP_ID}"
# 4. Get Client VPN CIDR Block
CLIENT_VPN_CIDR=$(aws ec2 describe-client-vpn-endpoints \
--client-vpn-endpoint-ids ${VPN_ENDPOINT_ID} \
--query "ClientVpnEndpoints[0].ClientCidrBlock" \
--output text \
--region ${REGION})
if [ -z "${CLIENT_VPN_CIDR}" ]; then
echo "Could not retrieve Client VPN CIDR Block. Exiting."
exit 1
fi
echo "Client VPN CIDR Block: ${CLIENT_VPN_CIDR}"
# 5. Add Ingress Rule to EC2 Security Group
echo "Adding ingress rule to EC2 Security Group..."
aws ec2 authorize-security-group-ingress \
--group-id ${SECURITY_GROUP_ID} \
--protocol tcp \
--port 22 \
--cidr ${CLIENT_VPN_CIDR} \
--description "Allow SSH from Client VPN" \
--region ${REGION}
if [ $? -ne 0 ]; then
echo "Failed to add ingress rule to security group. Exiting."
exit 1
fi
echo "Ingress rule added to security group successfully."
echo "Deployment complete!"
Usage:
- Save the above script as
deploy_client_vpn.sh
. - Make it executable:
chmod +x deploy_client_vpn.sh
. - Customize the variables at the beginning of the script (
STACK_NAME
,KEY_PAIR_NAME
,SERVER_CERT_ARN
,CLIENT_CERT_ARN
,REGION
,TARGET_NETWORK_CIDR
). - Run the script:
./deploy_client_vpn.sh
.
Cleanup
To avoid incurring unnecessary costs, it is important to clean up the AWS resources created by these CloudFormation stacks when they are no longer needed. You can delete the stacks using the AWS CLI.
Deleting the Client VPN Stack
aws cloudformation delete-stack \
--stack-name MyClientVPNStack \
--region ap-south-1
Wait for the stack deletion to complete:
aws cloudformation wait stack-delete-complete \
--stack-name MyClientVPNStack \
--region ap-south-1
Deleting the VPC and EC2 Stack (if deployed)
If you deployed the optional VPC and EC2 stack, you should delete it as well:
aws cloudformation delete-stack \
--stack-name MyVPCAndEC2Stack \
--region ap-south-1
Wait for the stack deletion to complete:
aws cloudformation wait stack-delete-complete \
--stack-name MyVPCAndEC2Stack \
--region ap-south-1
Note: Ensure that any associated resources (like EC2 instances, security groups, etc.) are properly terminated before attempting to delete the VPC, as CloudFormation might not delete resources that have dependencies.
Conclusion
Setting up an AWS Client VPN provides a secure and flexible way to access your AWS resources remotely. Whether you prefer the infrastructure-as-code approach with CloudFormation or the direct control of AWS CLI, both methods allow you to establish a robust VPN solution. Remember the crucial step of updating the security group ingress rules to ensure proper connectivity from your VPN clients to your target resources. The provided deployment script automates this process, streamlining your setup.
Happy VPNing!
Referrals
- Authentication
- Getting started with Client VPN
- VPN Photo by @privecstasy from unsplash
Top comments (0)