How to set up client VPN in AWS using Terraform

By Mateo Spak Jenbergsen
Published on 2023-03-09 (Last modified: 2023-10-03)

...

In this article we will look at how to set up an AWS Client VPN Endpoint to give users access to our VPC. This article assumes you have some knowledge on how to write and deploy Terraform code with AWS. If you don't have Terraform with AWS already up and running, please have a look at Terraform with AWS

 

Use case

There are many reasons to use a VPN. Let's says your running EC2 instances on AWS and would like to only allow inbound traffic from certain employees in your company. Many companies solves this problem by whitelisting the employee's IP address. This does not work when the employee's IP change. IP whitelisting also prevents the ability to place your EC2 instance in a private subnet. In our use case, we have multiple employee's that we want to give access to our EC2 instance. For security reasons, this EC2 instance will also be placed in a private subnet, to prevent any outside traffic from access our instance. Note, this article is not restricted to only EC2 instances but will also apply to any other AWS service.

 

AWS VPN

AWS Client VPN Endpoint, is an AWS service that enables clients to connect to a VPN session. This will allow the client to access services within our VPC, privately. AWS Client VPN Endpoint also allows for split tunneling. Split tunneling will split the clients traffic based on where it is going. This means that any traffic between the client and the VPC will be private through the VPN tunnel, and other traffic will be routed through the open web. 

 

Generate certificates

For this tutorial, we will be using OpenVPN easy-rsa to generate our client and server certificate and keys. 

 

First, we need to clone the easy-rsa Github repository.

git clone https://github.com/OpenVPN/easy-rsa.git

Next, lets move into the easyrsa3 directory.

cd easy-rsa/easyrsa3

Then we initialize a new PKI environment.

./easyrsa init-pki

To build a new certificate authority (CA), run this command.

./easyrsa build-ca nopass

Now we need to generate server and client certificates and keys. Run the following command to generate server certificates and keys.

./easyrsa build-server-full server nopass

...and lastly, the client certificate and key.

./easyrsa build-client-full client1.domain.tld nopass

To make things a little easier for ourselves, lets move all the needed files out in a another directory.

mkdir ~/my-vpn-files/
cp pki/ca.crt ~/my-vpn-files/
cp pki/issued/server.crt ~/my-vpn-files/
cp pki/private/server.key ~/my-vpn-files/
cp pki/issued/client1.domain.tld.crt ~/my-vpn-files
cp pki/private/client1.domain.tld.key ~/my-vpn-files/
cd ~/my-vpn-files/

Now that we have all our needed files, let's look at our Terraform code.

 

Terraform VPN

We'll start by adding the files we generated in the last step to AWS ACM to make them easily accessible later on. Create a file, acm.tf, and insert the following.

resource "aws_acm_certificate" "server_vpn_cert" {
  certificate_body  = var.server_cert
  private_key       = var.server_private_key
  certificate_chain = var.ca_cert
}

resource "aws_acm_certificate" "client_vpn_cert" {
  certificate_body  = var.client_cert
  private_key       = var.client_private_key
  certificate_chain = var.ca_cert
}

In the first resource we add the server files and then the client files in the second resource. You can also choose to put these certificates and keys in a separate variables.tf file. If your unfamiliar with variables, click here to learn more about Terraform variables. You could also just paste the content of each file directly into the code. Here we set the following arguments.

Resource "server_vpn_cert"

  • certificate_body, is the server.crt file we generated in the previous step.
  • private_key, is the server.key file we generated in the previous step.
  • certificate_chain, is the ca.crt file we generated in the previous step.

Resource "client_vpn_cert"

The Client VPN Endpoint needs one pair of client certificates and keys. This step is not necessary when we create more clients later on.

  • certificate_body, is the client1.domain.tld.crt file we generated in the previous step.
  • private_key, is the client1.domain.tld.key file we generated in the previous step.
  • certificate_chain, is the ca.crt file we generated in the previous step.

 

Next, let's create our security group. Create a file, security_groups.tf, and insert the following.

resource "aws_security_group" "vpn_secgroup" {
  name   = "vpn-sg"
  vpc_id = module.vpc.vpc_id
  description = "Allow inbound traffic from port 443, to the VPN"
 
  ingress {
   protocol         = "tcp"
   from_port        = 443
   to_port          = 443
   cidr_blocks      = ["0.0.0.0/0"]
   ipv6_cidr_blocks = ["::/0"]
  }
 
  egress {
   protocol         = "-1"
   from_port        = 0
   to_port          = 0
   cidr_blocks      = ["0.0.0.0/0"]
   ipv6_cidr_blocks = ["::/0"]
  }
}

Since security groups is not directly related to AWS Client VPN Endpoint, I won't go in to detail how they work. All you need to know is that this security group allows inbound traffic on port 443.

 

Now, we can create our client VPN endpoint. Start by creating a file, vpn.tf, and insert the following.

resource "aws_ec2_client_vpn_endpoint" "my_client_vpn" {
  description            = "My client vpn"
  server_certificate_arn = aws_acm_certificate.server_vpn_cert.arn
  client_cidr_block      = "10.100.0.0/22"
  vpc_id                 = module.vpc.vpc_id
  
  security_group_ids     = [aws_security_group.vpn_secgroup.id]
  split_tunnel           = true

  # Client authentication
  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = aws_acm_certificate.client_vpn_cert.arn
  }

  connection_log_options {
    enabled = false
   }

  depends_on = [
    aws_acm_certificate.server_vpn_cert,
    aws_acm_certificate.client_vpn_cert
  ]
}

