Increasing concern around the fact that our browsing habits and personal information are being mined by many of the websites and service providers we use on the Internet is driving a higher interest in ways to protect our anonymity and personal information on the Internet.

Virtual Private Networks or VPNs are one of the more comprehensive and robust answers to this problem. A VPN allows you to establish an IP tunnel between your computer and a remote VPN server, providing two key benefits:

  • It encrypts all of your IP datagrams. Since a VPN (at the very least some of them) operates at layers 2 through 4, all packets above those are encrypted by extension. This means even insecure Layer 7 protocols like HTTP are protected from eavesdropping.
  • It conceals your real IP address. The VPN server will forward all of your requests to their intended destination, swapping your requested information for its own, so it will be its IP appearing on the requests made to the server hosting the website you’re visiting, not your own.

These features make VPNs a sound mechanism to protect your data and personal information on the Internet. Many different VPN protocols have emerged over the years: PTTP, IPSec and OpenVPN, the most popular of the three, which we will be using today.

The issue with OpenVPN is that it is not particularly easy to set up. Many entrepreneurial minds realised this, which means there sure is no shortage of VPN as a Service products on the online market at the moment. These people will happily take care of all of the infrastructure required to support an OpenVPN session for you so that all you need to do is install an app on your phone or a VPN client on your computer, grab some connection details from your account and voilà!

There’s nothing wrong with this but personally, when it comes to security, I prefer to use my own infrastructure if possible (within the bounds of reason), hence why I’ve no issue rolling out my own OpenVPN server implementation on my personal Virtual Private Cloud in AWS. The process of setting this up from scratch is what I’ll be showing you today.

I’ve divided this article into two sections: first, I will show you the minimum amount of steps you can take to roll out your own OpenVPN server on AWS. Once we’ve got a configured server, in a follow-up article I will show you how to increase its reliability so you can enjoy having a dedicated, 99.99% uptime VPN server at your disposal.

The intended audience for this article is people wishing to deploy a personal OpenVPN server in the cloud. Knowledge of basic AWS services and concepts like EC2, EBS and Route 53 is assumed.

Getting Started

There are two ways to deploy an OpenVPN server to the Amazon cloud: by configuring and running it directly on an EC2 instance, or running it in a Docker container. I’ve chosen the latter as containers require far less setup and are much easier to scale and recover. To make this task even easier, we’ll be hosting our container in ECS, Amazon’s managed container service.

We will be using this Docker image to run and configure our OpenVPN server. There are a few preparatory steps we need to take before working on the server.

  1. From the EC2 pane in the AWS Console, launch a small EC2 instance that we will use to generate the config files for our VPN server. A t3.nano instance running Ubuntu Server 18.04 will do.
  2. In the Storage screen, add a new volume of a small size (5GB or less), as we will store our OpenVPN files here in a minute. Ensure the “Encrypted” option is set to “true”, as we will be storing sensitive information. The default aws/ebs master key will do.
  3. Select or create a security group allowing inbound traffic on the SSH port from your current IP.
  4. Finally, create a new EC2 keypair for it and download it (make sure to run chmod 400 on it afterwards).
  5. Launch the instance and give it a minute to start up.
  6. Once the instance is running, retrieve its auto-assigned IPv4 public IP address and SSH into it
ssh -i /path/to/keypair.pem ubuntu@<instance-ip>
  1. Run the following commands on the remote instance
sudo apt update && sudo apt install -y docker docker-compose
sudo systemctl start docker
  1. Add your current user to the docker group so we can run Docker commands without having to be root.
sudo usermod -aG docker $(whoami)
newgrp docker
  1. Run the groups command and check docker is part of the list. Then, verify the Docker daemon is running by running docker ps. If you don’t get the error below, everything is working OK.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
  1. Create a new file called docker-compose.yml and copy the container spec in it:
# docker-compose.yml

version: '2'
services:
  openvpn:
    cap_add:
     - NET_ADMIN
    image: kylemanna/openvpn
    container_name: openvpn
    ports:
     - "1194:1194/udp"
    restart: always
    volumes:
     - ./openvpn-data/conf:/etc/openvpn

Save the file. We will need to make the config we’re about to generate available to the OpenVPN instance we’ll be launching in a few moments. The simplest way to do this is by storing these files in a separate EBS volume and then attaching this volume to the instance running OpenVPN. To do this, format and mount the extra volume you created earlier. Run lsblk if you don’t know its name. E.g.

sudo mkfs.ext4 /dev/nvme0n1
sudo mount /dev/nvme0n1 /mnt

Now switch into the volume directory and create the directory that will be bind-mounted onto the containers we’ll be using next.

