Leveraging AWS to have our own VPN without a physical office

Leveraging AWS to have our own VPN without a physical office

The pandemic forced several companies to move out of their physical offices and adopt a remote-first arrangement. Origin was one of them.

There are countless articles about the upsides and downsides of making that decision, but this is not one of them. Here I am going to talk of a specific issue faced by Origin during that transition: not having an office means we don’t have a fixed IP, which prevents us from creating a site-to-site VPN inside AWS. This is how we found a secure way out of it.

Importance of a VPN for Origin business

Origin is a fintech startup. Privacy and security have to be in our DNA – our partners and our customers require it.

We have several private resources running in private VPCs (Virtual Private Cloud) inside AWS. Some of those resources are accessible to our employees, and we needed to make that possible in a secure way by adding more security layers besides our security groups. The next logical step was setting up a VPN, and AWS offered two options: AWS Site-to-site VPN and AWS Client VPN.

AWS Site-to-site VPN

A Site-to-Site VPN connection offers two VPN tunnels between a virtual private gateway or a transit gateway on the AWS side, and a customer gateway (which represents a VPN device) on the remote (on-premises) side.

Site-to-Site VPN is surprisingly easy to use, but the transition to remote work meant we didn’t have an on-premise network to put a VPN device in. Our only option was to set up an...

AWS Client VPN

AWS Client VPN is a managed client-based VPN service that enables you to securely access your AWS resources and resources in your on-premises network. With Client VPN, you can access your resources from any location using an OpenVPN-based VPN client.

AWS Client VPN is a great solution: it is based on OpenVPN, it is easy to configure, and, just as importantly, it managed by AWS. However, as always, there are downsides when using this solution:

  1. We can only add subnets from a single VPC on each Client VPN we create;
  2. It is quite expensive - we pay not only for the number of subnets registered but also hourly for each active connection;
  3. The Client VPN doesn't offer us a way to automatically close idle connections, making 2 a particularly risky proposition.

We considered running our own OpenVPN server using AWS EC2 (Elastic Container Service), but, as a startup, our engineering time is much better used elsewhere. So we accepted those weak points and decided to give it a try.

Getting our hands dirty (Setting up the VPN)

I strongly encourage you to read this article before moving on here so you can have a bit more context on how AWS Client VPN works. It also has a beautifully written section on how you can set it up manually using the AWS Client. Besides that, it is extremely important that you read and follow the pre-requisites section in order to make the most of our VPN setup.

Pre-requisites

We are going to use [mutual authentication](https://learn.akamai.com/en-us/webhelp/iot/internet-of-things-over-the-air-user-guide/GUID-21EC6B74-28C8-4CE1-980E-D5EE57AD9653.html#:~:text=Mutual authentication%2C also known as,certificates to prove their identities.), meaning that both our server and every client that uses our VPN will need a certificate issued by a Certificate Authority (CA).

In order to create those, you can follow the steps explained here. For now, you don't have to create the certificates, only the server and the CA (EasyRsa can help you with that).

After creating the server certificate, upload it to Amazon Certificate Manager as described in the doc above**.** We are going to use it to create our Client VPN.

Also, it is important to set up Terraform in your machine. We are going to describe what it is in the section below.

Terraforming the Client VPN

We are strong believers in infrastructure as code here at Origin, so the guide below will focus on how we configured it using Terraform.

As stated in their docs, Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions. Configuration files describe to Terraform the components needed to run a single application or your entire datacenter

In order to create the Client VPN, we need the following resources:

  1. ARN (Amazon Resource Name) of the server certificate you just uploaded to Amazon Certificate Manager.
  2. The CIDR (Classless Inter-Domain Routing) block we are going to use to our VPN (make sure that the CIDR you are using on your VPN doesn't overlap with your VPC CIDRs).
  3. A CloudWatch log group to store the connection logs of the VPN if you want to enable logging (pro tip: you should).

Let's start by creating the Terraform configuration that sets up the Client VPN. We’ll begin without any associations, authorization, or routing config, just the base setup.

The first step is getting Terraform to grab data from the certificate we've uploaded to Amazon Certificate Manager. In order to do that, take note of the "domain" that shows up for you in the console, and add the following to your Terraform config:

data "aws_acm_certificate" "server_vpn_certificate" {
  domain   = "your domain"
  statuses = ["ISSUED"]
}

Now we are able to programatically grab information about this domain – e.g. its ARN. Let's create our CloudWatch Log group. All we need is a name, as you can see below:

resource "aws_cloudwatch_log_group" "vpn_log_group" {
  name = "log-group-name"
}

Given that we have the certificate data and the CloudWatch log group, we can now move on and create the base set up for our Client VPN:

resource "aws_ec2_client_vpn_endpoint" "origin_vpn" {
  description            = "Description of your client VPN"
  server_certificate_arn = data.aws_acm_certificate.server_vpn_certificate.arn
  client_cidr_block      = "your VPN CIDR block"

  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = data.aws_acm_certificate.server_vpn_certificate.arn
  }

  connection_log_options {
    enabled              = true
    cloudwatch_log_group = aws_cloudwatch_log_group.vpn_log_group.name
  }

  dns_servers = ["10.0.0.2"]
}

This is a lot to digest, but let’s take a look at each item below.

  • description: the description of your client VPN. It is useful when you have several clients.
  • server_certificate_arn: the ARN of the certificate you uploaded to Amazon Certificate Manager. It will be used by the VPN to authorize client certificates in the future.
  • client_cidr_block: the CIDR used by the VPN to assign IPs for the clients.
  • authentication_options: here we define the type of authentication we are going to use. As mentioned before, we are going with certificate-authentication, but the Client VPN offers others possibilities like active-directory. The root_certificate_chain_arn can be the same as our server certificate.
  • connection_log_options: it is responsible for enabling logs (or not) and, if so, the name of the log group we are going to use.
  • dns_servers: the DNS server we are going to use to resolve internal DNS from AWS.

You can now run terraform apply and it will create the Client VPN for you. Don’t rush though: It might take around 3-5min to create everything.

Terraforming the Network Association

A target network is a subnet in a VPC. A Client VPN endpoint must have at least one target network to enable clients to connect to it and establish a VPN connection.

When creating the network association, we can specify which security groups have access to each resource. Note that we pay for each subnet associated with the Client VPN, so be careful when associating several subnets here. In our case, we decided to allow access to everything, so the security group is a bit simple, but you can add as many rules as you want to.

In order to create the association, we are going to need the following resources:

  1. A Security Group to manage access to the private resources.
  2. The ID of the VPC you want to associate with the Client VPN. You can find it on your AWS Console within the VPC section.
  3. The ID of the subnet you want to associate with. You can find it on your AWS Console within the subnet section of your VPC.

Let’s start by creating the security group. The ingress and egress rules below are basically telling it to allow access from every port using any protocol.

resource "aws_security_group" "vpn_security_group" {
  name        = "name of your security group"
  description = "description of your security group"
  vpc_id      = "id of the desired VPC"

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "name of your security group"
  }
}

