OpenVPN - zbrewer/homelab GitHub Wiki

OpenVPN is generally more challenging to setup than other VPN solutions and requires a lot of manual key management. It is also not as performant making it less than ideal for use as a general VPN for things like encrypting traffic on an untrusted network, getting around location-based restrictions, and accessing most internal resources remotely. That being said, there is one large advantage to OpenVPN that justifies the setup cost when used as a secondary or tertiary VPN: L2 bridging. Most VPNs operate at Layer 3 which only works for IP-based protocols. Anything that requires Layer 2 access (such as for device discovery) will not work on other VPNs while a L2 bridge/ethernet bridge/TAP connection with OpenVPN can provide this functionality.

My specific use case involves accessing my Tablo (OTA TV streaming device) remotely; however, other L2 use cases should work too.

Note: some client devices, such as Android phones and Chromebooks, can't connect to a TAP device and therefore can't take advantage of this setup. That being said, Linux and Windows laptops can which still provides sufficient functionality.

Prerequisites

OS Setup

This guide assumes installation on a fresh Ubuntu Server (specifically 22.04) install. In my case, I used a VM running on Proxmox as the OpenVPN server itself and a CT on Proxmox as the machine hosting the certificate authority (more about that below). A vanilla install of Ubuntu on both works as a starting point. They should also be configured to static IP addresses on a VPN VLAN/Subnet.

With the basic install completed, non-root users should be added to both for managing their respective services. The usernames can generally be whatever you want although openvpn works well for the OpenVPN server. These users can be given sudo access as well. The commands for this are:

# adduser <username>
# usermod -aG sudo <username>

The Ubuntu Firewall should also be enabled to provide some basic hardening of the machines. If SSH access is important that should be allowed through the firewall as well. The steps to see available applications, allow SSH, enable the firewall, and finally see its status are:

$ sudo ufw app list
$ sudo ufw allow OpenSSH
$ sudo ufw enable
$ sudo ufw status

A more detailed guide (that I used when setting this up initially) can be found here.

Certificate Authority

As alluded to earlier, a certificate authority is needed to serve as the root source of trust. While it is possible to use the same machine as the OpenVPN server for this, there are some security implications to doing that. Instead, a separate machine (or container in my case) should be used that can be powered-off when not actively signing keys.

The guide here describes the process of setting up a Certificate Authority in more detail but in general the steps are:

Install packages and setup directories (as a non-root user):

