26 Nov 2017

OpenVPN Setup Guide

Browse securely from anywhere using a personal VPN with OpenVPN, LDAP, FreeBSD, and PF.

A VPN allows you to securely extend a private network over the internet via tunneling protocols and traffic encryption. For most people, a VPN offers two primary features: (1) the ability to access services on your local network over the internet, and (2) secure internet connectivity over an untrusted network. In this guide, I'll describe how to set up a personal VPN using OpenVPN on FreeBSD. The configuration can use both SSL certificates and LDAP credentials for authentication. We'll also be using the PF firewall to NAT traffic from our VPN out to the internet.

One important note about running your own VPN: since you are most likely hosting your server using a VPS or hosting provider, with a public IP address allocated specifically to you, your VPN will not give you any extra anonymity on the internet. If anything, you'll be making yourself more of a target, since all your activity can be trivially traced back to your server's IP address. So while your VPN will protect you from a snooping hacker on the free WiFi at Starbucks, it won't protect you from a federal investigation.

This guide assumes you are running FreeBSD with the PF firewall. If you're using a different Unix flavor, I'll probably get you most of the way there—but you'll be on your own when configuring your firewall and networking.

Finally, I've used example.com and a non-routable public IP address for all the examples in this guide. You'll need to replace them with your own domain name and public IP address.

With all that out of the way, let's get started.

Configuring the OpenVPN Server

First, you'll need to install OpenVPN. You can use your distribution's package manager, but my examples assume you're using the FreeBSD ports tree. I recommend building with the EASYRSA option—you'll need it later.

cd /usr/ports/security/openvpn
make install clean

OpenVPN should ship with a default configuration file. Open it up in your editor to see all the available options. I've pasted my own configuration below, along with some commentary where appropriate. You'll almost certainly need to make some changes.

/usr/local/etc/openvpn/openvpn.conf
# port to listen on - 1194 is OpenVPN default port 1194 # OpenVPN works best over UDP, but has support for TCP as well. UDP is # recommended, since tunneling TCP over TCP has well-known performance # issues. proto udp # OpenVPN supports TUN and TAP devices for the virtual network: # TUN: uses layer 3, less overhead but cannot bridge with other interfaces # TAP: uses layer 2, more overhead (emulates ethernet frames) but can # bridge with other interfaces # The configuration I present here uses TUN. dev tun # DH params, CA, and key pair for your VPN server. Replace example.com with # your domain name. We will generate these using easy-rsa in the next step. dh /usr/local/etc/openvpn/keys/dh.pem ca /usr/local/etc/openvpn/keys/ca.crt cert /usr/local/etc/openvpn/keys/vpn.example.com.crt # change me! key /usr/local/etc/openvpn/keys/vpn.example.com.key # change me! # Modern cipher recommendations from Mozilla. Comment these out if you need # to support old/legacy clients. tls-version-min 1.2 tls-cipher ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384 # No need to set block cipher manually anymore - OpenVPN 2.4+ automatically # negotiates AES-256-GCM. Uncomment if you use older clients that default # to insecure ciphers. # cipher AES-256-CBC # VPN network - server accessible @ 10.8.0.1 topology subnet server 10.8.0.0 255.255.255.0 # cache client IP addresses in a file for later re-use ifconfig-pool-persist ipp.txt # This line will force clients to route ALL their internet traffic through # the VPN. If you only want to use your VPN to access internal services, # comment this out. push "redirect-gateway def1 bypass-dhcp" # These lines tell clients which DNS servers to use once they are connected # to the VPN. If you run a local DNS server, you can have clients use the # server's own address (10.8.0.1). If you don't run a local DNS server, you # can substitute your server's configured nameserver here (check # /etc/resolv.conf). push "dhcp-option DNS 10.8.0.1" push "dhcp-option DOMAIN example.com" # change me! # let clients see each other client-to-client # ping every 10s, timeout after 120s keepalive 10 120 # max simultaneous clients max-clients 20 # drop privileges user nobody group nobody persist-key persist-tun # log levels 0-9 verb 3 # silence repeated messages mute 10 # notify clients when the server restarts so they can automatically # reconnect explicit-exit-notify 1 # experimental TUN UDP I/O optimizations fast-io # Uncomment the following line if you want to use LDAP authentication in # addition to SSL certificates. # plugin /usr/local/lib/openvpn-auth-ldap.so "/usr/local/etc/openvpn/auth-ldap.conf"

LDAP Authentication

For added security, you can have clients supply their LDAP credentials in addition to their provisioned SSL certificates. You'll need to install a plugin first:

cd /usr/ports/security/openvpn-auth-ldap
make install clean

Then, just supply your LDAP connectivity details in a separate configuration file. Make sure you include each option below—I spent hours troubleshooting this part because I had neglected the Timeout parameter.

/usr/local/etc/openvpn/auth-ldap.conf
<LDAP> URL ldap://localhost TLSEnable no FollowReferrals no Timeout 15 </LDAP> <Authorization> BaseDN "ou=users,dc=example,dc=com" SearchFilter "(&(uid=%u))" RequireGroup false </Authorization>

