Ephemeral remote access IPSEC VPN server with Terraform and StrongSWAN
Dynamic VPN with Terraform and Strongswan
Introduction
StrongSWAN is a great opensource product for building software VPN networks, based on IPSEC. It is really easy to build Site-2-Site or Remote-Access VPN with different architectures using StrongSWAN, lots of examples are published in their wiki. At the same time this piece of software provides great test suite options for integration testing.
In this post we will try to make an automated and ephemeral remote access VPN server using Terraform infrastructure as a code abstraction tool, Digital Ocean and StrongSWAN. For convenience I have created github repository with all source codes and Makefile.
The remarkable part about this proposal is the fact that you are running Digital Ocean droplet only for the duration of VPN session and it could be destroyed afterwards. A pre-shared secret key is being generated every time uniquely and dynamically pushed into the configuration of the VPN server and local client.
Terraform providers
To implement the idea we are going to use the following terraform providers:
- digital ocean
- ssh-key
- randomkey
Please refer to Terraform Digital Ocean documentation.
Digital Ocean API and provider
Digital Ocean is one of the many providers that terraform supports (e.g. AWS, GCP and etc). Digital Ocean have a minimal API interface that allows you programmatically interact with their cloud. For example to list available droplet images of Ubuntu distributions you can use following command:
curl -X GET "https://api.digitalocean.com/v2/images?per_page=999" -H "Authorization: Bearer $TF_VAR_do_token" |jq ' .images[] | select( .distribution == "Ubuntu") '|grep 16-04
To find regions of Digital Ocean presence we can use another API call:
curl -X GET "https://api.digitalocean.com/v2/regions?per_page=999" -H "Authorization: Bearer $TF_VAR_do_token" |jq ' .[][] | .slug'
"nyc1"
"sfo1"
"nyc2"
...
The variable called $TF_VAR_do_token
should be set, this variable must contain digital ocean token. The TF_VAR
prefix is used by terraform to parse terraform related values from the shell environment.
For instance in the experiment described here we used the following variables:
TF_VAR_do_token=token
TF_VAR_domain_name=domain-name
TF_VAR_droplet_name=droplet-name
TF_VAR_ssh_key=~/.ssh/id_rsa_do.pub
TF_VAR_do_region=region # e.g. nyc1
Each variable with TF_VAR
suffix we can use later in the terraform template.
SSH key provider
Terraform allows you to upload ssh key from file on your local laptop to the Digital Ocean cloud using following provider:
resource "digitalocean_ssh_key" "do_sshkey" {
name = "Digital Ocean"
public_key = "${file("${var.ssh_key}")}"
}
where ${var.ssh_key}
is TF_VAR_ssh_key
variable from your environment. The variable represents a path to an ssh public key.
Random provider
Random provider is a great way to generate cryptographically random value, which could be used throughout the terraform template:
Terraform template to create Digital Ocean instance
Lets put all our providers and resources together. In this configuration file we are doing the following:
- Declaring our variables, that would be obtained from environment
- Using additional three resources
digitalocean_droplet
- which allows to create DO instance with defined parametersdigitalocean_domain
- create a domain name within Digital Oceandigitalocean_record
- create a domain record inside the created zone
As you can see we are using interpolation inside terraform file, with ${resource_name}
syntax construct.
Template files for terraform
Terraform can use template_file resource to run the commands as user_data inside digitalocean_droplet
resource or any other resources. When the instance is getting bootstrapped the file is being executed as a shell script. We are keeping it short and the main script is being downloaded from github repository:
#cloud-config
runcmd:
- curl https://raw.githubusercontent.com/logingood/dovpn/master/init.sh -o /tmp/init.sh
- chmod +x /tmp/init.sh
- /tmp/init.sh ${secret_key} | dd of=/var/log/bootstrap.log
${secret_key}
is the parameter that we are sending inside the init script, it is take from random_id
terraform provider.
Strongswan
Strongswan is a solid open source product for different IPSEC scenarios. In this short post we will look closely to remote access VPN scenario. Strongswan uses concept of “road warriors” which are remote connecting IPSEC clients. As probably you know IPSEC consists of the set of different protocols and standards, such as IKE, ISAKMP, DH and cryptographic transforms. These building blocks of IPSEC could be used in various ways in order to implement protection suites that satisfy the needs or meet requirements.
In this particular example we will use IKEv2, see RFC 7296. IKEv2 is supported by most of modern handsets, such as Apple iphone, ipad and etc (due to MOBIKE support), along with Windows and OS X based clients. The main difference between IKEv1 and IKEv2 is the way connection established. Instead of using quick and main mode during two phases as it is implemented with IKEv1, IKEv2 secures connection in four messages. That delivers certain security advantages and hardens parties’ identity validation. Additionally, a certain amount of flexibility has been added, e.g. now each party can use its own way of authentication (PSK, RSA and etc) contrary to IKEv1 where parties must use the same authentication method.
For simplicity we will use PSK authentication mode for road warrior with IKEv2. IKEv2 is implemented in Strongswan using Charon daemon.
Strongswan VPN Server configuration
Let’s look into server configuration file - ipsec.conf
:
We are using $IP_ADDRESS in the init.sh which could be extracted from Digital Ocean meta information url, e.g.
IP_ADDRESS=$(curl http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)
Provided API is quite convenient for self-provisioning purpose.
Another important configuration file that we are rendering dynamically called ipsec.sercrets
, there we will put a random value that was generated by terraform random_id resource
echo droplet phone : PSK \"${1}\" >> /etc/ipsec.secrets
You could noticed that we used leftid=droplet
and rightid=phone
, the same values should be set in ipsec.secrets
, or they could be replaced by keyword %any
. The order droplet phone
is important and should be opposite for client configuration.
Interesting traffic in our case is a default route, and remote clients are receiving IP addresses from the pool 10.0.0.0/24
.
It is important to pay attention to the traffic direction. Strongswan always calls left
VPN server and right
remotely connecting clients (road warriors). But if you are setting up client side, then these two values should be changed respectively.
VPN Client configuration
Short snippet of strongswan client could be found below:
As the right
you should put the domain name of your VPN server. Having a domain name in TF_VAR_domain_name
could be useful as an IP address would be changed each time you destroy and recreate the instance.
Using makefile to create a temporary droplet for VPN purpose
As I have mentioned in the beginning we can keep instance running only for the duration of VPN session. To do this we can wrap terraform apply
and terraform destroy
commands with make file adding some additional actions.
We are extracting IPSEC_KEY=$(shell terraform show|grep hex | cut -f 5 -d " ")
from terraform show
command using shell tools. And an extra check has been added if we are running client based configuration, for those who doesn’t have strongswan as a client installed.
Sleep timeout for 120 seconds required to let terraform bring a digital ocean instance fully up before trying to connect it.
When destroying the instance we should use ipsec
cli to take the client down before destroying terraform instance. That will ensure the kernel to gracefully close the connection and start routing traffic unencrypted.
Now you can create a droplet and VPN tunnel with
make all
And destroy everything using:
make destroy
Hopefully this simple example could be useful in certain applications, and could potentially save you some money on cloud providers. Not every task requires to have AWS,GCP, Digital Ocean and etc instances up and running all the time.