This guide will explain how to setup a site-to-site IPsec tunnel (i.e., tunnel mode IPsec) between two OpenBSD gateways. Throughout this document there are example configs shown, some of which contain secret key data. DO NOT use these example keys! Create your own (as shown) and keep them private.

The Tools

OpenBSD ships with all the tools needed to begin using IPsec. OpenBSD does not require a kernel recompile, software installtion, 3rd-party modules or anything else to get IPsec up and running.

The following is a list of OpenBSD's IPsec tool set.

isakmpd(8)
The Internet Security Association and Key Management Protocol (ISAKMP) Daemon. This daemon is responsible for exchanging cryptographic keys and setting up security associations (SAs) with IPsec peers.
ipsecctl(8)
The ipsecctl(8) utility is used to create and delete the flows that determine which traffic is protected using IPsec. It's also used as the primary method of configuring IPsec. It does this by reading its configuration file and then passing various commands to isakmpd(8) in order to establish the IPsec connection.
ipsec.conf(5)
The /etc/ipsec.conf file is the configuration file for the ipsecctl(8) utility. The flows and security associations that make up an IPsec connection are defined in this file using an easy-to-read syntax.
sasyncd(8)
The security association synchronization daemon. This daemon synchronizes IPsec SAs and flows between redundant gateways.
sasyncd.conf(5)
The /etc/sasyncd.conf file is the configuration file for sasyncd(8).

Terminology

Some of the terms and acronyms used when talking about IPsec are listed below.

Authentication Header (AH)
AH provides data integrity and origin authenticity of the payload. AH computes a hash using certain parts of the payload and sends the hash along with the packet payload. The receiver recomputes the hash and verifies it against the hash that it received in the packet to ensure that they match.
Encapsulating Security Payload (ESP)
ESP provides origin authenticity, integrity and confidentiality for the encapsulated IP packet. With ESP the payload of the IP packet is encrypted.
Internet Key Exchange (IKE)
IKE is the protocol that isakmpd(8) uses to setup Security Associations with IPsec peers.
Main Mode
When isakmpd(8) is negotiating with an IPsec peer using IKE, the first phase of the negotiation is called main mode. During main mode the two peers establish a secure channel which they then use to exchange the data needed to setup the security associations.
Quick Mode
When isakmpd(8) is negotiating with an IPsec peer using IKE, and after main mode has been completed, the two peers exchange the data necessary to setup the security associations. This exchange is called quick mode. Once the quick mode exchange is complete, data can flow across the IPsec connection.
Pre-Shared Key (PSK)
A secret key/password that is exchanged between two parties using some secure method. For example, generating a secret key and using SSH to upload they key to the IPsec gateways.
Security Association (SA)
A grouping of the security parameters that are used to protect the data sent over the IPsec connection. The parameters in the SA include key data and the encryption and hash algorithms. An SA controls traffic in one direction only. Two SAs are required on each gateway in order to achieve bidirectional IPsec traffic flow. SAs are established automatically by isakmpd(8) or can be created manually using ipsecctl(8).
Transport Mode
When an IPsec connection is run in transport mode only the payload of the original IP packet is encrypted. The packet header is left intact meaning the original destination address is used when routing the encrypted packet to its destination. Transport mode is used for host-to-host communication.
Tunnel Mode
When an IPsec connection is run in tunnel mode the entire original packet is encrypted. This encrypted packet is then encapsulated into a new IP packet. This new IP packet is routed to the remote IPsec peer. Tunnel mode is used for site-to-site (network-to-network) communication. Since the internal/site IP addresses are hidden inside the encrypted packet, these packets can be routed across public networks such as the Internet.

Building a Site-to-Site Tunnel

This scenario will describe building a site-to-site tunnel that's used to connect two physically separate networks together over the public Internet. At each site is an OpenBSD gateway that will act as the endpoints for the IPsec connection. The end goal of this scenario is to enable full communication between the two networks.

Our sample topology:

172.16.0.0/24     .1                   192.0.2.153
 (Network 1) --- em0[OpenBSD gateway 1]em1 ------+
                                                 |
                                            ((Internet))
                                                 |
  (Network 2) --- em0[OpenBSD gateway 2]em1 -----+
192.168.15.0/24   .1                    192.0.123.85

Gather Information

Before anything can be configured we need to gather some basic information. We already have the necessary IP address information from the topology diagram above. We now need to define the IPsec parameters that will be used to build the connection.

