c nerd blog — using OpenVPN on OpenBSD

introduction

I’ve wanted to set up a Virtual Private Network (VPN) to access the internet for a while now. I want to use one, with its accompanying encryption, to keep details of my life out of criminal hands, as much as I can. I do not wish to be a conman’s low–handing fruit. I do not want some criminal hoovering loose data to pick up information from me or those around me. I do not want that information to be used to abuse or break my reality for strangers’ gains.

Having said that, I don’t expect that anyone will explicitly target me: I am neither rich, nor powerful, nor popular. If one does, well, I’m making it difficult for them. I don’t intend to be worth the hassle.

I’m not concerned about state level interference, mostly because there’s no point. I’m an amateur having a bit of fun with a stick, whereas the state owns lots of tanks and other nasty things that go seriously bang, and even when they’re being nice they unwittingly leave tracks on ordinary peoples’ lawns. I want my lawn track free and desperately dull, as a lawn should be.

It helps that I trust Western European governments, in that it’s in their self–interest to keep an ordered society running effectively, and their concept of an ordered society is not too different to mine. For me, the state that’s governed is not a them, it’s a big, broad, disagreeable, us.

If state level interference is a concern for anyone reading this, then they may well find that my configuration is does not fit their needs, although I like to think it might offer inspiration to start considering a suitable set up.

service provider

I looked at some VPN providers, and settled on Private Internet Access’s (PIA) service, because it was recommended, and seems to have good reviews.

I’d have preferred to find a VPN service provided by a local company, and thus obviously protected by European law, but almost every company I found with a locally based server only pretended to be European. PIA don’t pretend to be anything. No doubt I missed some perfectly good providers.

For technology, I’m using OpenVPN, because it’s well supported and works with OpenBSD. My DMZ is built on OpenBSD.

starting off

I’m using the version of OpenVPN found in OpenBSD 6.0’s packages. As is usual with OpenBSD, it’s well documented, so I won’t waste time and space repeating its descriptions here.

I installed OpenVPN using OpenBSD’s pkg_add. After insufficient thought, I created a Linux style configuration subdirectory to hold my OpenVPN configuration, /etc/openvpn. I set the ownership name and attributes to suite a chrooted configuration: there’s more on that below.

encryption

I configured OpenVPN using an amended version of PIA’s UDP strong configuration. I chose UDP because it is significantly performant over TCP.

I use strong encryption because the alternatives use broken or near–defeated algorithms. To be fair to PIA, they’re forced to provide a service for such algorithms because many devices out there do not support anything better. That is not a problem with OpenBSD.

These configuration files includes PIA certificates needed to run OpenVPN. For example, the UDP strong configuration file contains crl.rsa.4096.pem and ca.rsa.4096.crt. I moved them to /etc/openvpn.

username and password

I got hold of a valid PIA username and password, obtained, unsurprisingly, from Private Internet Access. I used them to create /etc/openvpn/pia-login.txt:

username
password

configuration

I merged the suggested PIA strong configuration file with the OpenBSD OpenVPN sample client configuration to create /etc/openvpn/client.conf:

# These settings are common between the PIA and the OpenVPN sample configuration files.
# They, along with all the other settings, and documented in the OpenVPN man page, and
# various books. I don’t intend to describe them here.
client
proto udp
resolv-retry infinite
nobind
persist-key
persist-tun
tls-client
remote-cert-tls server
comp-lzo
reneg-sec 0

# Selecting a specific network interface makes the PF configuration slightly easier.
dev tun0

# I’ve put PIA’s strong certificates and login in /etc/openvpn.
# The PIA strong configuration download contains the certificates.
auth-user-pass /etc/openvpn/pia-login.txt
cipher aes-256-cbc
auth sha256
crl-verify /etc/openvpn/crl.rsa.4096.pem
ca /etc/openvpn/ca.rsa.4096.crt

# PIA recommend this, but I want occ checks; I want to know about potential problems.
# disable-occ

# I like to be polite.
explicit-exit-notify

# This should be disabled for Windows clients, but this is an OpenBSD client.
topology subnet

# Daemonise the service, and do the keepalive thang.
daemon openvpn
keepalive 10 30

# There is no PIA server in my host country, so I’ve chosen servers in nearby countries.
# They’ll all EU countries: I want to be covered by EU privacy law, for what it’s worth.
# I don’t think I’ve properly understood PIA’s service here, incidentally.
remote germany.privateinternetaccess.com 1197
remote nl.privateinternetaccess.com 1197
remote france.privateinternetaccess.com 1197
remote-random

# As suggested by OpenBSD OpenVPN when it runs.
auth-nocache

# Chroot adds complexity (and security) to the installation, which I discuss below.
user _openvpn
group _openvpn
chroot /var/openvpn

# Logging can be useful.
verb 3
log-append /var/log/openvpn/openvpn
status /var/log/openvpn/status 60

# To make some connections via the ISP, rather than the VPN,
# set up appropriate routes here.
route subnet netmask net_gateway

