How to create a VPN using Terraform in Digital Ocean - Infrastructure tutorial part one

Recently I've become steadily more and more excited about the latest developments in the DevOps arena. Tools like Terraform and Puppet allow us to create templates for an entire infrastructure, templates that can be repeated and mutated ad nauseam.

In this series of tutorials we will be using Terraform, Puppet, Ansible and other tools to create our own application hosting environment within Digital Ocean. The methods presented in these tutorials should be easily transferable to other providers.

You can see some sample code to go along with this tutorial in my example Github repo.

Note: This tutorial has been updated to use Debian 8.

What are we building?

Before we dive into Terraform I think we should take a look at what we are going to be creating:

               +---------+    +--------------------+   
+--------------+         +----+                    +--+
|              | VPN box |    | Loadbalancer boxes |  |
|    ----------+         +----+                    |  |
|    |         +---^-----+    +-----^------+---^--++  |
|    |             |                |      |   |  |   |
|    |             |                |      |   |  |   |
|   ++------------------------------+  +---v---+--++  |
|   | Mgmt boxes +---------------------> Web boxes |  |
|   ++--+--------+                     +--^---+---++  |
|    |  |                                 |   |   |   |
|    |  |                                 |   |   |   |
|    |  |                                 |   |   |   |
|    |  +----------------+    +-----------+   |   |   |
|    |                   |    | |-------------+   |   |
|  +-+---------+      +--v----+-v-+               |   |
|  |  DNS box  +------+ DB boxes  +---------------+   |
|  +-----------+      +-----------+                   |
|                                                     |
+-----------------------------------------------------+

We are going to be creating a VPN box that we will use to create a secure private network for our instances, all boxes within the VPN will be locked down to communicte over the VPN only.

To SSH into one of these boxes from your home machine you will need to be on the VPN as well, the VPN box and Load Balancers are exceptions to this and will be able to receive relevant traffic from the web, otherwise they would be fairly useless!

Within our private network we will have management boxes that will serve as Puppet masters, we bootstrap our nodes in masterless fashion but we will need a master to manage instances once they have been built.

The Web and DB boxes will house applications, they will be built once we get our base infrastructure up and running.

Here the tasks that need to be completed before we can start making the application focused boxes:

  • Create a secure private network.
  • Private DNS server for managing box hostnames over the VPN.
  • Puppet config and master for managing instances.

Once these steps are completed we will have a secure, flexible network that will be managed in an automated fashion.

Setting up for terraforming droplets

Before we start you will need to setup a few things within Digital Ocean so you can use Terraform with your Digital Ocean account.

First off you need an API access token. To generate this login to the Digital Ocean control panel, go to the API section then generate a new personal access token, give it any name you want and copy the resulting token and keep it somewhere safe.

Next you need to add an SSH key into Digital Ocean, skip this part if you already have a key in your account. Generate an SSH key on your machine, we will be using this key to access your entire infrastructure so please keep it safe! Since this key will be required to access everything you may want to distribute it to a few machines so I would give it a unique name as opposed to the standard id_rsa name. Once you've got an SSH key we just need to add it to Digital Ocean, login to the control panel and go to the security tab in your account settings, from there you can add a new SSH key, give it a name and add the contents of the public key into there.

You may want to read up on Terraform a bit and possibly follow one of the introductory guides from the Terraform site. To sum up, it is essentially an infrastructure orchestration tool, you define what machines/instances you want in config files and Terraform ensures that they exist, creating instances for you and bootstraping them according to your specification. Once you get rolling it makes provisioning new machines a doddle.

Go ahead and install Terraform according to the Terraform docs.

At this point I will assume the following things:

  • You have an API key for use with Digital Ocean's API.
  • You have SSH keys that have been added to your Digital Ocean account.
  • You are at least vaguely familiar with Terraform.

With these elements in place let's start creating our infrastructure. I recommend using private git repos to store your work, I use Bitbucket since they provide free private repos but you can use whatever you want for this.