Notes on Using Your Own DNS Server

If you're running your own DNS server for the VPN, make sure it's listening for DNS queries on the VPN's interface. If you followed my DNS Hosting Guide, you'll just need to add the VPN's network address to BIND's configuration file:

/usr/local/etc/namedb/named.conf
// localhost, and any other public IPs of your server acl localnetworks { 127.0.0.1; ::1; 10.8.0.0/24; 203.0.113.41; 203.0.113.42; 2001:db8::2; 2001:db8::3; };

Generating the Server Certificates

Finally, you need to generate a Certificate Authority and an SSL key pair. You'll distribute the CA to your clients so they can verify the authenticity of your VPN server. These files can be generated using the Easy-RSA package. Install it now if you didn't specify it in OpenVPN's build options earlier.

cd /usr/ports/security/easy-rsa
make install clean

Copy the whole Easy-RSA package into OpenVPN's configuration directory.

cp -r /usr/local/share/easy-rsa /usr/local/etc/openvpn/easy-rsa
cd /usr/local/etc/openvpn/easy-rsa

EDIT (29 Nov 2017): In a production setup, it's never a good idea to do certificate management on a public-facing server. Once you've verified everything works, you should move your Easy-RSA directory to a secure location away from the prying eyes of the internet. (Thanks to @DavidSommerseth for reminding me to point this out.)

Easy-RSA is configured by setting options in the vars configuration file:

/usr/local/etc/openvpn/easy-rsa/vars
set_var EASYRSA_DN "cn_only" # Elliptic curve cryptography is faster and arguably more secure than RSA. # I use EC in my setup, but stick to RSA if you need to support older # clients. set_var EASYRSA_ALGO ec set_var EASYRSA_CURVE secp384r1 # generated certificates will expire after 10 years. set_var EASYRSA_CA_EXPIRE 3650 set_var EASYRSA_CERT_EXPIRE 3650

Now you're ready to generate your certificate infrastructure:

./easyrsa.real init-pki

./easyrsa.real build-ca
# You will be prompted for a password when generating the CA. Keep it
# somewhere safe, since you will need it when generating client certs.
# You can put whatever you like for the Common Name. I use
# "vpn.example.com CA". 

# relace "vpn.example.com" with your server's FQDN. 
./easyrsa.real build-server-full vpn.example.com nopass # change me!
# enter password from previous step when prompted.

./easyrsa.real gen-dh

Copy the generated files into a separate directory, as we specified in openvpn.conf:

mkdir /usr/local/etc/openvpn/keys
cd /usr/local/etc/openvpn/keys
cp ../easy-rsa/pki/ca.crt .
cp ../easy-rsa/pki/dh.pem .
cp ../easy-rsa/pki/issued/vpn.example.com.crt  . # change me!
cp ../easy-rsa/pki/private/vpn.example.com.key . # change me!

Enable OpenVPN to start on boot:

/etc/rc.conf
openvpn_enable="YES"

Finally, start OpenVPN!

service openvpn start

You can check for any startup issues in /var/log/messages.

Routing VPN Traffic to the Internet

OpenVPN will create a secure virtual network for your VPN clients, but it's up to your firewall to route traffic from your VPN interface out to the internet using Network Address Translation (NAT). If you followed the PF section of my FreeBSD Server Guide, you just need to make a few small tweaks to pf.conf. I've highlighted the important changes in bold.

/etc/pf.conf
# the external network interface to the internet # public-facing interface ext_if="vtnet0" # your public-facing IP address - VPN traffic will NAT out of this address ext_ip="203.0.113.42" # change me! # vpn interface vpn_if="tun0" vpn_net = "10.8.0.0/24" # port on which sshd is running ssh_port = "55022" # allowed inbound ports (services hosted by this machine) inbound_tcp_services = "{auth, http, https, " $ssh_port ", openvpn }" inbound_udp_services = "{dhcpv6-client, openvpn}" # politely send TCP RST for blocked packets. The alternative is # "set block-policy drop", which will cause clients to wait for a timeout # before giving up. set block-policy return # log only on the external interface set loginterface $ext_if # skip all filtering on localhost set skip on lo # reassemble all fragmented packets before filtering them scrub in on $ext_if all fragment reassemble # route traffic from VPN interface out to the internet nat on ! $vpn_if from $vpn_net to any -> $ext_ip # block forged client IPs (such as private addresses from WAN interface) antispoof for $ext_if # default behavior: block all traffic block all # all traffic through VPN interface is assumed to be safe pass quick on $vpn_if # allow all icmp traffic (like ping) pass quick on $ext_if proto icmp all pass quick on $ext_if proto icmp6 all # allow incoming traffic to services hosted by this machine pass in quick on $ext_if proto tcp to port $inbound_tcp_services pass in quick on $ext_if proto udp to port $inbound_udp_services # allow all outgoing traffic pass out quick on $ext_if

