Stephen Surtees Portrait
Stephen Surtees software consultant

Open VPN

In this post we will setup an OpenVPN server. I’ll be using an Ubuntu instance, hosted on AWS using ec2.

What is a VPN?

A Virtual Private Network (VPN) is a tool that enhances the security of your internet browsing by creating a private connection over a public network. This is especially useful for accessing the internet through public Wi-Fi networks like those found at coffee shops, libraries, and other venues that provide free Wi-Fi.

The Server

I decided to use an Ubuntu instance on AWS as that’s what I’m most familiar with, but this guide should be applicable to the majority of Linux machines.

Ideally your server should have a static IP address. You don’t want to be constantly changing configuration files. The following inbound ports must also be opened.

  • TCP 22 - So we can ssh into the instance and use the scp command to copy key files.
  • UDP 1194 - The default port Open VPN Server listens on.

You can use the following command to connect to your server, just change the username, address and path to the keyfile you are using.

ssh -i ~/.ssh/mykey.pem ubuntu@109.157.16.5

Install Open VPN and dependencies

Let’s install everything we need.

sudo apt update
sudo apt install openvpn easy-rsa

openvpn is the server software, and easy-rsa is a tool we can use to create most of our certificates and key files for the server. You can read more about it here.

Configure Open VPN Server

We need to create a configuration file for our server. Open VPN files can be found at /etc/openvpn. We will change to this directory, create a configuration file and paste a basic configuration into it. I use vim in my examples for editing files, but feel free to use nano or another tool if you prefer.

cd /etc/openvpn
sudo vim server.conf

Paste the following configuration into the server.conf file.

port 1194
proto udp4
dev tun

# Paths to certificate files
ca server/ca.crt
cert server/server.crt
key server/server.key # This file must be kept secret
dh server/dh.pem
tls-auth ta.key 0

topology subnet
server 10.8.0.0 255.255.255.0
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 8.8.8.8"
keepalive 10 120

ifconfig-pool-persist /var/log/openvpn/ipp.txt
status /var/log/openvpn/openvpn-status.log
verb 4

cipher AES-256-CBC
auth SHA512
user nobody
group nogroup

persist-key
persist-tun

The above configuration “works on my machine”. This configuration includes the location of the key files. It also indicates that we intend to use TLS for greater security, the key for this will be placed in the root openvpn directory.

Open VPN comes with example configurations you can look at, which can be found at /usr/share/doc/openvpn/examples/sample-config-files

Enable IP Forwarding

IP Forwarding is usually disabled by default. Use the following command to temporarily enable it.

sysctl -w net.ipv4.ip_forward=1

To make the change persist on reboot, we need to edit the /etc/sysctl.conf file

sudo vim /etc/sysctl.conf

Add the following line, or modify it if it already exists. Make sure that the line is not commented out. It should look like this.

net.ipv4.ip_forward=1

I believe the actual value is stored in /proc/sys/net/ipv4/ip_forward, but it is not recommended to manually edit this file.

Enable Firewall and NAT

We need to enable our firewall and allow our ports through it. These commands are for the “Uncomplicated Firewall” though you can also modify your iptables, but I’d strongly recommend using ufw.

sudo ufw disable
sudo ufw allow ssh
sudo ufw allow 1194/udp

Now modify /etc/ufw/default to change the forwarding policy. This is the part that will allow us to reach out to the internet once we connect to the VPN.

sudo vim /etc/ufw/default

Modify the forward policy to look like this.

DEFAULT_FORWARD_POLICY="ACCEPT"

Finally we can modify /etc/ufw/before.rules.

sudo vim /etc/ufw/before.rules

Insert the following near the top of the file. Again this is for forwarding traffic coming in through the tunnel to the outside world.

*nat
:POSTROUTING ACCEPT [0.0]
-A POSTROUTING -s 10.8.0.0/8 -o eth0 -j MASQUERADE
COMMIT

It’s finally time to enable the firewall (and hopefully not lose our ssh connection)

sudo ufw enable

Lets start the server to see if it works. We normally don’t have to specify it to run on boot, as installing the package will do that for us. By default, openvpn will search the /etc/openvpn directory for configuration files and use those, starting a server for each one that’s found. A very nice feature if you need a backup running on TCP 443 for the rare occasions the default is blocked.

