Securing OpenVPN (2024)

About

OpenVPN is great and flexible but maybe a little too flexible. You can create any number of configurations (Sometimes not entirely valid!), even on the less sane side of things and it'll do its best to establish two or more running instances over a network.

For learning purposes (hopefully 'only'...) you can also create a configuration which connects the hosts without encryption which is great for packet capturing OpenVPN's traffic for learning about the protocol and ideally no valid production use case.

The problem I find with it isn't of its own but that of tutorials. There's a sea of openvpn tutorials out there with a bunch of default settings and other tunables without any hardening or annotation of what they're accomplishing with their settings. For users who might want to validate they're they're using the most secure settings Earth has to offer these often fall short.

So I figured I should add my own 'aged like milk' post to throw into the sea.

Getting started

OpenVPN will establish with just about any settings but to establish a connection we can consider secure there's some preparation required.

Public Key Infrastructure (PKI)

OpenVPN will need certificates for your client and server to validate each other and the planet uses Public Key Infrastructure and for the general web - the modest X.509 standard featuring public keys with digital signatures by an authority, identifying information and more to solve this.

I personally use a Hashicorp Vault cluster with an stupid amount of Access Control Lists managed by Saltstack to generate and maintain my home's Certificate Authority with an obscured root certificate (exported, encrypted offline, practically does not exist) to avoid potential threat actors gaining access to said certificate.

Outside the overkill of using a Vault cluster for infrastructure secret management at home its generated Certificate Authorities aren't special and can be exported for safety, generated elsewhere and imported and are the same as generated anywhere else (Save for the access lists).

So for the purposes of this post I'm going to use openssl to generate a CA for this OpenVPN server and client assuming readers also have access to that command.

Creating a Certificate Authority with openssl

OpenSSL is an all-in-one cryptography tool with many subcommands each of vast features. In general a certificate signing request must be made followed by the signing of said request with a CA.

In our case we'll create our CA by invoking the req subcommand to create and self-sign its own request in one go with RSA public key cryptography; valid for 10 years:

domain=home.internal # Set your own fancy domain here
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -keyout ${domain}.key -out ${domain}.crt

During the second command you will be prompted for a passphrase to encrypt the CA's private key with; followed by prompts for some information regarding who the organization the certificate belongs to.

Information regarding the certificate can also be appended to the command such as: -subj "/C=AU/ST=Canberra/O=Home/CN=${domain}"

Your CA is only as secure as the disks it sits on and the passphrase used to encrypt it for storage on said disks; among other policy-based gatekeeping such as Vault.

The passphrase can be skipped with -nodes but is unsuitable for anything other than testing. With this, you will be able to inspect the .key file too.

For example the certificate presented by reddit.com:443 with TLS has:

C=US,            (Country Name)
ST=California,   (State/Province Name)
L=SAN FRANCISCO, (Locality Name)
O=REDDIT, INC.,  (Organization Name)
CN=*.reddit.com  (Common Name) (in this case a wildcard certificate valid for all subdomains of the domain)

For the sake of this personal-focussed exercise and assumption that this may be used in a publicly accessible way I'd advise against filling in any personally identifying information for this simple throwaway OpenVPN client/server configuration.

Signing a certificate for your server to use

The OpenVPN server needs a certificate to perform a TLS handshake with clients.

We can sign one with our new CA; valid for server-use only for 5 years using the below command, including -subj to avoid prompts:

openssl req -nodes -x509 -newkey rsa:4096 -sha256 \
  -days 1825 -keyout server.${domain}.key -out server.${domain}.crt \
  -CA ${domain}.crt  -CAkey ${domain}.key \
  -addext "keyUsage = digitalSignature, keyEncipherment, dataEncipherment, cRLSign, keyCertSign" \
  -addext "extendedKeyUsage = serverAuth" \
  -subj "/C=AU/ST=Canberra/O=Home/CN=server.${domain}"

Without -subj the same questions will be asked again for certificate details. I would recommend setting the Common Name to something meaningful like server.home.internal.

This example also includes -nodes as the OpenVPN server will need access to its key autonomously.

Signing a certificate for your client to use

This will be more or less the same command as we used for the server but we'll sign this certificate with the clientAuth property and its own Common Name

openssl req -x509 -newkey rsa:4096 -sha256 \
  -days 1825 -keyout client.${domain}.key -out client.${domain}.crt \
  -CA ${domain}.crt  -CAkey ${domain}.key \
  -addext "keyUsage = digitalSignature, keyEncipherment, dataEncipherment, cRLSign, keyCertSign" \
  -addext "extendedKeyUsage = clientAuth" \
  -subj "/C=AU/ST=Canberra/O=Home/CN=client.${domain}"

The clientAuth and serverAuth extension keys prevent the certificates from being misused. In our case they prevent using the certificates in the reverse order and prevent somebody from trying to connect to the server using its own key for example (Which cryptographically checks out... but is only valid for server use, not clients).

This one doesn't include -nodes as its good safety to encrypt the client's private key for preventing access in the event the client device is physically compromises.

OpenVPN