cd /mnt
mkdir -p ./openvpn-data/conf

And with this, we’re ready to start!

Server Certificates

There’d be no such thing as secure and trusted communication over the Internet without proper identity verification so the first thing we need is to create a PKI certificate for our server to use in its sessions. A Certificate Authority (CA) will need to generate and sign the certificate. Since we’re setting up a personal VPN and we trust ourselves (right?) we will act as the CA and generate and sign our own certificates. To that purpose, the Docker image we’re going to be using offers a couple of useful commands which invoke the EasyRSA CA management application.

docker-compose run --rm openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM

Where VPN.SERVERNAME.COM is the domain name of the VPN server you’ll be running on AWS. This domain does not exist just yet but we will create it, so set this option to your desired VPN domain name later on. For example, I used vpn.mydomain.com.

docker-compose run --rm openvpn ovpn_initpki

You will be prompted for a CA passphrase and a Common Name. These can be anything you want but make sure to secure the CA passphrase as you will need it every time you want to generate a new certificate for your OpenVPN server, including user certificates. You will also be prompted for the passphrase you just created right at the end of this step, when the certificate database and the Certificate Revocation List are updated.

We have created all of the CA PKI files we need to create client certificates, including the certificate for the OpenVPN server itself. Now let’s create some client profiles.

Client Certificates

We can now generate client certificates, that is, the certificates that we will present to the server to authenticate ourselves when starting a VPN connection. Repeat these steps for every distinct OpenVPN user you require. I will be doing it just once as it’ll only be me connecting to the server for the time being.

export CLIENTNAME="your_client_name"
# with a passphrase (recommended)
docker-compose run --rm openvpn easyrsa build-client-full $CLIENTNAME
# without a passphrase (not recommended)
docker-compose run --rm openvpn easyrsa build-client-full $CLIENTNAME nopass

Where your_client_name is the name of the client user that will be opening a secure connection to your OpenVPN server. I recommend you set it up with a secure password (different to the one you used to set up the CA, of course!).

Fix the permissions on the directory, as it will most likely be owned by root, as that’s the user under which the containers ran.

sudo chown -R $(whoami): ./openvpn-data

Preparing the VPN config EBS snapshot

The config files for your OpenVPN server and clients have now been created. I encourage you to download the CA private files to a secure destination and don’t transfer them across to the VPN server, as it does not need them to work and it’s best for them not to be there from a security standpoint. The only files the OpenVPN server needs are

  • openvpn.conf
  • ovpn_env.sh
  • pki
    • private
      • vpn.mydomain.com.key (or whatever your domain name is)
    • issued
      • vpn.mydomain.com.crt (or whatever your domain name is)
    • ca.crt
    • dh.pem
    • ta.key

Everything else you should download and remove from this instance. An alternative to this would be to treat this EC2 instance as your CA instance, keep these files as they are and never delete them. Then, stop the instance and only launch it when you need to create new certs. It’s up to you.

Unmount the volume

cd /
sync && sudo umount /mnt

Head back to the EC2 > EBS Volumes pane in the AWS Console, select the volume you created which now holds all the VPN server config files, and create an EBS snapshot out of it (right-click > Create snapshot). Give it a useful description like “OpenVPN server config files” so it’s easy to find later. Ensure the snapshot will be encrypted. The encryption status of a snapshot always matches that of the volume it was taken from so it should be encrypted. Once the snapshot is created, you can stop (or terminate) the Ubuntu EC2 instance, but don’t delete the EBS volume that stores the config files, as we will be using it later on.

Deploying an OpenVPN server to Amazon ECS

We will be deploying our OpenVPN server as a container on Amazon’s ECS platform, so the first step is navigating to ECS on the AWS Console and creating a new ECS Cluster. Make sure you’re creating an ECS Cluster, not EKS, as that’s an entirely different (and much more complex) container cluster to manage!

Select “EC2 Linux + Networking” in the cluster template selection screen. We will run our cluster off EC2 instead of Fargate because Fargate does not support UDP port mapping, which is the protocol used by OpenVPN.

Now let’s choose our cluster configuration parameters. If you just want to create the task definition and not worry too much about the setup and why it’s like this, simply scroll all the way down to the bottom, click on “Configure via JSON” and replace the entire JSON document with the following:

