Rolling Out Your Own VPN in AWS ─ Part 1

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:

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

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:

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:

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

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:

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.

Back to top ↑

Do you have any questions, comments or feedback about this article to share with me or the world?

Send an email to my public mailing list. You can also reach out to me privately if you'd prefer. I would love to hear your thoughts either way!

Articles from friends and people I find interesting

FAFO 7

Joy projects. Impact. Transclusion?

via ronjeffries.com February 2, 2024

Two handy GDB breakpoint tricks

Over the past couple months I’ve discovered a couple of handy tricks for working with GDB breakpoints. I figured these out on my own, and I’ve not seen either discussed elsewhere, so I really ought to share them. Continuable assertions The assert macro …

via null program January 28, 2024

Using Hugo as a redirect service

I have been building my website with Hugo since early 2021. I love the control it gives me. I recently wanted to start using short URLs in presentations, that would link to either a longer URL on the website or to somewhere else altogether. It turns out Hu…

via Blog on Dan North & Associates Limited October 23, 2023

Generated by openring