# As suggested by PIA support.
fast-io

# These scripts keep PF happy.
script-security 2
up "/bin/sh /etc/openvpn/up.sh"
down "/bin/sh /etc/openvpn/down.sh"

OpenVPN routing

I use the OpenVPN route command to talk directly with particular destinations. The primary one is my ISP.

For example, I use email forwarding. I’m far too lazy to set up a properly authenticated email service from my system to the big bad world. My ISP has to do that anyway, and has done so. Furthermore, they can vouch for me. So, out of a sense of deep laziness, I forward my outgoing email to them to distribute.

I have no problem with this because EU law prevents ISPs reading my emails and selling information about their content to conmen, although that doesn’t help when the emails leave the EU. Unfortunately, the only real way to prevent conmen abusing private information once the email has left safe harbour is email encryption, SFAIK, and that’s nowhere near critical mass. If all my friends were among the technorati, ….

I’m quite confident that if I were to connect to my ISP from a random internet address selected by the VPN, rather than one of their allocated addresses, they might not be quite so confident that I really am me. So I don’t route email to them via the VPN. This requires a route command in the OpenVPN configuration file to route connections directly to my ISP.

If I were in the US, or many other countries, I’d set up my email server properly. I may do so eventually, anyway; leaving my email connectivity lazily configured is a loose end.

network interface

OpenVPN uses a tunnel network interface that needs to be configured. There are a number of ways of doing so. I’ve chosen the simplest, with the VPN automatically starting at boot. Here is my /etc/hostname.tun0:

up
!/usr/local/sbin/openvpn --daemon --config /etc/openvpn/client.conf

According to the OpenBSD OpenVPN readme, this configuration avoids problems in PF, particularly with queuing.

The daemon–eyed will note I’ve specified daemon both here and in the configuration file. This is because I have the annoying habit of forgetting it when it’s necessary. Only one daemon is required.

nat

I’m installing OpenVPN on a gateway machine between my internal network and the big bad world. This requires a more complex installation than for a simple client machine. To add to the complexity, I want my gateway to continue to render services connected through the ISP.

First of all, one must nat. Since my firewall already natted to the ISP, that was an easy extension to pf.conf:

ext_if = "???0"
vpn_if = "tun0"

# NAT to big bad world
match out on $ext_if inet from !($ext_if:network) nat-to ($ext_if:0)
match out on $vpn_if inet from !($vpn_if:network) nat-to ($vpn_if:0)

urpf

I had to turn pf.conf’s urpf check off. This is a protection to ensure that naughty routing isn’t being attempted. When a packet is received, urpf-check verifies that, if the sender can be reached by the default route, then any packets returned would use the default route’s network interface.

Unfortunately, this check is defeated by the standard configuration of OpenVPN under OpenBSD. When OpenVPN starts up, it does not change the default route; rather it adds a couple of specialised routes to use the VPN tunnel, routes that happen to cover the entire IPv4 address space. In consequence, any packets that come in on the old interface will fail the urpf check.

If this is what you want to happen, that all data leaves on the VPN interface, and all other external communication is blocked, then that’s good. That, however, is not what I want. I want to maintain contact with my ISP. So I disable the check:

# block in log quick from urpf-failed

routing

The OpenVPN protocol uses UDP packets that must be routed to a PIA server somewhere on the internet, so it’s rather important that those UDP packets are routed via the ISP, not over OpenVPN itself. PF has to be told to allow OpenVPN to do its thing, by adding this to pf.conf:

pass on $ext_if proto udp from self to any port pia-udp
pass on $ext_if proto udp from any to self port pia-udp

and in /etc/services (which is probably a little naughty):

pia-udp        1197/udp

PIA use different ports for different protocols and encryption strategies, so it may necessary to use different values—see the PIA configuration documentation for more information.

Once I’ve worked out the full potential network addresses of the PIA servers, I’ll refine the pass rules above so they only apply to those servers. Unfortunately, I’ve tried that a couple of times already, and found out, the hard way, that my list of addresses was incomplete. The domain names given in client.conf not only lookup to multiple IP addresses, but I’m reasonably sure the servers at those addresses hand over to other servers elsewhere. I could confirm or deny that suspicion by reading up on OpenVPN in detail, or find this out definitively by studying the OpenVPN source code, but I’ve not done so yet. I probably will.

If there are any firewalls and/or routers between the OpenVPN machine and the big bad world, it will be necessary to configure them to allow UDP 1197 out and to forward any incoming UDP 1197 packets to that machine.

logging

My configuration outputs log files to /var/log/openvpn/openvpn. Over time, this file might grow to be a little too humungous. The standard OpenBSD solution to this problem is newsyslog. I’ve added this line to /etc/newsyslog.conf to avoid the issue:

/var/log/openvpn/openvpn 600 7 250 * Z

When the file gets a bit too big, it will be archived.

chroot