Right off the bat we know that since we're connecting two networks together using IPsec that we need to use tunnel mode. We also know that since we want to keep the transported data private (i.e., encrypted) that we need to use ESP as the transport protocol. Now we need to decide on the encryption and authentication types to use for main mode and quick mode. The easiest thing to do here is to not specify any encryption or authentication types and insead allow the default types to be used. Since both endpoints of the IPsec tunnel are OpenBSD and they both support the same set of encryption/authentication types, they will not have any difficulty negotiating with each other. For this exercise, a set of non-default types will be chosen in order to demonstrate how to configure them. The complete list of support enc/auth types is listed in the ipsec.conf(5) manpage in the "CRYPTO TRANSFORMS" section. For main mode we will use hmac-sha2-256 for authentication and blowfish for encryption. For quick mode we will use hmac-sha2-384 and blowfish.

Next we need to decide what method of authentication should be used between the gateways. To keep things simple, this example will use a pre-shared key (PSK). In order to ensure a strong PSK is used, we will use the openssl(1) utility to generate the key. We could just as easily pick our own PSK to use, however using OpenSSL ensures that 1) the PSK is random and not predictable and 2) that it's sufficiently long enough that things like a brute-force attack are not feasible.

% openssl rand 20 | hexdump -e '20/1 "%02x"'
204931646879ae1265a7d0437a51cc08fe8d7484

This command has generated a random 20 byte (160 bit) string and formatted it in hexadecimal. This key should be treated with care and kept secret at all times. A secure method of transport should be used when uploading the key to the gateways such as using SSH or entering the key from the console.

Here is a summary of the information we've decided upon:

Gateway 1 IP 192.0.2.153

Gateway 2 IP 192.0.123.85

Network 1 172.16.0.0/24

Network 2 192.168.15.0/24

Tunnel/Transport Mode Tunnel Mode

Transport Protocol ESP

Main Mode enc/auth hmac-sha2-256, blowfish

Quick Mode enc/auth hmac-sha2-384, blowfish

Authentication method Pre-shared key

Pre-shared key 204931646879ae1265a7d0437a51cc08fe8d7484

Creating the ipsec.conf(5) File

Now that we've defined all the configuration parameters we can begin configuring the tools. We'll start with the /etc/ipsec.conf file. OpenBSD ships with a default ipsec.conf file that can be used as a starting point. We're going to start with an empty file and configure everything from the ground up.

Since our intent is to have isakmpd(8) configure the flows and SAs automatically, we will define the IKE parameters in ipsec.conf. When ipsecctl(8) loads the config, it will instruct isakmpd to configure itself appropriately. The basic IKE configuration syntax is:

ike [mode] [encap] [tmode] \ from src to dst \
 local localip peer remoteip \
 main auth algorithm enc algorithm \
 quick auth algorithm enc algorithm \

These options are explained below.

mode Defines whether isakmpd will attempt to initiate the IPsec connection or whether it will wait for a connection request from the remote peer. Possible values are:

  • active - Tells isakmpd to initiate the IPsec connection. This is the default.
  • passive - Tells isakmpd to wait for a connection request.
  • dynamic - Tells isakmpd to initiate the IPsec connection and to enable Dead Peer Detection. This mode should be used when the remote peer has a dynamic IP address.

encap The transport protocol to use, either esp or ah.

tmode Either tunnel or transport and indicates whether to enable tunnel mode or transport mode. The default is tunnel.

src The IP address or subnet where the traffic to be protected is coming from. For a transport mode connection this will be the IP address of the local host. For a tunnel mode connection this will be the subnet and mask of the local network that should have its traffic sent through the IPsec tunnel.

dest The IP address or subnet where the protected traffic is being sent to. For a transport mode connection this will be the IP address of the remote host. For a tunnel mode connection this will be the subnet and mask of the remote network that will be reachable through the IPsec tunnel.

localip The IP address on the local machine where isakmpd should source the connection from. This option is generally not needed since isakmpd will choose the correct IP address automatically.

remoteip The IP address of the remote IPsec gateway.

algorithm The authentication or encryption algorithm to use during main mode and quick mode. The possible algorithms are documented in the ipsec.conf(5) man page under the "CRYPTO TRANSFORMS" section.

key The pre-shared key.

