OpenVPN on FreeBSD for a secure self hosted virtual private network

Virtual Private Networks offer a way to take your office with you and FreeBSD has one of the best network stacks available. Whether you want to ensure your communication is encrypted, or need to access resources only available when you’re on your private network, VPNs allow you easily access your network as though you’re in your office from anywhere in the world. This article shows how to configure OpenVPN on FreeBSD (it’s similar on Linux) to provide a VPN service, and a Django application to easily generate and manage certificates as well as email a client OpenVPN configuration file to access your service.

FreeBSD VPN with Django and OpenVPN

Why VPN?

Virtual Private Networks enable users to access your own network, encrypted to prevent eavesdropping, as though you were there, from anywhere in the world. Network inspection would only see encrypted data.

There are two main categories, Site to Site and the subject of this article Remote Access.

Django – Making it easy

As managing certificates and distributing configuration files is meticulous and largely repetitive, it makes sense to automate the process. In addition to managing users and serving web pages, Django has the ability to create custom django-admin commands, and this is where Django significantly reduces the administrative overhead of managing x509 certificates.

Why a custom django-admin command and not a web app?

Although we're generally used to instant access, x509 security separates functions: request, signing and then distribution. The accompanying ca_tls.py management command module offers an easy way to manage a limited number of certificates, typically for individuals and SMEs who want the convenience of managing their own certificates but without the security overhead of putting that service online.

As security is an essential aspect of X.509 certificates, signing certificates should be carried out securely, and private keys must be protected.

The accompanying Django admin-command and templates can be easily configured to manage a delegated signing certificate. Just copy the ca_tls.py to your applications management/commands directory, add the various settings listed at the top of the module to your app/settings.py, then copy the template files to your app/templates/app/management/commands/ and you’re ready to test and go.

Feel free to copy and customise the templates to your templates directory.

Your Django app with this ca_tls.py module should look like the following:

manage.py
app/__init__.py
app/management/commands/__init__.py
app/management/commands/ca_tls.py
app/templates/app/management/commands/ca-tls-client.ovpn
app/templates/app/management/commands/ca-tls-ovpn-email.html
app/templates/app/management/commands/ca-tls-ovpn-email.txt
app/templates/app/management/commands/ca-tls-request.conf
app/settings.py

X.509 – Chain of trust – Certificate Authority

OpenVPN uses industry standard X.509 public key certificates for authentication, authorisation and encryption, this is the same technology that delivers Transport Layer Security aka https.

Managing your own Certificate Authority (CA) ensures control over authorising trust and revoking certificates when necessary. It’s essential to control your own CA as using a server certificate from another CA means any client certificates signed by that authority could gain access.

Although setting up and managing a certificate authority is straightforward, it’s outside the scope of this article, we will use and refer to the Advanced PKI from the OpenSSL PKI Tutorial.

Configuring OpenVPN will require:

  • Certificate Authority (CA);

  • TLS CA to sign certificates;

  • Server certificate with private key;

  • Client certificate with private key.

Create and operate Public Key Infrastructures with OpenSSL
https://pki-tutorial.readthedocs.io/en/latest/

Openssl Configuration

This article is based on a version of openssl included with FreeBSD, it will likely work on all the BSDs, Linux and macOS with minimal effort. Although there is a default openssl.cnf configuration with FreeBSD, this article uses the advanced configurations at https://pki-tutorial.readthedocs.io/en/latest/advanced/index.html. You will want to modify the configuration files for your organisation. Refer to man x509v3_config and the various ca, req, rsa, and x509 commands for further information.

Although you will probably be using the django app for the majority of your certificate management, it’s essential to ensure your ca is set up, initial folders created and TLS signing certificates are generated with your organisations information. You will want as a minimum to set your own base_url, countryName, organizationName, organizationalUnitName and commonName. As sha256 has superseded sha1 you will probably want to change the default digest default_md=sha256.

OpenVPN requires client and server certificates have appropriate Key Usage extensions and the TLS Server and TLS Client configurations have exactly what is required:

Mode Key usage Extended key usage
Client digitalSignature, keyAgreement TLS Web Client Authentication
Server digitalSignature, keyEncipherment, keyAgreement TLS Web Server Authentication

After creating a server or client certificate you can inspect it for X509v3 extensions.

openssl x509 -noout -text -in openvpn-server.crt

X509v3 extensions:
    X509v3 Basic Constraints:
        CA:FALSE
    X509v3 Subject Key Identifier:
        00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
    X509v3 Authority Key Identifier:
        keyid:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
    X509v3 Key Usage: critical
        Digital Signature, Key Encipherment
    X509v3 Extended Key Usage:
        TLS Web Server Authentication
    X509v3 Subject Alternative Name:
        DNS:example.com