cd /etc/openvpn
sudo openvpn server.conf

Certificate Authority and Server Keys

The first key we need to generate is the TLS key. This is the only one that we can generate using openvpn itself, so lets do that now and deal with the more involved easy-rsa process after.

cd /etc/openvpn
sudo openvpn --genkey --secret ta.key

That was easy. Now lets create the server certificates and keys. For convenience, we’ll copy the easy-rsa scripts into the openvpn folder

sudo cp -r /usr/share/easy-rsa . 

Now we need to copy and rename the vars.example file. You can get away with using the example fields for your certificate authority, but feel free to change them if you want.

cd /etc/openvpn/easy-rsa
sudo cp ./vars.example ./vars
sudo vim ./vars

Lets start by building our certificate authority (CA). It’s important that we do not run the init-pki command more than once, as we would have to start over with all our certificates. Creating and distributing them can be fairly time consuming and delicate.

sudo ./easyrsa init-pki
sudo ./easyrsa build-ca nopass

Upon entering that final command, you will be asked to provide a Distinguished Name. This must be consistent whenever it is asked for. I used “server”. Some of the keys generated will also have this name, and these are referenced in the server.conf file we created earlier. If you open for a different name, you will need to change those paths.

sudo ./easyrsa gen-req server nopass

Again, make sure the correct DN is used. You will also need to type “yes” when asked.

sudo ./easyrsa sign-req server server 

Lets make sure this all worked, and verify the certificate. The output should look something like this if everything worked correctly. “pki/issued/server.crt: OK”

sudo openssl verify -CAfile pki/ca.crt pki/issued/server.crt

We have one final key to generate.

sudo ./easyrsa gen-dh

Now that we have generated all the server keys, the final step is to copy then all to the server folder in the openvpn directory.

sudo cp pki/ca.crt pki/dh.pem pki/issued/server.crt pki/private/server.key /etc/openvpn/server/

Adding a Client

Before we can go out and enjoy our hipster coffee shop coding, we need to create the client certificates.

If you have many clients to add, you will definitely want to automate this step. My VPN is for personal use, but I have automated a small part of the problem, which we will see in the next section.

We need to generate a certificate for each client we add. Here we will discuss adding just one client, but keep the larger picture in mind. I will name my client, client1. Be careful copying the code. You will need to change this string if you are using a different client name. It’s easy to get confused especially when client and server are keywords in some of these commands.

cd /etc/openvpn/easy-rsa
sudo ./easyrsa gen-req client1 nopass

And now sign the certificate we just created

sudo ./easyrsa sign-req client client1

As we did with the server, lets get some assurance that our certificate is not a brick. We will use openssl to verify it.

sudo openssl verify -CAfile pki/ca.crt pki/issued/client1.crt

Finally, just as we did with the server keys, we need to copy the client keys into the openvpn client folder.

sudo cp pki/issued/client1.crt pki/private/client1.key /etc/openvpn/client/

As this is our first client, we also need to copy the certificate authoritie’s crt into this folder. You do not need to repeat this if you add another client.

sudo cp pki/ca.crt /etc/openvpn/client/

Client Configuration

We’ve created and signed a certificate for our client, great! But the client doesn’t know that…

The client needs quite a lot of information, so we will make a configuration file. In my case it will be called client1.ovpn. The name doesn’t have to match the name of your client, but for the sake of sanity it probably should. Although you can handle the key files separately, we are going to bundle them all into this file.

This is where I’ve done you a favour. I’ve written a python3 script to create the client file. You may need to modify it if you have deviated from this guide. Here is the script, save it as makeuserconfig.py.

#!/bin/python3

"""
This script can only be used after the client keys have been generated
The <client_name>.crt and <client_name>.key must have been moved to openvpn/client folder
It will generate an ovpn file containing all the information and keys needed for the client to connect
"""

import sys
import os

if len(sys.argv) < 3:
    print ("Errror: Not enough args. EG, ./makeuserconfig.py <client-name> <server-address>")
    quit() 

client_name = sys.argv[1]
server_address = sys.argv[2]
openvpn_path = "/etc/openvpn"
easyrsa_path = "/etc/openvpn/easyrsa"