Now that we have the syntax for creating the config file we can substitute our IPsec parameters from the table above into the file.

The ipsec.conf file on gateway 1:

ike esp from 172.16.0.0/24 to 192.168.15.0/24 \ peer 192.0.123.85 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484

The ipsec.conf file on gateway 2:

ike esp from 192.168.15.0/24 to 172.16.0.0/24 \ peer 192.0.2.153 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484

Note that even though we don't specify the active or tunnel options they are the defaults and will be automatically applied by ipsecctl.

Starting isakmpd

Now that the ipsec.conf file is written we can tell ipsecctl(8) to load this config. When it does, it will instruct isakmpd to setup the IPsec connection based on the parameters we've specified. It's then isakmpd's job to create the SAs, setup the flows, and authenticate the peer.

To have isakmpd start when the system boots, edit the /etc/rc.conf.local file and add the following on a new line:

isakmpd_flags="-K"

The -K option to isakmpd tells it not to load a keynote(4) policy. This option is necessary when using ipsecctl to control isakmpd.

You can start isakmpd from the shell by passing it the same flag:

# isakmpd -K

Now load the ipsec.conf file using ipsecctl:

# ipsecctl -f /etc/ipsec.conf

At this point ipsecctl will have communicated with isakmpd to tell it to establish the IPsec connection. To have ipsecctl run at system boot, add the following to /etc/rc.conf.local:

ipsec=YES

At this point isakmpd should be running and the ipsec.conf file should be loaded on both gateways.

After a short time the IPsec connection will be established. You can view the flows and SAs using ipsecctl.

gw1# ipsecctl -s all
FLOWS:
flow esp in from 172.16.0.0/24 to 192.168.15.0/24 peer 192.0.123.85 \
  srcid 192.0.2.153/32 dstid 192.0.123.85/32 type use
flow esp out from 192.168.0.0/24 to 172.16.0.0/24 peer 192.0.123.85 \
  srcid 192.0.2.153/32 dstid 192.0.123.85/32 type use

SAD:
esp tunnel from 192.0.123.85 to 192.0.2.153 spi 0x... \
  auth hmac-sha2-384 enc blowfish \
  authkey 0x... \
  enckey 0x...
esp tunnel from 192.0.2.153 to 192.0.123.85 spi 0x... \
  auth hmac-sha2-384 enc blowfish \
  authkey 0x... \
  enckey 0x...

We can see here on gateway #1 two flows have been established, one in each direction. Underneath that are the entries in the security association database (SAD). Like the flows, there is an SA in each direction. The "0x..." indicates values that will be unique for each SA. If the IPsec connection is not established then the output above will only show the flows and not the SAs.

Assuming you can see the flows and SAs then the connection is ready to pass traffic.

Allowing IPsec Traffic Through pf(4)

Since most OpenBSD gateways will be running pf(4), it's necessary to add some explicit rules to allow IPsec traffic to and from the remote gateway. Two types of traffic need to be permitted between gateways:

  1. UDP traffic on ports 500 and 4500. These are the ports that isakmpd uses to communicate.
  2. ESP or AH packets (depending which one the IPsec connection is using).

These rules can be used on gateway #1 to achieve this.

ipsec_peer = "192.0.123.85"ext_if = "em1"

pass in on $ext_if proto udp from $ipsec_peer \
 to ($ext_if) port { 500 4500 }
pass out on $ext_if proto udp from $ext_if \
 to $ipsec_peer port { 500 4500 }

pass in on $ext_if proto esp from $ipsec_peer to ($ext_if)
pass out on $ext_if proto esp from $ext_if to $ipsec_peer

These rules would need to be integrated into the existing rules on gateway #1 and a similar set of rules would need to be integrated on gateway #2.

Filtering Traffic on the Tunnel

The previous section talked about filtering IPsec traffic to and from the gateway. Now we'll look at how to filter user traffic that crosses the IPsec tunnel.

Filtering on enc0

Each OpenBSD gateway has a virtual enc(4) interface. This interface receives an unencrypted copy of all traffic sent and received on all IPsec connections. By using pf(4) to filter traffic on this interface we can control the types of traffic permitted on the tunnel.

The simplest filter is of course to allow all traffic inbound and outbound. This can be achieved using the "set skip" option.

set skip on enc0