openssl x509 -noout -text -in client.crt

X509v3 extensions:
    X509v3 Basic Constraints:
        CA:FALSE
    X509v3 Key Usage:
        Digital Signature, Non Repudiation
    X509v3 Extended Key Usage:
        TLS Web Client Authentication
    X509v3 Subject Key Identifier:
        00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
    X509v3 Subject Alternative Name:
        email:user@example.com

If you see the following error in your logs, it's because the certificate does not have the required key usage extension.

2021-04-13 11:10:40 TLS: Initial packet from [AF_INET]192.168.0.1:1194, sid=00000000 00000000
2021-04-13 11:10:40 Certificate does not have key usage extension
2021-04-13 11:10:40 VERIFY KU ERROR
2021-04-13 11:10:40 OpenSSL: error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed
2021-04-13 11:10:40 TLS_ERROR: BIO read tls_read_plaintext error
2021-04-13 11:10:40 TLS Error: TLS object -> incoming plaintext read error
2021-04-13 11:10:40 TLS Error: TLS handshake failed

Install OpenVPN

Using the default FreeBSD package manager:

pkg install security/openvpn

Configure OpenVPN

An OpenVPN server configuration is straightforward, this is what you need:

  • Network addresses

  • Server Certificate with private key

  • Diffie–Hellman key

Network addresses

You need to identify the internet facing ip address, this may be a public ip or if it's behind NAT, an rfc-1918 private address.

If your internet facing IP address is 192.168.0.0 and you wish to bind to that address then add the following to your configuration:

local 192.168.0.0

Otherwise leave it commented out to bind to all addresses

;local

As VPNs connect private networks, it's essential to use a unique subnet, if both sides of a network use 192.168.0.0/16 then 192.168.0.55 could be ambiguous so it's preferable to choose a small range in 10.0.0.0/8 for example 10.8.0.0/24 offers 254 private addresses. Choosing a small random ip address offers the best chance of avoiding collisions so please change 10.8.0.0.

server 10.8.0.0 255.255.255.0

https://en.wikipedia.org/wiki/Private_network

IPv6

The following options are available to handle IPv6 configuration, analogous to their IPv4 counterparts (--server <-> --server-ipv6, etc.)

  • server-ipv6

  • ifconfig-ipv6

  • ifconfig-ipv6-pool

  • ifconfig-ipv6-push

  • route-ipv6

  • iroute-ipv6

Choosing the IPv6 range to offer your VPN clients might come from either your own range or fd::/8

Using uuidgen on FreeBSD to generate a unique uuid, then using the first ten characters to construct your IPv6 Unique Local Address:

So using the first ten characters from 12345678-9a00-0000-0000-000000000000 would generate a unique local address fd12:3456:789a:1::1

server-ipv6 fd12:3456:789a:1::1/64

IPv6 addresses are designed to be global, but see the the following for more information:

https://en.wikipedia.org/wiki/Unique_local_address

https://en.wikipedia.org/wiki/IPv6_address#Local_addresses

Server Certificate

The opnvpn application needs access to your certificate and private key.

Using the ca_tls Django admin-command to generate and sign a server certificate

manage.py ca_crt --request vpn.example.com --server
manage.py ca_crt --sign vpn.example.com –server

You will be prompted for your signing certificate password. Then copy the files to your openvpn directory.

ca ca.crt

cert vpn.example.com.crt

key vpn.example.com.key # This file must be kept secret

Diffie–Hellman key exchange

Exchanging cryptographic keys securely over the internet is performed with a shared DH key

To generate a 2048 bit random dh parameter use openssl as follows:

openssl dhparam -out /usr/local/etc/openvpn/dh2048.pem 2048

dh dh2048.pem

https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange

TLS-AUTH setup security

Security is computationally intensive, so to identify denial of service attacks and reduce their impact OpenVPN uses tls-auth, a shared signature, that if not provided allow connections to be dropped with minimum effort.

Using OpenVPN to generate an OpenVPN static key:

openvpn --genkey secret /usr/local/etc/openvpn/openvpn-tls-auth.key

And in both the server and client config file:

tls-auth openvpn-tls-auth.key

Clients trying to connect without the correct tls-auth HMAC signature can be identified with the following error:

openvpn[pid] TLS Error: cannot locate HMAC in incoming packet from [AF_INET]<host address>

These errant client requests can be either ignored or used with your favourite intrusion detection software such as fail2ban to block them.

https://en.wikipedia.org/wiki/HMAC

Cipher