if os.path.exists("./" + client_name):
    print ("Error: Client already exists, directory must not contain folder with client name")
    quit() 

print ("Building ovpn for client: {}".format(client_name))

# Read certificate authority crt and add it to the template
openvpn_client_path = openvpn_path + "/client"
with open(openvpn_client_path + "/ca.crt") as ca_file:
    ca_contents = ca_file.read()
    ca_file.close()

# Read client certificate and add it to the template
with open (openvpn_client_path + "/" + client_name + ".crt") as client_cert_file:
    cert_contents = client_cert_file.read()
    client_cert_file.close()

# Read client key and add it to the template
with open (openvpn_client_path + "/" + client_name + ".key") as client_key_file:
    key_contents = client_key_file.read()
    client_key_file.close()

# Read tls key and add it to the template
with open (openvpn_path + "/ta.key") as tls_key_file:
    tls_key_contents = tls_key_file.read()
    tls_key_file.close()

client_file_contents = """
client
dev tun
proto udp4
remote {} 1194
resolv-retry infinite
nobind
user nobody
group nogroup
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-CBC
auth SHA512
verb 3
key-direction 1
<ca>
{}
</ca>
<cert>
{}
</cert>
<key>
{}
</key>
<tls-auth>
{}
</tls-auth>
""".format(server_address, ca_contents, cert_contents, key_contents, tls_key_contents)

# Write the output file
os.mkdir(client_name)
client_file = open(client_name + "/" + client_name + ".ovpn", "a")
client_file.write(client_file_contents)
client_file.close()

This script makes a few assumptions and does little error checking. You can run it the following way, filling in the client and server fields. For me, client-name is client1, and server-address is none of your business :) But seriously, just the static ip address of the server is fine, do not include the port. I suggest you modify the script if you want to change the configuration more substantially.

sudo ./makeuserconfig.py <client-name> <server-address>

If the script worked correctly, you should now have a valid client configuration file. The final step is to get this file onto the client, and start the server.

Copying the Configuration to the Client

We need to be careful here. Our configuration contains multiple keys and certificates that must be kept secret. It is highly recommend to use a secure method of transfering the files. Unfortunately, it is beyond my expertise to suggest how this should be done in the business case with complex key management solutions. I will cover how I did it for personal use.

While setting up my server, my desktop (client) was connected to the server via ssh, as it is the machine I used to create the Amazon ec2 instance. Getting my desktops configuration from the server was easy. Using the secure copy utility, the command is almost identical to the initial ssh command.

scp -i ~/.ssh/mykey.pem ubuntu@157.124.12.5:~/client1.ovpn .

Replace “ubuntu” with your user name on the server, and replace the IP address with your servers address. This also assumes you ran my python file from the home directory on your server. Change the path to the location of the ovpn file if you created it elsewhere.

Connecting to the Server

Now that we have the configuration file on the client, we need to install an Open VPN client. I did this on linux and mac using apt and brew respectively. Once you have a client installed, you should be able to connect using the following command from the cli.

openvpn client1.ovnp

This is just the command I would use for testing. Most operating systems have a way to define a new connection, and in the case of a vpn, provide the ovpn file and they will do the rest.

If you are running Linux, you can use the nmcli command to create the new connection. You can also optionally run it with a second command, though I prefer to use the gui for that once the first command has added it there.

    sudo nmcli con import type openvpn file client1.ovpn
    sudo nmcli con up client1

And there we have it. A few involved steps that admittedly took me most of an afternoon the first time but you are up and running.

If this optimism is misplaced, have a quick browse of the final section, as it didn’t work for me the first time either.

Troubleshooting

This is a very short section as most of my problems were stupidity related on my part.

  • TLS Handshake Failed - Oh boy I had fun with this one. My extensive googling lists many possible causes. I had two issues. I accidentally re-generated the TLS key and forgot to update the clients ovpn. I also signed some keys with the wrong DA name which got me another flavour of this error. There were also issues with the configs I was using, but they should be solved since I wrote the Python script for you.
  • No Internet - You can connect to your VPN, but you lose all internet access the moment you do. This is often a NAT problem. Double check the IP Forwarding and Firewall sections of this post. In my case I’d commented some of that configuration while debugging and forgot to re add it.