$ sudo apt-get update
$ sudo apt-get install easy-rsa
$ mkdir ~/easy-rsa
$ ln -s /usr/share/easy-rsa/* ~/easy-rsa/
$ chmod 700 ~/easy-rsa
$ cd ~/easy-rsa
$ ./easyrsa init-pki

Set the vars needed for the certificate authority by creating the ~/easy-rsa/vars file with the following contents (edited as necessary):

set_var EASYRSA_REQ_COUNTRY    "US"
set_var EASYRSA_REQ_PROVINCE   "Colorado"
set_var EASYRSA_REQ_CITY       "Denver"
set_var EASYRSA_REQ_ORG        "Brewer"
set_var EASYRSA_REQ_EMAIL      "[email protected]"
set_var EASYRSA_REQ_OU         "Family"
set_var EASYRSA_ALGO           "ec"
set_var EASYRSA_DIGEST         "sha512"

The following command can then be run from the easy-rsa directory to setup the CA. This will also ask for a password that will be used when signing certificates in the future.

$ ./easyrsa build-ca

The public and private keys can now be found at ~/easy-rsa/pki/ca.crt and ~/easy-rsa/pki/private/ca.key respectively.

DDNS Server

In order to connect to the VPN remotely, the client config will eventually need to include the public IP of the OpenVPN server. While this IP could be used directly if it is a static IP or if it practically doesn't change, a DDNS setup will allow a (sub) domain to point to the public IP so that it doesn't have to be updated in the config whenever it changes and changes while you are away from the VPN server (and therefore don't know the new IP) don't break the configuration.

A DDNS server can be set up in a variety of ways including using the OPNsense plugin.

Installation

Server Installation

I based this setup on the guide here as well as the OpenVPN instructions here for ethernet bridging.

The start is similar to the setup for the CA by installing packages and creating the key infrastructure:

$ sudo apt-get update
$ sudo apt-get install openvpn easy-rsa bridge-utils net-tools
$ mkdir ~/easy-rsa
$ ln -s /usr/share/easy-rsa/* ~/easy-rsa/
$ sudo chown <username> ~/easy-rsa
$ chmod 700 ~/easy-rsa

Note: bridge-utils are needed to set up the ethernet bridge and net-tools are needed to install ifconfig which is used by the script that creates the bridge interface used by OpenVPN.

Now the vars must be set up for the OpenVPN server. These are still created at ~/easy-rsa/vars but this time only include the following two lines:

set_var EASYRSA_ALGO "ec"
set_var EASYRSA_DIGEST "sha512"

From here, the key infrastructure can be created and a signing request can be made for the server's public key:

$ cd easy-rsa
$ ./easyrsa init-pki
$ ./easyrsa gen-req <server> nopass
$ sudo cp ~/easy-rsa/pki/private/server.key /etc/openvpn/server/

Note that <server> is a placeholder for the common name of the server. Anything (such as the hostname) can be used although there are places later where this must be substituted as well.

After this, the signing request must be copied to the CA machine so that the key can be signed and copied back (along with the CA's public key in ca.crt). From the CA machine:

$ scp <username>@<openvpn_ip>:/home/<username>/easy-rsa/pki/reqs/<server>.req /tmp
$ cd ~/easy-rsa
$ ./easyrsa import-req /tmp/<server>.req <server>
$ ./easyrsa sign-req server <server>
$ scp ~/easy-rsa/pki/issued/<server>.crt <username>@<openvpn_ip>:/tmp
$ scp ~/easy-rsa/pki/ca.crt <username>@<openvpn_ip>:/tmp

With this done, return to the OpenVPN server and copy the ca.crt and <server>.crt files to the appropriate location:

$ sudo cp /tmp/ca.crt /etc/openvpn/server/
$ sudo cp /tmp/<server>.crt /etc/openvpn/server/

A tls-crypt pre-shared key will also be created and copied into this directory:

$ cd ~/easy-rsa
$ openvpn --genkey --secret ta.key
$ sudo cp ta.key /etc/openvpn/server/

After that, keys for the first client can be created. From the OpenVPN server:

$ mkdir -p ~/client-configs/keys
$ chmod -R 700 ~/client-configs
$ cd ~/easy-rsa
$ ./easyrsa gen-req <client_name> nopass
$ cp pki/private/<client_name>.key ~/client-configs/keys/

And once again from the CA machine:

$ scp <username>@<openvpn_ip>:/home/<username>/easy-rsa/pki/reqs/<client_name>.req /tmp
$ cd ~/easy-rsa
$ ./easyrsa import-req /tmp/<client_name>.req <client_name>
$ ./easyrsa sign-req client <client_name>
$ scp ~/easy-rsa/pki/issued/<client_name>.crt <username>@<openvpn_ip>:/tmp

Now, return to the OpenVPN server and copy all of the generated secrets needed by the client to the client-configs directory:

$ cp /tmp/<client_name>.crt ~/client-configs/keys/
$ cp ~/easy-rsa/ta.key ~/client-configs/keys/
$ sudo cp /etc/openvpn/server/ca.crt ~/client-configs/keys/
$ sudo chown <username>.<username> ~/client-configs/keys/*

Next, create the initial config file for the server by copying the template (replacing the name of the file with the friendly name of the server selected earlier):

$ sudo cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf /etc/openvpn/server/<server>.conf

Begin editing this file. First find the tls-auth line and comment out the existing configuration and replace it with a tls-crypt cirective:

;tls-auth ta.key 0 # This file is secret
tls-crypt /etc/openvpn/server/ta.key

Next, find the cipher line and change it to AES-256-GCM and add a new line below it that specifies auth SHA256.

;cipher AES-256-CBC
cipher AES-256-GCM
auth SHA256

After that, comment out the diffie-hellman seed file (dh directive, filename may be different) and add dh none:

;dh dh2048.pem
dh none

Uncomment the following lines to reduce privileges once the server is running. Note that the group must be changed from the default nobody to nogroup on Ubuntu.

user nobody
group nogroup

The DNS servers must also be configured by uncommenting the redirect-gateway line and setting a dhcp-option DNS directive with the appropriate DNS server(s):

push "redirect-gateway def1 bypass-dhcp"

push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS 8.8.4.4"

The cert, key, and ca must also be updated to use the absolute path of their location:

ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/<server>.crt
key /etc/openvpn/server/<server>.key

Finally, in order to set up ethernet bridging, dev tun must be replaced with dev tap0 and the server directive must be replaced with server-bridge. The server-bridge directive specifies the IP address of the server, the netmask, and then the first/last IP address of the range of addresses to assign to connected clients. This will look like the following:

;dev tun
dev tap0

;server 10.8.0.0 255.255.255.0

server-bridge 10.0.200.12 255.255.255.0 10.0.200.150 10.0.200.199

In addition, the start and stop scripts for the bridge device should be copied from /usr/share/doc/openvpn/examples/sample-scripts:

$ sudo cp /usr/share/doc/openvpn/examples/sample-scripts/bridge-start /etc/openvpn/server/
$ sudo cp /usr/share/doc/openvpn/examples/sample-scripts/bridge-stop /etc/openvpn/server/

The stop script should work as intended but the start script needs to add the following lines to re-assign the default route after creating the new interfaces (with the IP address replaced as appropriate):

eth_gateway="10.0.200.1"
route add default gw $eth_gateway

DNS also needs to be preserved by creating a directory/file at /etc/systemd/resolved.conf.d/dns_servers.conf with the following contents (replacing the DNS server IPs as appropriate):

[Resolve]
DNS=8.8.8.8 1.1.1.1

Then run $ sudo systemctl restart systemd-resolved to re-load this file.

Next, run the script to create the new interfaces and some additional commands to allow traffic over them:

$ sudo /etc/openvpn/server/bridge-start
$ iptables -A INPUT -i tap0 -j ACCEPT
$ iptables -A INPUT -i br0 -j ACCEPT
$ iptables -A FORWARD -i br0 -j ACCEPT

At this point, $ ip addr should show the new interfaces, pinging a remote address should work, and DNS lookups (using nslookup) should resolve correctly. Run $ sudo /etc/openvpn/server/bridge-stop to remove the extra interfaces and check that it was successful with $ ip addr.

Ensure that these scripts run automatically by editing /lib/systemd/system/[email protected] and adding the following lines to the bottom of the [Service] block:

ExecStartPre=/etc/openvpn/server/bridge-start
ExecStopPost=/etc/openvpn/server/bridge-stop

The server networking configuration must be edited a little more at this point to allow IP forwarding: Edit /etc/sysctl.conf and uncomment the net.ipv4.ip_forward = 1 line. Then run $ sudo sysctl -p to reload the file.

Edit the /etc/ufw/before.rules file to contain the following lines at the very top (with br0 and the IP range replaced as appropriate):

# START OPENVPN RULES
# NAT table rules
*nat
:POSTROUTING ACCEPT [0:0]
# Allow traffic from OpenVPN client to br0
-A POSTROUTING -s 10.0.200.0/24 -o br0 -j MASQUERADE
COMMIT
# END OPENVPN RULES

The /etc/default/ufw needs to be edited next to change the DEFAULT_FORWARD_POLICY from DROP to ACCEPT:

DEFAULT_FORWARD_POLICY="ACCEPT"

Finally, allow connections to the OpenVPN server through the appropriate port/protocol and restart the Ubuntu Firewall:

$ sudo ufw allow 1194/udp
$ sudo ufw disable
$ sudo ufw enable

Testing/Enabling

Local testing can be done by running OpenVPN directly from the command line:

$ openvpn /etc/openvpn/server/<server>.conf

Once this doesn't show errors, the service can be registered and then started with:

$ sudo systemctl -f enable openvpn-server@<server>.service
$ sudo systemctl start openvpn-server@<server>.service

Its status can then be checked to ensure that it started correctly:

$ sudo systemctl status openvpn-server@<server>.service

Firewall Setup

The main network firewall (not the machine's firewall) must also be configured to allow a port forward on the appropriate port (typically 1194/UDP) to the OpenVPN server.

Firewall rules for the VPN VLAN must also be configured as desired. In my case, to allow access to the Tablo, the same Tablo rule described here must be added and a mDNS repeater must be enabled across the VPN and IoT VLANs (or whichever the Tablo and VPN use) as described in the same section.

Client Configuration Infrastructure

A script can be be used to automate some of the process of generating new client configs. Start by creating a directory for the configs and copying the sample config over:

$ mkdir -p ~/client-configs/files
$ cp /usr/share/doc/openvpn/examples/sample-config-files/client.conf ~/client-configs/base.conf

Edit the base.conf file to match the server's configuration. Name, change dev tun to dev tap, set the remote to either the server's public IP or domain (as appropriate), set the remote port and proto as appropriate, uncomment the user and group directives in the same way as the server config (changing the group to nogroup), comment out the ca/cert/key/tls-auth directives (they will be replaced below), set the cipher to AES-256-GCM to match the server, and set auth SHA256.

It is also very important to add the line key-direction 1 somewhere in the config file.

Then, at the bottom of the file, add a few commented out lines for the DNS configuration. The appropriate lines will be commented out based on the client's configuration once the client config has been created:

# resolvconf
; script-security 2
; up /etc/openvpn/update-resolv-conf
; down /etc/openvpn/update-resolv-conf

# systemd-resolved
; script-security 2
; up /etc/openvpn/update-resolv-conf
; down /etc/openvpn/update-resolv-conf

At this point, the base client config should have the following options set (along with some defaults):

dev tap

remote <server_ip> 1194
proto udp

user nobody
group nogroup

;ca ca.crt
;cert client.crt
;key client.key
;tls-auth ta.key 1

cipher AES-256-GCM
auth SHA256

key-direction 1

# resolvconf
; script-security 2
; up /etc/openvpn/update-resolv-conf
; down /etc/openvpn/update-resolv-conf

# systemd-resolved
; script-security 2
; up /etc/openvpn/update-resolv-conf
; down /etc/openvpn/update-resolv-conf

Now create the script to generate the client config at ~/client-configs/make_config.sh. Edit it to contain the following:

#!/bin/bash
 
# First argument: Client identifier
 
KEY_DIR=~/client-configs/keys
OUTPUT_DIR=~/client-configs/files
BASE_CONFIG=~/client-configs/base.conf
 
cat ${BASE_CONFIG} \
    <(echo -e '<ca>') \
    ${KEY_DIR}/ca.crt \
    <(echo -e '</ca>\n<cert>') \
    ${KEY_DIR}/${1}.crt \
    <(echo -e '</cert>\n<key>') \
    ${KEY_DIR}/${1}.key \
    <(echo -e '</key>\n<tls-crypt>') \
    ${KEY_DIR}/ta.key \
    <(echo -e '</tls-crypt>') \
    > ${OUTPUT_DIR}/${1}.ovpn

Make the script executable and then generate the first config with the following commands:

$ chmod 700 ~/client-configs/make_config.sh
$ ~/client-configs/make_config.sh <client_name>

This will create a <client_name>.ovpn file in ~/client-configs/files that should be copied to the client itself.

Adding a Client

First generate the keys.

From the OpenVPN server:

$ cd ~/easy-rsa
$ ./easyrsa gen-req <client_name> nopass
$ cp pki/private/<client_name>.key ~/client-configs/keys/

From the CA machine:

$ scp <username>@<openvpn_ip>:/home/<username>/easy-rsa/pki/reqs/<client_name>.req /tmp
$ cd ~/easy-rsa
$ ./easyrsa import-req /tmp/<client_name>.req <client_name>
$ ./easyrsa sign-req client <client_name>
$ scp ~/easy-rsa/pki/issued/<client_name>.crt <username>@<openvpn_ip>:/tmp

Now, return to the OpenVPN server and copy the signed key to the client-configs directory:

$ cp /tmp/<client_name>.crt ~/client-configs/keys/
$ sudo chown <username>.<username> ~/client-configs/keys/*

The config can then be created by running the script:

$ ~/client-configs/make_config.sh <client_name>

Setting up Clients

Linux (Ubuntu)

Install the openvpn package and then check to see if systemd-resolved is being used by checking the contents of /etc/resolv.conf. If it contains something to the effect of # This file is managed by man:systemd-resolved(8). Do not edit. and the nameserver is specified as nameserver 127.0.0.53 then it is being managed with systemd-resolved.

In that case, install the openvpn-systemd-resolved package and uncomment the following lines from the .ovpn file:

script-security 2
up /etc/openvpn/update-systemd-resolved
down /etc/openvpn/update-systemd-resolved
down-pre
dhcp-option DOMAIN-ROUTE .

If not using systemd-resolved, uncomment the following lines instead:

script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf

A connection can now be established with the following command:

$ sudo openvpn --config <client>.ovpn
⚠️ **GitHub.com Fallback** ⚠️