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.
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.
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.
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.
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 andnet-tools
are needed to installifconfig
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
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
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.
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.
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>
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