personal AWS VPN using OpenVPN

Using Ansible Container, Ansible, and AWS, deploy a personal VPN server, while also creates a specified user, the appropriate OpenVPN configuration file, and saves it to the local user’s desktop.


Having a personal VPN server is immensely useful; we live in a mobile world with plenty of wifi hotspots. With these open access points, security is definitely a large issue. There are VPN services available, but it is often desirable to manage our own service. By creating our own personal VPN service, we can protect our network traffic and manage that service accordingly.

Price wise, this is somewhat close to the costs of common VPN services out there. If starting out with a new AWS account, it will be nearly free, as AWS provides 750 hours of t2.micro for free. If you are using an existing account that is not free tier eligible, the AWS t2.nano on-demand pricing costs $4.75/month, plus storage (~$0.80/month for 8 GB) and bandwidth ($0.01/GB OUT of the instance). So if a t2.nano on-demand instance is used for one month, and 100 GB of data is transferred, this will cost about $6.55 for the month. Not bad. This can be further dropped by using either spot instance, reserved instance, or on-demand instance with starting/stopping the instance as needed.

quickstart starting from zero

  • Sign up and register for an AWS account.
  • After creating the account, create an IAM user.
  • Give this user the Administrator role (or restrict the policy if needed).
  • Create an AWS access/secret key pair. Save them somewhere safe. Export them as environment variables:
  • Optional - create the SSH key pair (optional - modify your ansible.cfg as needed, if you save it to a non-default path).
# it will prompt you if a key already exists.  
ssh-keygen -t rsa -b 4096
  • Save your SSH private and public key to your favorite password manager for future reference.
  • Install Ansible. This requires virtualenv. You can do this with brew or whatever your preferred package manager is. This uses’s OS X’s default:
# install virtualenv
sudo pip install virtualenv
# install Ansible and other needed packages (runs from the base directory of the playbooks repo)
# activate the virtualenv
sourece ./env/bin/activate
# change to the ansible-playbooks directory
cd ansible-playbooks
  • Run the create_vpc.yml playbook. This will create a VPC, subnets, security groups, and all the other things needed for AWS. Example that can be used:
ansible-playbook vpc_create.yml -e "env=dev"

Note - sometimes availability zones are “full”. Modify vars/dev_aws/aws.yml accordingly.

  • Finally, to deploy the OpenVPN server:
ansible-playbook openvpn.yml -e "env=dev public_ip=true instance_type=t2.micro"

Note the instance type extra variable. This overrides the default t2.nano instance size. Free tier is only applicable to t2.micro.

  • Import the user.ovpn into your preferred VPN client. Test accordingly.

The next sections describe some of the details of the OpenVPN Docker container and Ansible playbook.

the container

I’ve mostly been using Ansible Container to build custom containers. If fully invested in the Ansible world and making somewhat complex Docker images, it’s pretty useful and will only improve over time (still version 0.2!). In this case, the OpenVPN Docker image was created using Ansible Container.

This Docker image is very similar, albeit simplified and built with a singular purpose, to the project here:

The container was built with the ability to easily automate the task of initializing the OpenVPN CA using easy-rsa. Modified easy-rsa scripts were created/added, so that they were non-interactive. More information can be found here, at the Ansible Container OpenVPN project.

With this customized OpenVPN container, the OpenVPN Ansible playbook can deploy an OpenVPN server, create the user, and retrieve the OVPN configuration file. The Docker image is created and pushed with the following commands:

ansible-container build --with-volumes ${PWD}/../ansible-roles:/roles
ansible-container --debug push --with-volumes ${PWD}../ansible-roles:/roles --with-variables docker_url=,docker,namespace=modops

Replace docker_url and namespace with whatever Docker repository is preferred.

the playbook

Now that the container is created and pushed, the playbook can deploy it. The playbook (found here), can be run:

ansible-playbook openvpn.yml -e "env=mod public_ip=true"

There are a few extra variables that can be passed in, but in this case, there are two; env and public_ip. env is the environment; this allows for a playbook to be reused and run against multiple AWS environments, be it development, testing, or production. mod is simply the name of one of the AWS environments used. public_ip is a boolean value for the aws.security_groups role. This adds the public IP address of where the playbook is running to the security group. This way, no bastion host or NAT gateway is needed. Useful for simple instance deployments, like this OpenVPN server. If a bastionhost, VPN, or some other inbound NAT gateway is used, there is no need to use public_ip=true. NOTE - if using a new AWS account and the free tier, add the extra variable instance_type=t2.micro.

A couple playbook highlights… The playbook will create and configure the AWS security group, the IAM role for the instance, and the OpenVPN EC2 instance itself. Note that this playbook makes liberal use of tags and variables, so it is recommended to use those tags accordingly. After the AWS resources are deployed, it scans AWS for the SSH public key (blog post describing that here, and then adds the found instances (again based on AWS tags) to Ansible groups using the ansible.groups_init role. The next play configures the openvpn_public host (the group created from the ansible.groups_init role). Note that there are automatic updates configured (along with automatically rebooting). See this role for more information.

The OpenVPN Docker container (which is added in the The Ansible Container created and pushed above (which can be found here) is deployed and started with the instance.openvpn_configure role is run. This configures the OpenVPN systemd service, OpenVPN volume directories, and starts the service.

The final role, instance.openvpn_create_user_config, takes care of the client configuration magic. It retreives the public IP address of the instance, as that is needed for the client OVPN configuration file. It creates the user, based on the openvpn_user variable (defaults to user). After creating the user, it gets the private key, public key, and CA public key. With this information, it templates the OVPN configuration file and pulls that to the local Desktop.

This configuration can be loaded into Tunnelblick, Viscosity, or any other OpenVPN client software. Instant ~$5/month private VPN server.


A playbook for deploying a personal VPN server. Pretty useful for managing and running your own infrastructure and should be competitive with other VPN services out there.

future improvements

  • Verify there is no IPv6 leakage (note that Tunnelblick disables IPv6 by default).
  • Add additional protocols, such as IKEv2 or IPSec.
  • Use CFSSL instead of easy-rsa. It would also be a good idea to seperate the SSL keys from the VPN service.
  • Add a shred keys option, where the CA key and all private keys are deleted off the instance.
  • Deploy Ansible Container and create a systemd service that automatically rebuilds and redeploys the container, thus ensuring the container is always up-to-date.
  • Evaluate what this would look like deploying to Kubernetes.
  • Consider deploying spot instances.