Domain name needed

You should think of a domain name for your infrastructure first off, this could be an organisation name, your name, your cats name, anything really. You will use this to give your droplets an FQDN, I tend to give my droplets an FQDN straight away when creating them as this makes life easier later on.

Make sure that your domain name ends in a top level domain like .com, .co.uk or whatever you like, don't worry about making it unique, it won't be published into the global domain namespace.

We need a plan

OK we are ready to get terraforming, finally!

On your local machine create a parent folder for your infrastructure, something like example.domain.com-inf in your home directory. This will directory will house Terraform config, Puppet config and anything else related to the infrastructure. Don't check the whole folder into Git, each constituent part should be a separate repo that you clone into your parent folder, this will make your life easier.

Create a terraform folder within your project folder and create two files, provider.tf and vpn.example.domain.com.tf within it like so:

mkdir -p ~/example.domain.com-inf/terraform
cd ~/example.domain.com-inf/terraform
touch provider.tf vpn.example.domain.com.tf

Inside provider.tf setup options for the Digital Ocean Terraform provider like so (we will pass these vars in when we invoke Terraform on the command line):

# ~/example.domain.com-inf/terraform/provider.tf
variable "do_token" {}
variable "pub_key" {}
variable "pvt_key" {}
variable "ssh_fingerprint" {}

provider "digitalocean" {
  token = "${var.do_token}"
}

Next let's add the basics of the vpn droplet to vpn.example.domain.com.tf like so:

# ~/example.domain.com-inf/terraform/vpn.example.domain.com.tf
resource "digitalocean_droplet" "vpn-example-domain-com" {
    image = "debian-8-x64"
    name = "vpn.example.domain.com"
    region = "ams3"
    size = "512mb"
    private_networking = false
    ipv6 = false
    ssh_keys = [
      "${var.ssh_fingerprint}"
    ]

  connection {
      user = "root"
      type = "ssh"
      key_file = "${var.pvt_key}"
      timeout = "2m"
  }

  provisioner "remote-exec" {
    inline = [
      "export PATH=$PATH:/usr/bin",
      "sudo apt-get update",
      "sudo apt-get -y upgrade"
    ]
  }
}

To run the Terraform commands to build your infrastructure you will need a few things, the location of both the public and private parts of the SSH key that you wish to use, your Digital Ocean token and the fingerprint of the public part of the SSH key, you can grab this like so:

ssh-keygen -lf ~/.ssh/[KEY NAME].pub | awk '{print $2}'

You can check to see what Terraform will actually do with the following command, note this will not actually create any instances (replace the placeholders as needed):

terraform plan -var "do_token=[DO TOKEN]" -var "pub_key=$HOME/.ssh/[KEY NAME].pub" -var "pvt_key=$HOME/.ssh/[KEY NAME].pem" -var "ssh_fingerprint=[SSH FINGERPRINT]"

In the output you should see '+digitalocean_droplet.{droplet name}', meaning that Terraform will create the droplet when you apply this plan.

You can actually apply the plan and create the droplet like so:

terraform apply -var "do_token=[DO TOKEN]" -var "pub_key=$HOME/.ssh/[KEY NAME].pub" -var "pvt_key=$HOME/.ssh/[KEY NAME].pem" -var "ssh_fingerprint=[SSH FINGERPRINT]"

The output should show the creation of the instance followed by the apt-get update commands, magic!

So you now have the beginnings of a plan for the infrastructure, but it's only a test one, so for now just destroy the created instance so you don't get charged for it:

terraform destroy -var "do_token=[DO TOKEN]" -var "pub_key=$HOME/.ssh/[KEY NAME].pub" -var "pvt_key=$HOME/.ssh/[KEY NAME].pem" -var "ssh_fingerprint=[SSH FINGERPRINT]"

Setting up OpenVPN