We're at a point where we have a CA and both a server and client certificate for our openvpn server and client to use. We're almost ready to put together some openvpn configurations but what exactly should we secure ourselves with?

At the time of writing here we're up to TLS 1.3 which includes improvements over TLS1.2 for both the speed of the handshake process and privacy.

Regardless of the TLS version we're using - the handshake between the client and server involves an exchange of supported cipher specifications they're willing to secure the connection with. If they can agree on a common cipher suite they'll proceed.

But how do we make sure we're not picking some outdated weak cipher when safer standards are being made every year?

Ensuring we use the best ciphers and standards

As mentioned OpenVPN is powerful and flexible. So much so that it will allow you to use the worst dirt out there as long as it still supports them.

There are plenty of websites out there to help keep up to date with what set of cipher suites the world's using. I personally like to visit https://ssl-config.mozilla.org/ which can be used to generate strong configurations for many TLS-capable applications. https://ciphersuite.info/cs/ is also a reliable resource for what's considered insecure, weak, secure and recommended.

Upon visiting mozilla's ssl-config site it shows an nginx configuration file by default which is suitable for us to peak. Contained in the config further down the page is an ssl_protocols option set to accept TLS versions 1.2 and 1.3.

It also includes a nifty list of ciphers we can reference in our configuration: ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;

Creating a server config

According to openvpn --show-tls for version 2.6.9 it supports the three currently recommended Cipher Suites for TLS 1.3, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, and TLS_AES_128_GCM_SHA256.

For TLS 1.2's many secure and not-so-secure options we'll stick with TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384 and TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256

So we will use those below

For the sake of keeping things simple I'll assume the OpenVPN server will be issuing DHCP for its own clients for a Routed VPN configuration. (Not a L2 tap attached to an existing network's bridge). I've annotated as much as possible in this example server configuration:

mode server
tls-server
proto udp # Use UDP
port 1194 # Listen on 1194/udp
dev tun0  # Create tun0
float     # Allow the client's IP to change without disconnecting them (e.g. Mobile carrier Internet)

tls-version-min  1.2 or-highest # Use TLS 1.2 or greater
tls-version-max  1.3            # Use TLS 1.3 at most
remote-cert-tls  client         # Require the clientAuth extension in the client's certificate
tls-cert-profile preferred      # Require certs of SHA2 or better, RSA 2048 or better with any elliptic curve
tls-exit                        # Exit OpenVPN on TLS negotiation failure (More for clients)

verify-client-cert require      # Reject clients who don't send us a certificate

data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305               # Support these ciphers
tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256 # Use these two ciphersuites
ecdh-curve       secp384r1                           # Use the NIST P-521 elliptic curve for ECDH

  # Use these ciphers for TLS 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256 

# Define a network for OpenVPN to operate in.
topology subnet            # subnet mode assigns addresses with a mask rather than point-to-point topologies.
push "topology subnet"     # Push this setting to clients

ifconfig 10.99.0.1 255.255.255.224                  # A small subnet of 30 address (+1 broadcast)
ifconfig-pool 10.99.0.2 10.99.0.30 255.255.255.224  # Assign clients anywhere from 2-30.

# Have the server ping clients every 10s. After 120s assume they're gone and end the connection.
keepalive 10 120

ca        /etc/openvpn/server/home.internal.crt
cert      /etc/openvpn/server/server.home.internal.crt
dh        none # Not needed as we're using ECDHE
key       /etc/openvpn/server/server.home.internal.key
tls-crypt /etc/openvpn/server/tls-auth_shared.key       # openvpn --genkey tls-auth tls-auth_shared.key

# Push some client options to use the VPN
push "redirect-gateway def1"   # Sets a default gateway on the client through the VPN for all traffic.
push "route-gateway 10.99.0.1" # Helps to avoid 'UNSPEC' errors on Win11 OpenVPN Connect clients.
push "dhcp-option DNS 1.1.1.1" # If you have a local DNS server you can specify it here

push "dhcp-option DOMAIN-SEARCH home.internal"
push "keepalive 10 60" # Have the client ping the server every 10 seconds timing out after 60s.

# Push an optional route to the client for LAN resources.
push "route 192.168.0.1 255.255.255.0   10.99.0.1 1"

client-to-client # Allow clients to communicate with one another.

#crl-verify /etc/openvpn/server/home.internal.crl.crt # Optionally check certs against a revocation list

Creating a client config

The client will be using a configuration which is mostly the same with a few key differences:

  1. The client uses remote-cert-tls server to verify the remote is signed for serverAuth usage
  2. The client connects with remote x.x.x.x port proto
  3. The CAcert and client's cert and key will be embedded into the file for easy setup and usage on portable devices such as a mobile phone.
tls-client
dev tun
remote x.x.x.x 1194 udp

tls-version-min  1.2 or-highest # Use TLS 1.2 or greater
tls-version-max  1.3            # Use TLS 1.3 at most
remote-cert-tls  server         # Require the serverAuth extension in the server's certificate
tls-cert-profile preferred      # Require certs of SHA2 or better, RSA 2048 or better with any elliptic curve
tls-exit                        # Exit OpenVPN on TLS negotiation failure