Chroot is a bit of a b*gger, and I’m not that experienced with it. I want to be able to run OpenVPN when it is chrooted, but I will sometimes need to run it non–chrooted, usually when chasing down a problem.

So I’ve set up my OpenVPN configuration files for running OpenVPN as root. Then, for chroot, I created a directory under /var for the user _openvpn, and added links to the files and (if necessary) directories OpenVPN needs. I don’t claim this is decent chroot foo, I just find OpenVPN doesn’t immediately fail.

My directory structure is:

/etc/openvpn/ configuration files
/var/log/openvpn # log file
/var/openvpn # user directory
/var/openvpn/status # log file
/var/openvpn/bin/ link to sh
/var/openvpn/etc/openvpn/ links to config files
/var/openvpn/var/log/ link to log directory

The configuration files are owned by root, but can be read by members of the _openvpn group. They are not generally accessible.

scripts

When OpenVPN starts and stops, network interfaces changes, and network addresses change. The OpenBSD firewall, PF, works out network configuration when it loads its configuration. Thus bringing OpenVPN up and down requires us to tell pf to reload its configuration.

Here is my simple up.sh script to do this:

#!/bin/sh
pfctl -f /etc/pf.conf

My down script is a little more complicated, because I’m not convinced OpenVPN always takes down routes as it should. This might be me being impatient. I would have omitted the route deletes had I wanted to force all connections through the VPN to the extent of preventing connectivity when it is down.

#!/bin/sh
route delete 0/1
route delete 128/1
pfctl -f /etc/pf.conf

When not chrooting OpenVPN, it’s easy to tell OpenVPN to run these scripts. Here’s a snippet from the configuration file:

up "/bin/sh /etc/openvpn/up.sh"
down "/bin/sh /etc/openvpn/down.sh"

The real problems come when chrooting OpenVPN. Although the up script is run as root, the down script will run as the chrooted user. Since root access is required to tell PF to reload its configuration, as the down script must do, this is a little awkward.

Fortunately, the OpenBSD distribution of OpenVPN includes a plugin that can run a down script as root. Here are the lines I’ve added to client.conf to tell OpenVPN to do so:

up "/bin/sh /etc/openvpn/up.sh"
plugin /usr/local/lib/openvpn/plugins/openvpn-plugin-down-root.so "/bin/sh /etc/openvpn/down.sh"

This presumes the plugin is located as installed.

dns

DNS leaking occurs when using a VPN to navigate the net, but keeping using DNS servers through another link, such as that to an ISP. If, like me, someone sets up a direct route to their ISP, and if, like many, that someone also uses their ISP’s DNS servers, then they’ll leak. Illicit observers may not know what took place naughty-donkeys.com, but they’ll know the visit occurred.

I’ve always disliked my ISP’s DNS servers: I found them slow and too often down. So, back when, I worked out what public DNS servers were fast and located near me, and have long since used them. When using the VPN, my queries go through the VPN, so those DNS queries leak false information.

PIA offers DNS servers at 209.222.18.218 and 209.222.18.222. They do not leak at all. I’ve added them to my preferred DNS server list.

Having said all this, I’m not particularly fussed about DNS leaking, again because of European privacy laws.

redirection

I run a separate DMZ web server. I want web connections coming from the ISP to be forwarded to that server.

This configuration, as it stands, only supports web users from explicitly routed addresses (as per the routes in client.conf). I have to work out how to support general usage.

tcp_gen = "{ www https 8080 }"
udp_gen = "www"
pass in on $ext_if proto tcp to self port $tcp_gen rdr-to $dmz_server synproxy state (max 200, source-track rule, max-src-nodes 100, max-src-states 3, max-src-conn 100, max-src-conn-rate 15/5, overload flush)
pass in on $ext_if proto udp to self port $udp_gen rdr-to $dmz_server synproxy state (max 200, source-track rule, max-src-nodes 100, max-src-states 3, max-src-conn 100, max-src-conn-rate 15/5, overload flush)
pass out on $dmz_if proto tcp to port $tcp_gen keep state
pass out on $dmz_if proto udp to port $udp_gen keep state

Although using rdr-to to redirect a packet to the same port on another server works whether or not OpenVPN is running, using rdr-to to redirect to a different port fails when OpenVPN is up. Everything works when OpenVPN is down, but not when it’s up. I don’t understand why. I probably need to make another configuration change, but I’ve not sussed it. Luckily, I can easily, and have, amended the DMZ server to accept connections on the original port.

acknowledgements

When I first started to configure OpenBSD, I found PeteDana’s FreeBSD instructions rather useful, even though I’ve ended up with a very different configuration. I had some useful conversations with PIA support, too.

disclaimer

I must mention that what I’ve done does not represent good practice, it’s simply how I made VPN work for me. I am not a professional sysadmin, I am a teddy–bear tester.

Apologies for the lack of IPv6 configuration. I only use IPv4.

conclusion

My VPN configuration seems to work most of the time.


UPDATE

I messed up the log settings, and rather a lot of grammar, in earlier versions of this piece. All that’s now fixed.