Now that we have the security group, we can define our network association

resource "aws_ec2_client_vpn_network_association" "network_association" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.origin_vpn.id
  subnet_id              = "id of the subnet you want to associate with"
  security_groups        = [aws_security_group.vpn_security_group.id]
}

That's it. Now if we run terraform apply again, it will create the network association for us.

Terraforming the VPN Route

Each Client VPN endpoint has a route table that describes the available destination network routes. Each route in the route table determines where the network traffic is directed. You must configure authorization rules for each Client VPN endpoint route to specify which clients have access to the destination network.

When you associate a subnet from a VPC with a Client VPN endpoint, a route for the VPC is automatically added to the Client VPN endpoint's route table. To enable access for additional networks, such as the internet, you must manually add a route to the Client VPN endpoint's route table. Given that we want to offer internet access to everyone using our VPN, we have to add another route:

resource "aws_ec2_client_vpn_route" "vpn_route" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.origin_vpn.id
  destination_cidr_block = "0.0.0.0/0"
  target_vpc_subnet_id   = aws_ec2_client_vpn_network_association.network_association.subnet_id
}

The code snippet above is basically allowing access to our NAT Gateway (0.0.0.0/0) so everyone using the VPN can access the internet.

Terraforming the Authorization Rules

Authorization rules act as firewall rules that grant access to networks. You should have an authorization rule for each network for which you want to grant access.

Since we are using mutual authentication, we don't have control over user groups and things like that. However, authorization rules allow you to control who can access each CIDR from our VPC, which acts as a firewall. In our case, every rule we add needs to be applied to all clients of our VPN.

Another important point: We wanted to allow access to the internet for people using our VPN, so we had to add another rule allowing access to our NAT Gateway (CIDR 0.0.0.0/0) (in the previous section, we've created a route to our NAT gateway, now we have to allow access to that route).

resource "aws_ec2_client_vpn_authorization_rule" "vpc_authorization_rule" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.origin_vpn.id
  target_network_cidr    = var.vpc_cidr
  authorize_all_groups   = true
}

resource "aws_ec2_client_vpn_authorization_rule" "internet_authorization_rule" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.origin_vpn.id
  target_network_cidr    = "0.0.0.0/0"
  authorize_all_groups   = true
}

The code snippet above is basically creating two authorization rules:

  1. Allowing access to our VPC;
  2. Allowing access to the internet.

That was the last step needed to build the entire setup for the Client VPN. After you run terraform apply again, everything should be working and all you have to do now is generate the client certificates, download the configuration file and send it to the people that will have access to the VPN.

You can download the configuration file directly from the Client VPN section in the AWS console. I suggest you use Tunnelblick to handle the connections to the VPN (if you are a MAC user).

Conclusion

We had a huge problem in the past when trying to test AWS products that were only accessible from inside the VPC – e.g. Redis. By setting up this VPN, Origin was capable of having a better control of who is accessing our network while keeping the engineering team delivering products as fast as possible. To wrap it up, we are now capable of using every feature and service provided by AWS (and explore our infrastructure) without any problems.

Show Comments