3. IPsec on OpenBSD

Now that we have an adequate working knowledge of the IPsec architecture and protocols, we are finally ready to move from theory to practice and start having some fun with OpenBSD!

OpenBSD ships by default with full IPsec support in the stock kernel and provides a set of user-space daemons and tools for managing IPsec configuration, dynamic key exchange and high availability; and the great thing is that, as you'll see, setting up an IPsec VPN on OpenBSD is an incredibly simple and fast task, especially compared to most other IPsec implementations out there.

But before proceeding to edit configuration files and run system commands, let's take a brief look at the basic network topology of the VPN that we are going to set up in this document; it's a very simple site-to-site VPN, with a couple of multi-homed security gateways (VPN1 and VPN2) linking two remote private networks (172.16.0.0/24 and 192.168.0.0/24).

VPN topology

In this chapter, we will set up the VPN using IPsec: to be more precise, we will configure it in tunnel mode (the only option for network-to-network VPNs) and use the ESP protocol in order to encrypt the VPN traffic as it traverses the Internet; we will also consider the case of redundant IPsec gateways with carp(4). Then, in the next chapters, we will see how the same VPN can be implemented using alternative solutions, in particular OpenVPN and OpenSSH.

3.1 Preliminary steps

Before proceeding to configure IPsec, we have to perform a few preliminary steps to make sure the systems are correctly set up for IPsec to work properly. The IPsec protocols are enabled or disabled in the OS's TCP/IP stack via two sysctl(3) variables: net.inet.esp.enable and net.inet.ah.enable, both enabled by default; you can check this by running the sysctl(8) command:

# sysctl net.inet.esp.enable
net.inet.esp.enable=1
# sysctl net.inet.ah.enable
net.inet.ah.enable=1

Since our VPN gateways will have to perform traffic routing, we also need to enable IP forwarding, which is turned off by default. This is done, again, with sysctl(8), by setting the value of the net.inet.ip.forwarding variable to "1" if you want any kind of traffic to be forwarded or "2" if you want to restrict forwarding to only IPsec-processed traffic:

# sysctl net.inet.ip.forwarding=1
net.inet.ip.forwarding: 0 -> 1

Optionally, you may also want to enable the IP Payload Compression Protocol (IPComp) to reduce the size of IP datagrams for higher VPN throughput; however, bear in mind that the reduction of bandwidth usage comes at the expense of a higher computational overhead (see [RFC3173] for further details):

# sysctl net.inet.ipcomp.enable=1
net.inet.ipcomp.enable: 0 -> 1

To make these settings permanent across reboots, add the following variables to the /etc/sysctl.conf(5) file:

/etc/sysctl.conf
[ ... ]
net.inet.esp.enable=1     # Enable the ESP IPsec protocol
net.inet.ah.enable=1      # Enable the AH IPsec protocol
net.inet.ip.forwarding=1  # Enable IP forwarding for the host. Set it to '2' to
                          #   forward only IPsec traffic
net.inet.ipcomp.enable=1  # Optional: compress IP datagrams

Finally, we need to bring up the enc(4) virtual network interface. This interface allows you to inspect outgoing IPsec traffic before it is encapsulated and incoming IPsec traffic after it is decapsulated; this is primarily useful for filtering IPsec traffic with PF and for debugging purposes.

# ifconfig enc0 up

To make the system automatically bring up the enc(4) interface at boot, create the /etc/hostname.enc0 configuration file:

/etc/hostname.enc0
up

3.2 Setting up the PKI

OpenBSD's IKE key management daemon, isakmpd(8), relies on public key certificates for authentication and therefore requires that you first set up a Public Key Infrastructure (PKI) for managing digital certificates.