We are now going to use Terraform to set up a VPN machine that we will use to secure future instances, we are going to create our own private network that all of our instances will live on and then block pretty much all traffic on the external interfaces of our instances.

To setup our VPN instance we need to create keys and config for our VPN instance, we will also create one for your home computer so you can test that everything works.

The keys and config will be created on your local machine, this is so you can recreate the VPN box if you need to, it will have the same config and keys everytime so clients will be able to reconnect in the event that you lose the VPN machine.

Generating keys

Generating keys is pretty straight forward, first off we generate a set of base keys that will be used to generate all the other keys, we are going to store these in a key store directory so that when we actually install our VPN config on an instance we can copy the keys that we need to each instance, this will make more sense when we take a look at the Terraform plan later on.

On your local machine run the following commands to install OpenVPN and prepare a few things (these instructions are for Ubuntu/Debian, adjust as needed):

sudo apt-get install easy-rsa
cd ~/example.domain.com-inf
mkdir -p openvpn/key-store
cp -R /usr/share/easy-rsa/ openvpn/
cd openvpn/easy-rsa
vi vars

Adjust KEY_COUNTRY, KEY_PROVINCE, KEY_CITY, KEY_ORG, KEY_EMAIL as needed and ensure KEY_SIZE is 2048, before saving the file set KEY_DIR to the key store directory you created earlier eg. "$HOME/example.domain.com-inf/openvpn/key-store"

Now we need to generate the certificate authority certificate, key and Diffie-Hellman parameters, this step allows the generation of further keys for the server and client machines:

source vars
./clean-all
./build-ca
./build-dh

This will place the keys in "$HOME/example.domain.com-inf/openvpn/key-store", make sure that you do not run the ./clean-all command again as this will remove the keys that are required to generate compatible keys for future clients.

Next generate the server key:

./build-key-server vpn.example.domain.com

Press enter/y for all the options and do not enter a passphrase.

Generating keys for clients can be done like so (from with the easy-rsa directory);

source vars
./build-key [client FQDN]

Create server config files

Now that we have some keys we next need to create a configuration file for the VPN droplet. As with the keys we will create our configs in a config-store directory so when we bootstrap an instance we can copy it's config from this store using Terraform:

cd ~/example.domain.com-inf
mkdir openvpn/config-store
vi openvpn/config-store/vpn.example.domain.com.conf

Set the contents like so:

mode server
ca keys/ca.crt
cert keys/vpn.example.domain.com.crt
key keys/vpn.example.domain.com.key
dh keys/dh2048.pem
ifconfig-pool-persist ipp.txt
client-config-dir client-configs
proto udp
port 1194
comp-lzo
group nogroup
user nobody
status status.log
dev tun0
server 10.8.0.0 255.255.0.0
keepalive 10 120
topology net30
client-to-client
persist-key
persist-tun

Cient config

Before we move onto the Terraform part let's create a config file for a remote machine, such as the one your using now, like so:

vi openvpn/config-store/remote-machine-dreed.conf