Reload PF for these changes to take effect (you may want to do this from a serial console in case you messed something up):

pfctl -f /etc/pf.conf

Tell the kernel to enable packet forwarding in order to support NAT:

sysctl net.inet.ip.forwarding=1

One last thing: since your server will now be forwarding packets, you must disable hardware offloading on your network card to avoid nasty issues:

# replace vtnet0 with your external network interface
ifconfig vtnet0 -tso -lro

To make these changes permanent, make the following changes in /etc/rc.conf:

/etc/rc.conf
# disable TSO and LRO on your primary NIC when routing packets ifconfig_vtnet0="inet 203.0.113.42 netmask 255.255.255.0 -tso -lro" # enable packet forwarding gateway_enable="YES"

Provisioning Your VPN Clients

For clients to connect to your VPN server, they will need a client configuration file (usually with an .ovpn extension) and a certificate key pair signed by your VPN's CA. You'll have to generate these files on your server and provide them to your clients out of band. I usually just put the files in a tarball and email them, but you might have more stringent security requirements. Proceed accordingly.

The client configuration file will be the same for all clients, so let's start with that first. Create a file called client.conf in OpenVPN's configuration directory. Most of the options in this file will mimic your configuration in openvpn.conf.

/usr/local/etc/openvpn/client.conf
client dev tun proto udp # FQDN of your VPN server remote vpn.example.com 1194 # change me! # let clients use any random port on their side nobind ca ca.crt cert client.crt key client.key # No need to set block cipher manually anymore - OpenVPN 2.4+ automatically # negotiates AES-256-GCM. Uncomment if you use older clients that default # to insecure ciphers. # cipher AES-256-CBC # keep trying to resolve remote hostname indefinitely resolv-retry infinite # reuse resources between invocations persist-key persist-tun # WLANs produce a lot of duplicate packets - mute this warning mute-replay-warnings # verify remote certificate remote-cert-tls server # log level 0-9 verb 3 # silence repeated messages mute 10 # uncomment the following line if you're using LDAP authentication # auth-user-pass

Now you need to create a client certificate. You'll need a different certificate for each device you want to connect to the VPN, because a single certificate cannot be used for multiple simultaneous connections. In this example, we'll create a certificate called macbook for my $2000 ssh machine.

cd /usr/local/etc/openvpn/easy-rsa

# Generate a client keypair for "macbook" - you will need to provide the CA
# password you specified last time.
./easyrsa.real build-client-full macbook nopass

Note the nopass option used here—since I'm additionally using password authentication via LDAP, I don't see a need to encrypt the certificate with a password. If you aren't using password authentication, you probably want to have your clients generate their own private key and send you a Certificate Signing Request. You can read about that process in the Easy-RSA documentation.

Anyway, the macbook key pair is now located in the pki directory on the server. Now we need to shuffle some filenames around and generate a tarball so I can get the configuration onto my MacBook.

CLIENT=macbook
SERVER=vpn.example.com # change me!

mkdir /tmp/${CLIENT}
cp pki/ca.crt /tmp/${CLIENT}/ca.crt
cp pki/issued/${CLIENT}.crt /tmp/${CLIENT}/client.crt
cp pki/private/${CLIENT}.key /tmp/${CLIENT}/client.key
cp ../client.conf /tmp/${CLIENT}/${SERVER}.ovpn
chmod 644 /tmp/${CLIENT}/*
tar cvzf /tmp/${CLIENT}.tar.gz /tmp/${CLIENT}
rm -rf /tmp/${CLIENT}

A tarball with the server's CA certificate, the client key pair, and the client configuration file for macbook is now located at /tmp/macbook.tar.gz. You can then scp (or worse, email) this file to the client machine and fire up your OpenVPN client. On macOS, I'm a big fan of Tunnelblick. Just double-click the .ovpn file and Tunnelblick will automatically import your VPN profile.

As of this writing, there is currently one small change to make in Tunnelblick: edit your VPN profile and change OpenVPN version to Latest (2.4.4). This allows you to use the more secure GCM cipher. In future versions of Tunnelblick, this step may be unnecessary.

After that, you can connect to your VPN server and start browsing. You can verify your traffic is being tunneled through the VPN by checking your external public IP address (I use icanhazip.com). If you run into any issues, check the OpenVPN logs on your client and server. To troubleshoot, try pinging a public IP address (like 8.8.8.8), as well as your own server (10.8.0.1). If you can ping locally but can't ping out to the internet, it's most likely a firewall or routing issue.

Conclusion

A VPN is a must-have tool to protect your privacy, especially when using a sketchy public network. It also provides a reliable and convenient way to securely access your personal "intranet" from anywhere in the world.

Recent versions of OpenVPN have a very small resource footprint, making it suitable for use on a VPS. When maxing out my server's internet connection with a speed test, OpenVPN delivered 85 mbps while using only 50% of a single CPU core. With typical traffic, system load is minimal.