The first step in setting up the PKI is the creation of the root CA certificate (/etc/ssl/ca.crt) and private key (/etc/ssl/private/ca.key) on the signing machine (which doesn't have to be necessarily one of the VPN gateways) using openssl(1); e.g.:

CA# openssl req -x509 -days 365 -newkey rsa:1024 \
> -keyout /etc/ssl/private/ca.key \
> -out /etc/ssl/ca.crt
Generating a 1024 bit RSA private key
........................................++++++
......++++++
writing new private key to '/etc/ssl/private/ca.key'
Enter PEM pass phrase: <passphrase>
Verifying - Enter PEM pass phrase: <passphrase>
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []: IT
State or Province Name (full name) []: Italy
Locality Name (eg, city) []: Milan
Organization Name (eg, company) []: Kernel Panic Inc.
Organizational Unit Name (eg, section) []: IPsec
Common Name (eg, fully qualified host name) []: CA.kernel-panic.it
Email Address []: danix@kernel-panic.it
CA#

The next step is the creation of a Certificate Signing Request (CSR) on each of the IKE peers; for instance, the following command will generate the CSR (/etc/isakmpd/private/1.2.3.4.csr) for the VPN1 machine (the IP address, in this case "1.2.3.4", is used as unique ID):

VPN1# openssl req -new -key /etc/isakmpd/private/local.key \
>  -out /etc/isakmpd/private/1.2.3.4.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []: IT
State or Province Name (full name) []: Italy
Locality Name (eg, city) []: Milan
Organization Name (eg, company) []: Kernel Panic Inc.
Organizational Unit Name (eg, section) []: IPsec
Common Name (eg, fully qualified host name) []: 1.2.3.4
Email Address []: danix@kernel-panic.it

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: <enter>
An optional company name []: <enter>
VPN1#

Next, the CSRs must be sent to the CA, which will generate the signed certificates out of the certificate requests. For instance, assuming the CSR file is in the current directory:

CA# env CERTIP=1.2.3.4 openssl x509 -req \
>  -days 365 -in 1.2.3.4.csr -out 1.2.3.4.crt \
>  -CA /etc/ssl/ca.crt -CAkey /etc/ssl/private/ca.key \
>  -CAcreateserial -extfile /etc/ssl/x509v3.cnf -extensions x509v3_IPAddr
Signature ok
subject=/C=IT/ST=Italy/L=Milan/O=Kernel Panic Inc./OU=IPsec/CN=1.2.3.4/emailAddress=danix@kernel-panic.it
Getting CA Private Key
Enter pass phrase for /etc/ssl/private/ca.key: <passphrase>
CA#

Finally, you need to copy the newly-generated certificates (the files ending in .crt) to the respective machines in the /etc/isakmpd/certs/ directory, as well as the CA certificate (/etc/ssl/ca.crt) in /etc/isakmpd/ca/.

3.3 Configuration

So we have conveniently set up the system for IPsec use and generated all the required certificates for IKE peer authentication; now we're finally ready to configure our VPN connection. On OpenBSD, all the configuration for IPsec takes place in a single file, /etc/ipsec.conf(5), which uses a very compact syntax, similar to pf.conf(5), to define almost every characteristic of the VPN; the basic format of the file is as follows:

There are different types of ipsec.conf(5) rules, depending on whether you want IPsec flows and SAs to be set up automatically (using isakmpd(8)) or manually; we will only consider the former case (which is usually what you want), so please refer to the documentation for further details on manual setups. The syntax is as follows:

ike [mode] [encap] [tmode] [proto protocol] \
    from src [port sport] [(srcnat)] to dst [port dport] \
    [local localip] [peer remote] \
    [mode auth algorithm enc algorithm group group] \
    [quick auth algorithm enc algorithm group group] \
    [srcid string] [dstid string] \
    [psk string] [tag string]

Though it may look rather complex at first, actual rules are usually very short and simple because most of the parameters can be omitted, in which case the default values are used. But let's examine the rule syntax in detail:

ike [mode] [encap] [tmode]
the ike keyword specifies that isakmpd(8) must be used to automatically establish the Security Associations for this flow; mode can be either "active" (isakmpd(8) will immediately start negotiation of this tunnel), "passive" (to wait for an incoming request from the remote peer to start negotiation) or "dynamic" (to be used for hosts with dynamic IP addresses) and defaults to "active"; encap specifies the encapsulation protocol and can be either "esp" (default) or "ah"; tmode is the transport mode to use, i.e. "tunnel" (default) or "transport".
proto protocol
Restrict the flow to a specific IP protocol (e.g. TCP, UDP, ICMP); by default all protocols are allowed.
from src [port sport] [(srcnat)] to dst [port dport]
Specify the source and destination addresses of the packets that this rule applies to; you may also specify source and/or destination ports, but only in conjunction with the TCP or UDP protocols. The srcnat parameter can be used to specify the actual source address in outgoing NAT/BINAT scenarios.
local localip peer remote
Specify the local and remote endpoints of the VPN; the local endpoint is required only if the machine has multiple addresses; the remote endpoint can be omitted if it corresponds to the dst parameter.
mode auth algorithm enc algorithm group group
Specify the mode ("main" or "aggressive") and cryptographic transforms to be used for IKE phase 1 negotiation; please refer to the documentation for a complete list of the possible values and their defaults.
quick auth algorithm enc algorithm group group
Specify the cryptographic transforms to be used for IKE phase 2 negotiation; please refer to the documentation for a complete list of the possible values and their defaults.
srcid string dstid string
Define the unique ID that isakmpd(8) will use as the identity of the local (srcid) and remote (dstid) peer; if omitted, the IP address is used.
psk string
Use a pre-shared key for authentication instead of isakmpd(8).
tag string
Add a pf(4) tag to IPsec packets matching this rule.

So let's write the configuration files for the site-to-site VPN we're setting up; as you'll see, it's a really trivial task and a few rules will do. On the VPN1 host, the /etc/ipsec.conf(5) file will look like this:

/etc/ipsec.conf
# Macros
ext_if      = "rl0"                               # External interface (1.2.3.4)
local_net   = "172.16.0.0/24"                     # Local private network
remote_gw   = "5.6.7.8"                           # Remote IPsec gateway
remote_nets = "{192.168.0.0/24, 192.168.1.0/24}"  # Remote private networks

# Set up the VPN between the gateway machines
ike esp from $ext_if to $remote_gw
# Between local gateway and remote networks
ike esp from $ext_if to $remote_nets peer $remote_gw
# Between the networks
ike esp from $local_net to $remote_nets peer $remote_gw

and on VPN2:

/etc/ipsec.conf
# Macros
ext_if     = "rl0"                               # External interface (5.6.7.8)
local_nets = "{192.168.0.0/24, 192.168.1.0/24}"  # Local private networks
remote_gw  = "1.2.3.4"                           # Remote IPsec gateway
remote_net = "172.16.0.0/24"                     # Remote private network

# Set up the VPN between the gateway machines
ike esp from $ext_if to $remote_gw
# Between local gateway and remote network
ike passive esp from $ext_if to $remote_net peer $remote_gw
# Between the networks
ike esp from $local_nets to $remote_net peer $remote_gw

Now we are ready to start the isakmpd(8) daemon on both gateways; we will make it run in the foreground ("-d" option) in order to easily notice any errors:

# isakmpd -K -d

Then, again on both gateways, we can parse ipsec.conf(5) rules ("-n" option of ipsecctl(8)) and, if no errors show up, load them:

# ipsecctl -n -f /etc/ipsec.conf
# ipsecctl -f /etc/ipsec.conf

You can check that IPsec flows and SAs have been correctly set up by running ipsecctl(8) with the "-s all" option; for example:

VPN1# ipsecctl -s all
FLOWS:
flow esp in from 192.168.0.0/24 to 1.2.3.4 peer 5.6.7.8 srcid 1.2.3.4/32 dstid 5.6.7.8/32 type use
flow esp out from 1.2.3.4 to 192.168.0.0/24 peer 5.6.7.8 srcid 1.2.3.4/32 dstid 5.6.7.8/32 type require
flow esp in from 192.168.1.0/24 to 1.2.3.4 peer 5.6.7.8 srcid 1.2.3.4/32 dstid 5.6.7.8/32 type use
flow esp out from 1.2.3.4 to 192.168.1.0/24 peer 5.6.7.8 srcid 1.2.3.4/32 dstid 5.6.7.8/32 type require
[ ... ]

SAD:
esp tunnel from 5.6.7.8 to 1.2.3.4 spi 0x027fa231 auth hmac-sha2-256 enc aes
esp tunnel from 1.2.3.4 to 5.6.7.8 spi 0x13ebc203 auth hmac-sha2-256 enc aes
esp tunnel from 1.2.3.4 to 5.6.7.8 spi 0x25da85ac auth hmac-sha2-256 enc aes
esp tunnel from 5.6.7.8 to 1.2.3.4 spi 0x891aa39b auth hmac-sha2-256 enc aes
[ ... ]
VPN1#

Well, since everything seems to be working fine, we can configure the system to automatically start the VPN at boot by adding the following variables in /etc/rc.conf.local(8) on both security gateways:

/etc/rc.conf.local
isakmpd_flags="-K"    # Avoid keynote(4) policy checking
ipsec=YES             # Load ipsec.conf(5) rules

3.4 Packet filtering

IPsec traffic can be filtered on the enc(4) interface, where it appears unencrypted before encapsualtion and after decapsulation. The following are the main points to keep in mind for filtering IPsec traffic:

3.5 Redundant VPNs with sasyncd(8)

One of the most interesting features of OpenBSD's implementation of the IPsec protocol is the possibility to set up multiple VPN gateways in a redundant configuration, allowing for transparent failover of VPN connections without any loss of connectivity.

Typically, in OpenBSD, redundancy at the network level is achieved through the carp(4) protocol, which allows multiple hosts on the same local network to share a common IP address. Redundancy at the logical VPN layer, instead, is provided by the sasyncd(8) daemon, which allows the synchronization of IPsec SA and SPD information between multiple IPsec gateways.

We have already covered the carp(4) protocol in a previous document about redundant firewalls, so we won't come back to this topic now; therefore, I assume that you already have a working carp(4) setup and that you have modified your configuration accordingly (in particular the ipsec.conf(5) and pf.conf(5) files).

Please note that, as stated in the documentation, for SAs with replay protection enabled, such as those created by isakmpd(8), the sasyncd(8) hosts must have pfsync(4) enabled to synchronize the in-kernel SA replay counters (for a detailed discussion of the pfsync(4) protocol, please refer to [CARP]).

The sasyncd(8) daemon is configured through the /etc/sasyncd.conf(5) file, which has a rather self-explanatory syntax; below is a sample configuration file:

/etc/sasyncd.conf
# carp(4) interface to track state changes on
interface carp0
# Interface group to use to suppress carp(4) preemption during boot
group     carp
# sasyncd(8) peer IP address or hostname. Multiple 'peer' statements are allowed
peer      172.16.0.253
# Shared AES key used to encrypt messages between sasyncd(8) hosts. It can be
# generated with the openssl(1) command 'openssl rand -hex 32'
sharedkey 0x115c413529ba5ac96b208d83a50473b3e6ade60e66c59a10a944ad3d273148dd

Since sasyncd.conf(5) contains the shared secret key used to encrypt data between the sasyncd(8) hosts, it should have restrictive permissions (600) and belong to the "root" or "_isakmpd" user:

# chown root /etc/sasyncd.conf
# chmod 600 /etc/sasyncd.conf

Well, now we're ready to run the sasyncd(8) daemon on the redundant gateways; but first we need to restart isakmpd(8) with the "-S" option, which is mandatory on redundant setups (remember to add it also to isakmpd_flags in /etc/rc.conf.local(8)):

# pkill isakmpd
# isakmpd -S -K
# sasyncd

You can use ipsecctl(8) to verify that SAs are correctly synchronized between the IPsec gateways. Finally, if everything is working fine, we only have to add the following variable to the /etc/rc.conf.local(8) file to automatically start sasyncd(8) on boot.

/etc/rc.conf.local
sasyncd_flags=""

Note: sasyncd(8) must be manually restarted every time isakmpd(8) is restarted.