Replace 'dreed' with your own username as appropriate. Set the contents of the file like so (you will have to fill in the IP once you've spun up the VPN instance):

client
ca keys/ca.crt
cert keys/remote-machine-dreed.crt
key keys/remote-machine-dreed.key
dev tun
proto udp
remote [VPN INSTANCE IP] 1194
comp-lzo
resolv-retry infinite
auth-retry none
nobind
persist-key
persist-tun
mute-replay-warnings
ns-cert-type server
verb 3
mute 20

Next let's generate keys for your home computer:

cd ~/example.domain.com-inf/openvpn/easy-rsa
source vars
./build-key remote-machine-dreed

Finally to complete our client config it's generally a good idea to assign a static IP to each client, to do this we need to create some client config files that the VPN server will use to assign IPs to the client based on their key name:

cd ~/example.domain.com-inf/openvpn/
mkdir client-configs
vi client-configs/remote-machine-dreed

Set the contents to:

ifconfig-push 10.8.0.100 10.8.0.101

I am starting from 100 here because if a client connects without a config file it will get assigned the next available IP address starting from the VPN servers IP (which is 10.8.0.1). If a client connects without a corresponding config file on the server before one of your configured ones you may get an IP clash, hence it's best to leave some space for unconfigured clients.

For future clients you will need to add 4 to the previous clients first IP value (this is just a requirement of OpenVPN), hence the next client's IP will need to be '10.8.0.104 10.8.0.105', after that '10.8.0.108 10.8.0.109' and so on.

Can we Terraform yet?

Yes! We are now ready to create the VPN server, we will use Terraform to create the instance and bootstrap it with the config and keys that we have just created.

Edit the the VPN instance plan that we created earlier and set the contents like so:

# ~/example.domain.com-inf/terraform/vpn.example.domain.com.tf
resource "digitalocean_droplet" "vpn-example-domain-com" {
  image = "debian-8-x64"
  name = "vpn.example.domain.com"
  region = "ams3"
  size = "512mb"
  private_networking = false
  ipv6 = false
  ssh_keys = [
    "${var.ssh_fingerprint}"
  ]

  connection {
      user = "root"
      type = "ssh"
      key_file = "${var.pvt_key}"
      timeout = "2m"
  }

  # Copy openvpn config (need to create symlink in files)
  provisioner "file" {
    source = "files"
    destination = "/root"
  }

  # Bootstrap openvpn
  provisioner "remote-exec" {
    inline = [
      "export PATH=$PATH:/usr/bin",
      "sudo apt-get update",
      "sudo apt-get -y upgrade",
      "sudo apt-get -y install openvpn",
      "mkdir -p /etc/openvpn/keys",
      "cp /root/files/openvpn/config-store/vpn.example.domain.com.conf /etc/openvpn/",
      "cp /root/files/openvpn/key-store/ca.crt /etc/openvpn/keys/",
      "cp /root/files/openvpn/key-store/vpn.example.domain.com.crt /etc/openvpn/keys/",
      "cp /root/files/openvpn/key-store/vpn.example.domain.com.key /etc/openvpn/keys/",
      "cp /root/files/openvpn/key-store/dh2048.pem /etc/openvpn/keys/",
      "cp -R /root/files/openvpn/client-configs/ /etc/openvpn/",
      "sh /root/files/firewall-server.sh",
      "sh -c 'iptables-save > /etc/iptables.conf'",
      "echo 'post-up iptables-restore < /etc/iptables.conf' >> /etc/network/interfaces",
      "rm -rf /root/files",
      "chmod -R 400 /etc/openvpn/keys",
      "service openvpn@vpn.example.domain.com start"
    ]
  }
}

So what's going on here then? As in the previous part of this tutorial we are spinning up a Digital Ocean instance, in addition to this OpenVPN is installed and bootstrapped with the config that we just created.

Terraform comes with a file provisioner that allows local files to be pushed to an instances, in this case we are copying our OpenVPN config to the droplet. To make this part work we need to create the files directory and create a symlink to the OpenVPN config:

cd ~/example.domain.com-inf/terraform
mkdir files
ln -s ~/example.domain.com-inf/openvpn files/openvpn

The remote-exec provisioner installs OpenVPN and copies the parts that we need from the OpenVPN config and deletes anything that is not needed. Before Terraform starts OpenVPN firewall rules need to be created to lock the instance down (we will expand these in later tutorials), you can see some commands in the remote-exec provisioner that handles this. Always lock down an instance down as soon as possible, you'd be surprised at how many random attacks you will receive even if your box is new and empty.

Let's create the firewall-server.sh shell script in the terraform/files directory that will bootstrap iptables:

vi ~/example.domain.com-inf/terraform/files/firewall-server.sh

Set the contents like so:

#!/bin/sh

# Drop everything and clear rules
iptables -P OUTPUT DROP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -F

# Drop input and forward requests by default
iptables -P OUTPUT ACCEPT
iptables -P INPUT DROP
iptables -P FORWARD DROP

# Prevent external packets from using loopback addr
iptables -A INPUT -i eth0 -s 127.0.0.1 -j DROP
iptables -A FORWARD -i eth0 -s 127.0.0.1 -j DROP
iptables -A INPUT -i eth0 -d 127.0.0.1 -j DROP
iptables -A FORWARD -i eth0 -d 127.0.0.1 -j DROP

# Loopback
iptables -A INPUT -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -d 127.0.0.1 -j ACCEPT

# Allow ping
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Openvpn over external
iptables -A INPUT -i eth0 -p udp --dport 1194 -j ACCEPT

# Accept all via vpn
iptables -A INPUT -i tun+ -j ACCEPT
iptables -A FORWARD -i tun+ -j ACCEPT

# Keep state of connections from local machine and private subnets
iptables -A OUTPUT -m state --state NEW -o eth0 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m state --state NEW -o eth0 -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED

Ok that's it, this has been a long road! We are now ready to apply the Terraform config like so:

# destroy the current instances if any:
terraform destroy -var "do_token=[DO TOKEN]" -var "pub_key=$HOME/.ssh/[KEY NAME].pub" -var "pvt_key=$HOME/.ssh/[KEY NAME].pem" -var "ssh_fingerprint=[SSH FINGERPRINT]"

# bring up our shiney new vpn instance
terraform apply -var "do_token=[DO TOKEN]" -var "pub_key=$HOME/.ssh/[KEY NAME].pub" -var "pvt_key=$HOME/.ssh/[KEY NAME].pem" -var "ssh_fingerprint=[SSH FINGERPRINT]"

Can we connect?

Once the instance is up and running we can go ahead and test the VPN, install OpenVPN on your local machine and copy over your config and keys like so:

sudo apt-get install openvpn
sudo mkdir -p /etc/openvpn/keys
sudo cp ~/example.domain.com-inf/openvpn/config-store/remote-machine-dreed.conf /etc/openvpn/
sudo cp ~/example.domain.com-inf/openvpn/key-store/ca.crt /etc/openvpn/keys/
sudo cp ~/example.domain.com-inf/openvpn/key-store/remote-machine-dreed.crt /etc/openvpn/keys/
sudo cp ~/example.domain.com-inf/openvpn/key-store/remote-machine-dreed.key /etc/openvpn/keys/
sudo chmod -R 400 /etc/openvpn/keys

Make sure that you pop the IP of the vpn instance into the remote-machine-dreed.conf file.

Instead of using the OpenVPN service to bring up the connection we can invoke OpenVPN directly, this will immediately show any errors in the output  if we've made any mistakes:

sudo openvpn /etc/openvpn/remote-machine-dreed.conf

Hopefully you'll see an error free start up, if so run ifconfig in another terminal window, in the output you should see a new tun0 connector. Next ping the vpn server (ping 10.8.0.1) and you should see some packets getting sent and received.

Exit out of the manually started instance and start the OpenVPN service properly:

sudo service openvpn start

At this point you can ssh into the VPN instance and check that you can ping your home computer:

ssh -i ~/.ssh/[KEY NAME].pem root@10.8.0.1
ping 10.8.0.100

If you encounter any errors whilst testing the VPN connection please check the OpenVPN documentation and forums, I normally find a post and an answer to most problems that I encounter.

Adding a box to the VPN

So we've created a VPN server that we can recreate to our hearts content, destroy the whole plan by issuing a terraform destroy command and rebuild it a few times to check that the VPN does indeed come back up and can be connected to. Sometimes the instance might come back with a different public IP, in which case you will need to update your home computer's VPN client config. At the moment Digital Ocean does not allow for the reservation of IP addresses (so no elastic IP as in AWS), however we can leverage Digital Ocean's nameservers to assign hostnames to droplets as we create them.

Let's add a dummy Digital Ocean instance to the VPN, at the moment this box won't do anything useful but it will serve as a template for future instances.

Create a new terraform file called dummy.example.domain.com.tf and set the contents like so:

# ~/example.domain.com-inf/terraform/dummy.example.domain.com.tf
resource "digitalocean_droplet" "dummy-example-domain-com" {
  depends_on = ["digitalocean_droplet.vpn-example-domain-com"]
  image = "debian-8-x64"
  name = "dummy.example.domain.com"
  region = "ams3"
  size = "512mb"
  private_networking = false
  ipv6 = false
  ssh_keys = [
    "${var.ssh_fingerprint}"
  ]

  connection {
      user = "root"
      type = "ssh"
      key_file = "${var.pvt_key}"
      timeout = "2m"
  }

  # Copy openvpn config (need to create symlink in files)
  provisioner "file" {
    source = "files"
    destination = "/root"
  }

  # Bootstrap openvpn
  provisioner "remote-exec" {
    inline = [
      "export PATH=$PATH:/usr/bin",
      "sudo apt-get update",
      "sudo apt-get -y upgrade",
      "sudo apt-get --force-yes -y install openvpn dnsmasq",
      "cp /root/files/dns/dnsmasq.conf /etc/",
      "service dnsmasq restart",
      "mkdir -p /etc/openvpn/keys",
      "cp /root/files/openvpn/config-store/dummy.example.domain.com.conf /etc/openvpn/",
      "cp /root/files/openvpn/key-store/ca.crt /etc/openvpn/keys/",
      "cp /root/files/openvpn/key-store/dummy.example.domain.com.crt /etc/openvpn/keys/",
      "cp /root/files/openvpn/key-store/dummy.example.domain.com.key /etc/openvpn/keys/",
      "cp -R /root/files/openvpn/client-configs/ /etc/openvpn/",
      "sh /root/files/firewall-client.sh",
      "sh -c 'iptables-save > /etc/iptables.conf'",
      "echo 'post-up iptables-restore < /etc/iptables.conf' >> /etc/network/interfaces",
      "rm -rf /root/files",
      "chmod -R 400 /etc/openvpn/keys",
      "service openvpn@dummy.example.domain.com start"
    ]
  }
}

As you can see dnsmasq has been introduced to the mix here, this is so we can leverage Digital Ocean's nameservers and give our VPN instance a static hostname, more on this below. First off however let's go through the easy bits.

We need an OpenVPN client config file for the dummy box, note that I am setting the remote to a hostname (vpn.example.domain.com) rather than an IP:

# ~/example.domain.com-inf/openvpn/config-store/dummy.example.domain.com.conf
client
ca keys/ca.crt
cert keys/dummy.example.domain.com.crt
key keys/dummy.example.domain.com.key
dev tun
proto udp
remote vpn.example.domain.com 1194
comp-lzo
resolv-retry infinite
auth-retry none
nobind
persist-key
persist-tun
mute-replay-warnings
ns-cert-type server
verb 3
mute 20

Now we need a client config file for the server so it will assign our dummy instance a static IP:

# ~/example.domain.com-inf/openvpn/client-configs/dummy.example.domain.com
ifconfig-push 10.8.0.104 10.8.0.105

And of course we need VPN keys for the dummy instance so it can authenticate:

cd ~/example.domain.com-inf/openvpn/easy-rsa
source vars
./build-key dummy.example.domain.com

We also need to create the firewall-client.sh shell script in the terraform/files directory that will bootstrap iptables:

vi ~/example.domain.com-inf/terraform/files/firewall-client.sh

Set the contents like so:

#!/bin/sh

# Drop everything and clear rules
iptables -P OUTPUT DROP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -F

# Drop input and forward requests by default
iptables -P OUTPUT ACCEPT
iptables -P INPUT DROP
iptables -P FORWARD DROP

# Prevent external packets from using loopback addr
iptables -A INPUT -i eth0 -s 127.0.0.1 -j DROP
iptables -A FORWARD -i eth0 -s 127.0.0.1 -j DROP
iptables -A INPUT -i eth0 -d 127.0.0.1 -j DROP
iptables -A FORWARD -i eth0 -d 127.0.0.1 -j DROP

# Loopback
iptables -A INPUT -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -d 127.0.0.1 -j ACCEPT

# Accept all via vpn
iptables -A INPUT -i tun+ -j ACCEPT
iptables -A FORWARD -i tun+ -j ACCEPT

# Keep state of connections from local machine and private subnets
iptables -A OUTPUT -m state --state NEW -o eth0 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -m state --state NEW -o eth0 -j ACCEPT
iptables -A FORWARD -m state --state ESTABLISHED,RELATED

These rules make the client accessible via the VPN/tun interface only.

Leveraging Digital Ocean's nameservers with dnsmasq

That's the easy stuff out of the way, now let's take a look at the dnsmasq parts that have been introduced in the plan for our dummy instance.

At the time of writing Digital Ocean cannot guarantee IP addresses of instances, this means that when we rebuild the stack or if in the future we lose the VPN box for some reason there is a high chance that when we bring up a new VPN instance it will come back with a different public IP, this presents us with a big problem! If this happens and all of our clients are set to connect to the VPN server via it's public IP then we will lose the entire network since our instances are locked down to allow access via the VPN only. Ouch!

In the absence of static IPs we need another method to find the VPN box, we need to attach a hostname to it for use in our client VPN configs, if we have this in place it doesn't matter If the VPN box goes down, as long as it comes up with the same keys and is behind the same hostname then the clients will eventually reconnect, the OpenVPN client on our instances will try to reconnect forever until it reconnects.

Digital Ocean provide their own nameservers (ns[1-3].digitalocean.com), we can add records to their nameservers using Terraform, so let's introduce domain creation to our VPN instance plan by adding the following to the bottom of the VPN plan file file:

# ~/example.domain.com-inf/terraform/vpn.example.domain.com.tf
resource "digitalocean_domain" "default" {
   name = "vpn.example.domain.com"
   ip_address = "${digitalocean_droplet.vpn-example-domain-com.ipv4_address}"
}

Now that the VPN instance has it's own domain that we can point to the next part of the puzzle is setting up our dummy instance so that it uses Digital Ocean's nameservers. For this we will use Dnsmasq as it allows us to combine several nameservers without too much effort, we will use DYN's nameservers for external hosts and Digital Ocean's nameserves for our own hosts.

To achieve this aim we just need to install and configure Dnsmasq, create a dns directory and create a dnsmasq.conf files like so:

mkdir ~/example.domain.com-inf/terraform/files/dns
cd ~/example.domain.com-inf/terraform/files/dns
touch dnsmasq.conf

Set the contents of the file like so:

# ~/example.domain.com-inf/terraform/files/dns/dnsmasq.conf
listen-address=127.0.0.1
# dyn dns servers
server=216.146.35.35
server=216.146.36.36
# do dns servers for externally available do nodes
server=/.example.domain.com/173.245.58.51
server=/.example.domain.com/173.245.59.41
server=/.example.domain.com/198.41.222.173
no-resolv
bind-interfaces
conf-dir=/etc/dnsmasq.d/

Let's test it

OK so that should be everything in place, please refer to my example Github repo here if you are unsure about anything.

Destroy any existing instances using Terraform's destroy command and then apply our new plan. You should see everything come up as expected, you may need to adjust the remote IP in your home machines VPN config.

Once your home machine is connected to the VPN you should be able to ping and login to the dummy server like so:

ping 10.8.0.104
ssh -i ~/.ssh/[KEY NAME].pem root@10.8.0.104

We're done

That's it for now, you can see the Terraform config mentioned in this tutorial on Github here.

Please check out my tutorial on using r10k with Puppet for some more infrastruture learning.

There is a second part to this series as well that shows how to use Puppet to build the VPN.