Keeping traffic between clients and the server secret uses a cryptographic cipher. The default, Advanced Encryption Standard (AES) is in the example server configuration at /usr/local/share/examples/openvpn/sample-config-files/server.conf. If you are using the example cipher AES-256-GCM you should also add it to data-ciphers.

cipher AES-256-GCM
data-ciphers AES-256-GCM

Advanced Encryption Standard
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

Routing

Routing is a complex area dependent on your network configuration so you are advised to refer to the openvpn man page.

If you wish all traffic to go through your tunnel, then add the following to your configuration. There are a number of flags that can be added to redirect-gateway and you are advised to check the man page for further information.

push "redirect-gateway”

Your server configuration file might look like the following:

proto udp
local 192.1.1.1
ca /path/to/your/ca.crt
cert /path/to/your/openvpn-server.crt
key  /path/to/your/openvpn-server.key
tls-auth  openvpn-tls-auth.key 0
remote-cert-eku "TLS Web Client Authentication"
cipher AES-256-CBC
data-ciphers AES-256-CBC
dh dh2048.pem
dev tun
topology subnet

server 10.8.0.0 255.255.255.0
ifconfig-pool-persist ipp.txt

status openvpn-status.log
keepalive 10 120

DNS

Many users of a VPN want secure access to internal resources not available to the general public. To achieve this, most organisations configure a local DNS server to offer local clients different IP addresses to the general public. This is sometimes known as split horizon.

Who are you, where do you want to go?

As VPN users will be given an IP address from one of those identified by the server directive, in our example it might be 10.8.0.5, the DNS server needs to know to offer them local private resources rather than public ones.

Bind version 9 introduced views, where different configurations can be offered to different hosts.

The following shows fragments of a bind configuration file, note that 10.8.0.0/24 was added to the acl allowing VPN clients identical access to local clients.

Options {
	// main options
	allow-query ( internal; };
};
acl “internal” {
	localhost;
	192.168.0.0/24;
	10.8.0.0/24;
};
view “internal” {
        match-clients { "internal"; };
        allow-query { "internal"; };
        recursion yes;
/*** Your zones here
zone "example.org" {
        type master;
        allow-update {
                key "exampleorgkey";
        };
        file "/usr/local/etc/namedb/dynamic/example.org";
};
***/
}; // End view internal

It should be remembered that security by obscurity isn’t a complete defence and it’s possible to configure a client to use an address not supplied by DNS.

Application Security

Many applications such as Apache web server and SSH offer security based on IP address.

For example Apache has Require in mod_authz_core and mod_authz_host that can be used as follows

<RequireAny>
	Require ip 127
	Require ip 192.168.0.0/24
	Require ip 10.8.0.0/24
</RequireAny>

And in SSH there are a number of directives, for example the Match LocalAddress=”” block.

Client configuration

Each client uses its own configuration file to access the vpn. Using the Django ca_tls management commands makes it easy creating and signing certificates and distributing OpenVPN configuration files using those certificates to end users.

The three stages are Request a certificate, Sign a certificate and Send a personal configuration file to an end user.

It’s your site policy to decide how long to validate certificates, any less than a few months can increase user frustration as they need to manually install a configuration distributed via their email, any longer than a year increases potential harm from key compromise and mis-issuance.

https://letsencrypt.org/2015/11/09/why-90-days.html

User requests

Each client/user accessing a VPN needs their own configuration containing a certificate signed by your certificate authority (ca).

The first stage is to generate a request. Using the ca_tls Django admin-command to generate and sign a client certificate:

manage.py ca_crt --request username

The ca_tls admin-command will fetch the user’s details from your app so it’s important to ensure names and email addresses are accurate.

Django applications can write custom admin commands and we will use the example below.

The initial request creates a unique request configuration for a user, this is then used to generate a certificate request.

Create a certificate

Certificates requests are generated first using information from Django users and the Django app config settings.

manage.py ca_crt --sign username

Then a certificate is signed. You will be prompted for your signing certificate password. If a previous certificate exists then it needs to be revoked and you will be prompted for your password again. Intermediate certificates are added enabling the full chain of trust to your ca.

Send client configuration

After a certificate is signed, all the information required for a connection can be collated and sent to an end user. Using the ca_tls Django admin-command to generate and send a personal configuration to a user:

manage.py ca_crt --send-ovpn username

Default templates exist for the OpenVPN configuration (ca-tls-client.ovpn) and the email (ca-tls-ovpn-email.txt and ca-tls-ovpn-email.html) sent to the user with instruction on how to install it.

These templates can be modified in the usual Django way.

Testing

Test your VPN tunnel is working by visiting https://default.p-o.co.uk/ where can check which ip address you are using.