verify-client-cert require      # Reject clients who don't send us a certificate

data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305               # Support these ciphers
tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256 # Use these two ciphersuites
ecdh-curve       secp384r1                           # Use the NIST P-521 elliptic curve for ECDH

  # Use these ciphers for TLS 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256 

persist-key # Keep keys in memory
persist-tun # Keep the tunnel interface if the connection drops
pull        # Pull options from the server

# Verify the server certificate has the fields we configured
verify-x509-name "C=AU, ST=Canberra, O=Home, CN=server.home.internal"

<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
[Paste your tls-auth_shared.key in this area]
-----END OpenVPN Static key V1-----
</tls-crypt>

<ca>
-----BEGIN CERTIFICATE-----
[Paste the CA certificate here: home.internal.crt]
-----END CERTIFICATE-----
</ca>

<cert>
-----BEGIN CERTIFICATE-----
[Paste the client's certificate here: client.home.internal.crt]
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN ENCRYPTED PRIVATE KEY-----
[Paste the CA certificate here: client.home.internal.key]
-----END ENCRYPTED PRIVATE KEY-----
</key>

It is critical to change the client's verify-x509-name to match your chosen field values.

The remote line near the top must also be filled in.

Finishing touches

Files

At this point we're ready to start the server. Copy home.internal.crt, server.crtandserver.key` into /etc/openvpn/server.

Also generate a TLS shared static key for the client and server to use: openvpn --genkey tls-auth /etc/openvpn/server/tls-auth_shared.key and add the contents of this new file to the client's configuration in the <tls-crypt> section.

Firewalls

The server will need an inbound firewall rule to allow OpenVPN to accept incoming connections. For systems running iptables this can be accomplished with:

iptables -I INPUT -p udp --dport 1194 -j ACCEPT

This rule can be further refined by defining the expected input interface such as -i Wanint1 before the protocol specifier.

If the server is running behind a firewall (Natted network) and the building firewall/router is Linux-based these iptables rule examples can redirect to the intended internal IP:

iptables -t nat -A PREROUTING -p udp --dport 1194 -j DNAT --to-destination vpn.server.ip `iptables -I FORWARD -d vpn.server.ip -p udp --dport 1194 -j ACCEPT

Launching

Start your distribution's relevant openvpn service. In my case this is systemctl enable --now openvpn-server@server.service on the server intended to run the VPN.

Send the client configuration to another device either on the same network or expecting to access the server via the Internet and install the profile into the OpenVPN client available for the platform and try connecting!

Industry standard networking gotchas

You may find everything connects perfectly and both the server and client can ping each-other, but networking doesn't work.

There are a few potential issues in this problem. The most popular for running the VPN on a non-router would be that IPv4 forwarding isn't enabled by default. It can be enabled with sudo sysctl -w net.ipv4.ip_forward=1 on the VPN server.

Past that the most popular solution to making the VPN client network world routable is to NAT the network out the VPN server. This causes traffic generated by VPN clients to take on the VPN server's IP and it translates it back as traffic returns. This can be done with sudo iptables -t nat -A POSTROUTING -o VPNServer_Primary_Network_Interface -j MASQUERADE

The alternative option is to make your network aware of this network. Again if OpenVPN is running on the building router who knows of all routes - this should already be fine. Otherwise adding a static route for the network with something like ip route add 10.99.0.0/27 via vpn.server.ip

If the VPN server happens to be the router then it is probably already doing all these things and has a routing table entry for the VPN client subnet. It would be worth checking for any conflicts with existing firewall rules which may be getting in the way

IPTables rules can be made persistent under /etc/iptables/iptables.rules in modern distributions. Sometimes /etc/sysconfig/iptables in older RHEL-based ones. There is also an accompanying iptables service which often needs to be enabled.

Room for improvement

For an individual this is a sound standalone configuration and additional client certificates (And replacements) can be issued with the same request signing command right from the shell history - complete with an encrypted CA key and client keys. But we can always do more.

Public Key Infrastructure

While openssl is evidently capable of pulling this setup off on its own this solution doesn't scale well for enterprise environments.

I can highly recommend trying to follow this guide using a Hashicorp Vault cluster as for a Certificate Authority and configuring all the extra parameters to restrict exactly what types of certificates can be issued with specific naming conventions per role. With Vault its also possible to create a Root CA and sign an Intermediate for issuing your certificates while the Root certificate remains obscured (Or even securely exported and deleted from Vault). Vault is nice in that it lets you run vault server -dev to instantly bring up a test server in memory, destroyed on exit. Combined with their documentation its a great platform for learning.

Wrapping up

Its easy to set up an OpenVPN server with a far less protective approach and newer editions even make some relatively safe assumptions when it comes to a cryptography selection. Though the defaults and lower limits on what an OpenVPN server is open to negotiating by default and while done in the name of backwards compatibility they aren't perfect and leave room for improvement.

I find keeping up with modern recommended ciphers makes it easy to bring up a VPN server confidently knowing exactly what its willing to negotiate and that those chosen are sane.