{
    "ipcMode": null,
    "executionRoleArn": null,
    "containerDefinitions": [
        {
            "dnsSearchDomains": null,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/openvpn",
                    "awslogs-region": "eu-west-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "entryPoint": null,
            "portMappings": [
                {
                    "hostPort": 1194,
                    "protocol": "udp",
                    "containerPort": 1194
                }
            ],
            "command": null,
            "linuxParameters": {
                "capabilities": {
                    "add": [
                        "NET_ADMIN"
                    ],
                    "drop": null
                },
                "sharedMemorySize": null,
                "tmpfs": null,
                "devices": null,
                "initProcessEnabled": null
            },
            "cpu": 0,
            "environment": [],
            "resourceRequirements": null,
            "ulimits": null,
            "dnsServers": null,
            "mountPoints": [
                {
                    "readOnly": false,
                    "containerPath": "/etc/openvpn",
                    "sourceVolume": "openvpn-data"
                }
            ],
            "workingDirectory": null,
            "secrets": null,
            "dockerSecurityOptions": null,
            "memory": 400,
            "memoryReservation": null,
            "volumesFrom": [],
            "image": "kylemanna/openvpn",
            "disableNetworking": null,
            "interactive": null,
            "healthCheck": null,
            "essential": true,
            "links": null,
            "hostname": null,
            "extraHosts": null,
            "pseudoTerminal": null,
            "user": null,
            "readonlyRootFilesystem": null,
            "dockerLabels": null,
            "systemControls": null,
            "privileged": true,
            "name": "openvpn"
        }
    ],
    "memory": null,
    "taskRoleArn": null,
    "family": "openvpn",
    "pidMode": null,
    "requiresCompatibilities": [
        "EC2"
    ],
    "networkMode": "bridge",
    "cpu": null,
    "volumes": [
        {
            "name": "openvpn-data",
            "host": {
                "sourcePath": "/ecs-data/openvpn-data/conf"
            },
            "dockerVolumeConfiguration": null
        }
    ],
    "placementConstraints": []
}

Save and create the task definition. You can skip the next section if you wish, as it’s an explanation of the settings you just pasted from above.

Task Definition Configuration

The name of your task definition can be anything. Most of the default options are OK except for the following which we do want to change:

  • EC2 instance type: if all you’re planning to run on the cluster is a single OpenVPN container for very few concurrent users, pick the smallest instance type, i.e. t3.nano at the time of writing. This will give you a 2 vCPU, 0.5 GB RAM VM with up to 5 Gbps network throughput, which is more than plenty to run an OpenVPN server, whose typical requirements do not exceed 300MB RAM and 1 core; again, provided it doesn’t have dozens of simultaneous connections open. If you have a need for many concurrent connections or need high bandwidth from your VPN (because you’re going to be torrenting or streaming media), then you may want to start looking at compute or memory-optimised instances or larger sizes to increase the network cap too, as well as scaling out your cluster from a single instance to more, but bear in mind the cost difference will be rather high, especially if you run the cluster 24/7.
  • Key pair: create and download a new key pair. Again, chmod 400 it afterwards.
  • VPC: pick your default VPC or create a new one, it’s up to you. Just make sure the cluster instance runs in a public subnet with access to the Internet. Check if there’s a route table attached to the instance subnet with an entry for 0.0.0.0/0 pointing at an Internet or NAT Gateway. If you just pick the defaults it should just work.
  • Security group: create a new security group, ensuring there’s a single entry in the inbound section, allowing traffic on port UDP 1194 from anywhere. In SG terms, select
    • Custom UDP Rule
    • Port 1194
    • Source IP: 0.0.0.0/0. Alternatively, you can specify a different IP range if you want to restrict access to your VPN to only allow a set amount of places.

Once you confirm these options, ECS will start provisioning a fresh EC2 instance for you. In the meantime, let’s create the task definition for our VPN container. If you’re unfamiliar with ECS’ nomenclature, a task definition would be equivalent to a docker-compose Service, which groups up a set of tightly coupled containers under a single logical unit, whose resources and quotas can be managed as a single entity. In our case, our task definition will just contain one container, which is typical with Docker Services anyway.

Select “Create a new Task Definition” in the ECS console and select “EC2”. Name your definition anything you want (openvpn perhaps?). All the defaults will be OK. Scroll to the bottom of the page, to the “Volumes” section and create a new one called “openvpn-data”, giving it a source path of /ecs-data/openvpn-data/conf.

After you’ve created the volume, edit the task definition’s JSON config file, as there’s an option we need to set which is not covered by the wizard. Select “Configure via JSON”. Add this to the linuxParameters key which should be an empty array.