Of course much more fine-grained control can be applied as well. Let's assume in our sample topology that we only want to allow users on network #1 to be able to access a specific web server located on network #2. Users on network #2 should not be able to access any resources on network #1. By applying some filter rules to the enc0 interface on gateway #1 we can achieve this policy:

ext_if = "em1"
int_if = "em0"
ipsec_peer = "192.0.123.85"
www_server = "192.168.15.5"
www_ports = "{ 80 443 }"

block on enc0

pass in on enc0 proto ipencap from $ipsec_peer to ($ext_if) \
 keep state (if-bound)

pass out on enc0 proto tcp from $int_if:network to $www_server \
 port $www_ports keep state (if-bound)

Two items need special attention here.

  • Passing ipencap traffic: Because of how the OpenBSD IP stack works, incoming tunnel mode traffic must be explicity permitted by passing packets of protocol type "ipencap". A corresponding outbound rule is not necessary because the stack handles outbound tunnel mode packets slightly differently. Care should be taken to only allow ipencap traffic from the specific hosts that we have IPsec connections with. If the remote IPsec peer has a dynamic IP address this obviously won't be possible and the above filter rule would have to be modified to not filter on source IP address. If the IPsec connection is using transport mode then this rule is not needed.
  • Interface-bound state entries: Filter rules that permit user traffic over the tunnel should specify the "if-bound" state option. Without this option, if the IPsec connection is down (for example, if the remote gateway is offline) then users' traffic will be routed using the normal routing table which would result in this traffic being sent over the public Internet. Using "if-bound" ensures that this traffic will only be permitted across the enc0 interface and therefore only across the IPsec tunnel. Care should also be taken when writing filter rules that permit regular user traffic to and from the Internet. Ensure that these rules do not inadvertantly allow private traffic to leak to the Internet when the IPsec connection is down.

Filtering using pf Tags

A pf tag is an internal identifier that can be attached to a packet as it enters an OpenBSD host. The tag can then be used as filter criteria by pf. See the Packet Tagging section of the PF User's Guide for more details on tagging. The isakmpd daemon is capable of applying pf tags to incoming IPsec packets.

In order to apply a tag to an IPsec connection it must specified in the ipsec.conf file using the "tag" keyword. Taking our existing ipsec.conf file from gateway #2, we simply add the tag to the end:

ike esp from 192.168.15.0/24 to 172.16.0.0/24 \ peer 192.0.2.153 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484 \
 tag net1-to-net2

In this case the arbitrary string "net1-to-net2" was chosen as the tag. All incoming IPsec packets belonging to this connection will be tagged with "net1-to-net2". We can now incorporate this tag into the pf filter rules on gateway #2. Since we know from the previous section that we only want to allow users on network #1 to access a specific web server on network #2, we can apply this policy on gateway #2 by using the pf tag:

ext_if = "em1"int_if = "em0"
ipsec_peer = "192.0.2.153"
www_server = "192.168.15.5"
www_ports = "{ 80 443 }"

block on enc0

pass in on enc0 proto ipencap from $ipsec_peer to ($ext_if) \
 keep state (if-bound)

pass in on enc0 keep state (if-bound) tagged net1-to-net2

pass out on $int_if proto tcp to $www_server port $www_ports \
 keep state (if-bound) tagged net1-to-net2

This example ruleset allows users on network #1 to access the web server on network #2 by matching on the pf tag that's being applied to packets by isakmpd. An additional rule is added on $int_if to demonstrate how filtering on the tag and on IP address and port can be combined.

Filtering Traffic Using IKE Policy

The last way of filtering traffic is by using the IKE policy to limit what kind of traffic will be encapsulated and sent across the tunnel. Within the ipsec.conf file it's possible to define the IP protocol and TCP/UDP ports to allow traffic to and from. Continuing with our example of only allowing network #1 to reach a web server on network #2, we could adjust the ipsec.conf file on gateway #1 to read:

ike esp proto tcp from 172.16.0.0/24 to 192.168.15.0/24 port 80 \ peer 192.0.123.85 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484

When this ipsec.conf is loaded by ipsecctl, it will setup a flow between 172.16.0.0/24 and 192.168.15.0/24 for TCP port 80 only. Similar changes should be made on gateway #2.

There's one thing we're missing now though. In the previous filtering methods we allowed port 443 traffic to reach the web server in addition to port 80. How do we allow more than one port (or protocol) using this method? The answer is create a second IKE policy that specifes port 443:

