DEV Community

Cover image for Setting up AWS Client VPN with CloudFormation and AWS CLI
Sri for AWS Community Builders

Posted on

Setting up AWS Client VPN with CloudFormation and AWS CLI

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?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. Navigate to the EC2 console and find the security group created by the CloudFormation stack (e.g., MyPrivateSG).
  2. Go to the Inbound Rules tab and click Edit inbound rules.
  3. 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.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}]'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

Usage:

  1. Save the above script as deploy_client_vpn.sh.
  2. Make it executable: chmod +x deploy_client_vpn.sh.
  3. Customize the variables at the beginning of the script (STACK_NAME, KEY_PAIR_NAME, SERVER_CERT_ARN, CLIENT_CERT_ARN, REGION, TARGET_NETWORK_CIDR).
  4. 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
Enter fullscreen mode Exit fullscreen mode

Wait for the stack deletion to complete:

aws cloudformation wait stack-delete-complete \
    --stack-name MyClientVPNStack \
    --region ap-south-1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Wait for the stack deletion to complete:

aws cloudformation wait stack-delete-complete \
    --stack-name MyVPCAndEC2Stack \
    --region ap-south-1
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)