We set the follow arguments.

  • description, is the description for our Client VPN Endpoint.
  • server_certificate_arn, is the ACM certificate ARN containing the server certificate we generated earlier.
  • client_cidr_block, is the IPv4 CIDR range we want our clients to have. This cannot overlap with the CIDR of the VPC.
  • vpc_id, is the ID of our VPC.
  • security_groups_ids, is a list of security groups we want attached to our VPN Endpoint. Here we set the security group ID we created in the previous step. 
  • split_tunnel, is whether or not we want split tunneling enabled. We set this to true.

# Client authentication #

This contains information about how we want to authenticate our clients accessing our VPN.

  • type, is set to certificate-authentication since we want to authenticate clients using the generated files we created earlier.
  • root_certificate_chain_arn, is the ARN of the client certificate we generated earlier.

# Connection logs option #

  • enabled, is set to false since we don't want to log information about the client connection.

 

Lastly, we need to add some network configurations to our VPN endpoint. Insert the following in vpn.tf.

### Network Association ###
  
resource "aws_ec2_client_vpn_network_association" "client_vpn_association_private" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.my_client_vpn.id
  subnet_id              = tolist(module.vpc.private_subnets)[0]
}

resource "aws_ec2_client_vpn_network_association" "client_vpn_association_public" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.my_client_vpn.id
  subnet_id              = tolist(module.vpc.public_subnets)[1]
}


### Authorization ###

resource "aws_ec2_client_vpn_authorization_rule" "authorization_rule" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.my_client_vpn.id
  
  target_network_cidr    = "10.0.0.0/16"
  authorize_all_groups   = true
}

Here we set the following arguments.

# Network Association # 

  • client_vpn_endpoint_id, is the ID of the client VPN endpoint we created earlier. 
  • subnet_id, is the ID of the subnet we want to associate with the client connection. If you are not using a VPC module, you might need to adjust this to fit whatever setup you have.

# Authorization #

  • client_vpn_endpoint_id, is the ID of the client VPN endpoint we created earlier.
  • target_network_cidr, here we set the CIDR for our VPC.
  • authorize_all_groups, is set to "true" since we want to allow all groups access to our VPN. I'll explain in the next step how we control access to our VPN per-person based.

 

Now, we should be able to run terraform plan and apply. 

 

Next step is to go into AWS Console and view our VPN Endpoint. In the top right corner, press "download client configuration". We need this to connect to our VPN. Open the file and insert the following as such.

<cert>
* insert client1.domain.tld.cert we generated earlier.
</cert>

<key>
* insert client1.domain.tld.key we generated earlier.
</key>

Note: Make sure these are placed above reneg-sec 0 in the client configuration we downloaded.

 

The final step is connecting to our VPN. We need to download AWS VPN client. Other VPN clients should also work, but using AWS own VPN client is recommended. Open AWS VPN Client, press manage profiles -> add profile and press connect. Now, you should be connected to our Client VPN Endpoint. 

Note: To access your EC2 instance, you need to allow inbound traffic from the Client VPN security group we created earlier. Remember to use the private IP address of the EC2 instance, since we are connecting to it from inside our VPC.

 

Add more clients

If we want to add more clients to our Client VPN Endpoint, all we need to do is go back to our OpenVPN easy-rsa repo and run:

./easyrsa build-client-full <new-client-name> nopass

This should create a new client certificate and key which we'll add to a client configuration file like we did in the previous step. We create a new client configuration file for each client we want to give access to our VPN. This way we can control who gets access to our VPC through our Client VPN Endpoint.

 

Summary

In this article, we have looked at what AWS Client VPN Endpoint is and why we should use it. We've gone through the steps of generating certificates and keys for both our VPN server and our clients. Lastly, we looked at how we provision out a Client VPN Endpoint, using Terraform.




About the author



Mateo Spak Jenbergsen

Mateo is a Devops at Spak Consultants, with strong focus on AWS, Terraform and container technologies. He has a strong passion for pushing the limits when it comes to building software on cloud platforms.

Comments




Dinesh Balendran Superb Tutorial! Could you please tell me where does the PKI environment reside? And where is this CA? Thanks Then we initialize a new PKI environment. ./easyrsa init-pki To build a new certificate authority (CA), run this command. ./easyrsa build-ca nopass
2024-08-20



Devops-91237 Nice Tutorial! Just a note that for my experience you don't need any inbound rule on the security group. I don't know exactly how it works but apparently the ENI used for the connection is not the one in your AWS account.
2024-05-30



Chriswassy Hello, im noob :)
2024-05-03



Will Great article, it works! Thank you.
2023-11-28



mike You're missing a `build-ca` command after the `init-pki` command. I hit an error there
2023-10-02



Mateo Spak (Author) Yes, you are right. I’ve updated the article. Thank you for pointing that out.
2023-10-03



Mauricio What if we add more clients (we create new certs, keys and vpn file) but suddenly, one of them leaves the company and keeps the vpn file?
2023-08-12



Mateo Spak (Author) Hello Mauricio, Thank you for reaching out with your excellent question! To assist you, I'd like to direct you to the AWS documentation that provides detailed information about client VPN access revocation. You can explore the details via the following link: https://repost.aws/knowledge-center/client-vpn-revoke-access-specific-client.
2023-10-03