ike esp proto tcp from 172.16.0.0/24 to 192.168.15.0/24 port 443 \ peer 192.0.123.85 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484

A matching policy needs to be created on gateway #2.

Adding Redundancy

OpenBSD has built-in support for deploying redundant IPsec gateways. Synchronization of SAs and flows is handled by the sasyncd(8) daemon. This daemon is most often used in conjunction with carp(4). By enabling CARP on the redundant gateways they able to share a common external IP address. Configuring all IPsec connections to use this shared IP address allows the connections to failover between the redundant gateways.

We'll modify our example topology to include a set of two redundant gateways connected to network #1.

172.16.0.0/24     .2                      192.0.2.154
 (Network 1) --- em0[OpenBSD gateway 1a]em1 -----------+
           |    .1 carp1           carp2 192.0.2.153  |
           +---- em0[OpenBSD gateway 1b]em1 --+         |
                  .3             192.0.2.155  |        |
                                              |        |
                                             ((Internet))
                                                   |
  (Network 2) --- em0[OpenBSD gateway 2]em1 -------+
 192.168.15.0/24   .1                  192.0.123.85

Network #1 now has two gateways: gateway #1a and #1b. These gateways are running CARP on their internal and external interfaces.

  • Internal: carp1, 172.16.0.1
  • External: carp2, 192.0.2.153

The general operation and configuration of CARP is covered in the PF User's Guide Firewall Redundancy with CARP and pfsync.

The ipsec.conf file used on gateway #1a and #1b is mostly the same as in the earlier sections. The only thing that needs changing is to add the "local" keyword which tells isakmpd which IP address to bind to. Without this option, isakmpd would automatically use the IP address assigned to the physical em0 interface.

ike esp from 172.16.0.0/24 to 192.168.15.0/24 \ local 192.0.2.153 peer 192.0.123.85 \
 main auth hmac-sha2-256 enc blowfish \
 quick auth hmac-sha2-384 enc blowfish \
 psk 204931646879ae1265a7d0437a51cc08fe8d7484

By telling isakmpd to use the shared CARP IP address we ensure that traffic will continue to flow when the gateways failover. Likewise on gateway #2, the ipsec.conf file should be changed so that the "peer" IP is set to the external CARP IP, 192.0.2.153.

The next bit of redundancy needed is for the IPsec SAs and flows. This is handled by sasyncd(8). We'll use a very basic sasyncd.conf file with these options:

interface The name of the CARP interface to track. When this interface is in "master" state then sasyncd will know the gateway is the current leader for the CARP group.

peer The IP address of the other gateway. This is the address that synchronization messages will be sent to.

sharedkey A pre-shared key used for encrypting updates sent between gateways.

The sasyncd.conf file on gateway #1a looks like:

# track the external carp interfaceinterface carp2
# address of gateway #1b
peer 172.16.0.3
# 256 bit shared key
sharedkey 0xda23169c9f49bb1fcdfab7d556588975433f831b3723d273a06e9d780f2cc

To start sasyncd on system boot, add a line to /etc/rc.conf.local:

sasyncd_flags=""

Check your pf.conf ruleset to ensure that traffic between the internal interfaces of gateway #1a and #1b on TCP port 500 is permitted. This is the port that sasyncd will use.

One last thing to do and that is if isakmpd is already running on gateway #1a or #1b, kill it and restart it by adding the -S flag to its startup options. This option tells isakmpd not to delete SAs when it's killed. The system startup scripts will automatically add the -S option when sasyncd is enabled.

At this point the tunnel should be up between the active gateway and gateway #2. Traffic should be flowing as before. On gateway #1b (the standby gateway), you should be able to see the IPsec SAs and flows which are being synchronized from gateway #1a using the ipsecctl -s all command.

Once the SAs are synchronized it should be possible to fail over between gateway #1a and #1b without disruption to the IPsec connection.

Troubleshooting

Some troubleshooting steps in case things aren't working correctly.

Startup Order is Important

When OpenBSD boots, the rc(8) startup script runs the various IPsec tools in the correct order to ensure that everything works as it should (assuming of course that all the correct tools are enabled in /etc/rc.conf.local). If you're starting the tools by hand from the command line, do so in this order:

  1. Run isakmpd with the -K and -S flags
  2. Load the /etc/ipsec.conf file with ipsecctl(8)
  3. Run sasyncd