"linuxParameters": {
                "capabilities": {
                    "add": [
                        "NET_ADMIN"
                    ],
                    "drop": null
                },

This will allow the container to make changes to the underlying host’s network during its setup process. Let’s provide the spec for our container now. Navigate to the section above, “Container definitions”, and select “Add container”. Settings below:

  • Container name: “openvpn”
  • Image: kylemanna/openvpn
  • Memory limits: a hard limit of 400MB if you selected a 0.5 GB RAM instance type. Linux and Docker need some memory to run too so you won’t be able to reserve all memory just for the container.
  • Port mappings: set both the host and container port to “1194” and the protocol to “udp”. Fargate runs on an awsvpc network, as opposed to the Docker bridge type used by EC2. awsvpc networks do not allow port mappings on protocols other than TCP so this is the main reason why we are using EC2.

Keep scrolling down until you reach the “STORAGE AND LOGGING” section in the advanced options and set the following parameters:

  • Mount points
    • Source volume: select the “openvpn-data” volume we created before.
    • Container path: /etc/openvpn

In the next section, “SECURITY”, tick the “Privileged” box, as the OpenVPN container needs to change a couple of settings in the underlying host network which can only be altered by a privileged system user. This is not ideal, but if this is the only container running in the ECS instance and there are no other other co-located processes in it either, this shouldn’t be a massive problem.

You can now click on “Create” to create this task definition. Note that ECS Task Definitions are versioned so if you want to make further updates to this definition, you will not be able to modify the definition directly. You will need to create a new revision of the definition and then make all the necessary changes.

When we set up our CA PKI at the beginning, we supplied a domain name for our VPN server, e.g. vpn.mydomain.com. An X509 certificate was created for this domain. Now it’s time to create said domain. Head to Route 53 and add a new record set in a hosted zone pointing at the public IP of the ECS instance. It might take a few minutes for the changes to take effect.

The last step before running our server on ECS is to mount the encrypted EBS volume with the OpenVPN config into the EC2 instance so it’s accessible to the container.

To do this, just go to the “EC2” pane in AWS, select “Volumes” on the left under the Elastic Block Store (EBS) heading and locate the volume we used earlier with the instance where we generated the certificates. Right-click on it and attach it to the ECS instance, giving it a device name of /dev/sdf.

Next, we need to mount the volume onto a directory in the ECS instance filesystem. With the keypair you downloaded when setting up the ECS cluster, SSH into the ECS instance. The instance will probably be called something like ECS Instance - EC2ContainerService-openvpn in EC2.

ssh -i /path/to/keypair.pem ec2-user@<ecs-instance-public-ip>

Once you log in, create the directory to mount the volume on and add an entry to /etc/fstab so the volume is automatically mounted on reboot.

mkdir -p /ecs-data/openvpn-data
echo "/dev/sdf /ecs-data/openvpn-data ext4 defaults 0 2" >> /etc/fstab
mount -a

That’s it as far as set-up goes! All that’s left to do now is to launch the OpenVPN server container. Navigate back to the ECS console, select your openvpn cluster, select the “Tasks” tab and click on “Run new task”. Launch options below:

  • Launch type: EC2
  • Task definition: pick the latest revision of your openvpn task definition. If you didn’t make any changes to it after creating it, it should be the only option on the dropdown, e.g. openvpn:1.
  • Cluster: openvpn
  • Number of tasks: 1

Leave the defaults for everything else and click on “Run Task” at the bottom right to confirm. If everything went alright, you should see a new entry appear under the Tasks tab of your openvpn cluster. For the first few seconds, its last status will be different to its desired status of “RUNNING”. You’ll know the container has initialised correctly if after refreshing the table, the last status is “RUNNING” too. If this is the case, you’re pretty much good to go from the server’s perspective! You may click on the Task ID and then on the “Logs” tab to see the container log output. Container logs can also be viewed on CloudWatch. Make sure you change the retention policy on the /ecs/openvpn log group to something other than “Never” as that might cost you some money!

To connect to the server from a Linux command prompt, locate your client OpenVPN profile file in your computer and launch the OpenVPN client like so

sudo openvpn /path/to/client-name.ovpn

You can verify it’s working as expected by searching “what is my ip” on your favourite search engine. It should display the IP address of the ECS EC2 instance and your ISP should be Amazon. Congratulations, you just deployed your own VPN server to AWS! Enjoy your private and secure browsing!

What’s Next

We have an OpenVPN server up and running in ECS now, but at the moment it won’t be very tolerant to failure. An outage in AWS or the underlying EC2 hypervisor may wipe out your instance and therefore your server. By default, ECS will launch a new instance automatically but it will not associate the same IP address to the new instance. Moreover, ECS will not launch a new openvpn task if the active one exits.

In the next post, I’ll talk about the improvements that could be made to the current, barebones setup to make it fully self-healing and reliable.

Credits to kylemanna for providing the OpenVPN server Docker image I used in this tutorial as well as the setup steps.