Roadwarrior IPsec

OpenIKED is OpenBSD's native VPN solution. It is included with the base system, so no installation will be necessary. iked(8) handles IPsec flows and security associations (SAs). Using ipsec(4), we can provide users with a VPN.

Pros:

  • Clean
  • Secure
  • Interoperable
  • Simple to Configure

In this guide, we configure a server to provide ipsec for devices in a road warrior setup: an end-user wants to relay all his traffic from a single device through the VPS to the Internet. The server acts as an IPsec gateway, and only the server needs a public IP address. The end-user can be behind a NAT.

This is based on the VPN guide from the OpenBSD FAQ.

192.0.2.1 (NAT)      198.51.100.1
  init <-----IPsec-----> resp <--> Internet
10.0.5.0/24            10.0.5.1

Note: If you are trying to connect two networks together, consult the site-to-site ipsec guide.

Initiator and Responder Addressing

We start with two machines: init, which will initiate the connection to resp, which will respond. resp will be running OpenBSD; init can run any operating system that supports IPsec.

In this example, init sits behind a NAT, with IP address 192.0.2.1. resp has the public IP address 198.51.100.1. init will be dynamically assigned an IP address in the range 10.0.5.0/24 by resp, who itself will use the IP address 10.0.5.1 internally.

Authentication

iked can authenticate peers with RSA and ECDSA public keys, EAP MSCHAPv2, and X.509 certificates.

Exchanging Public Keys

Using RSA and ECDSA public keys is quick and simple if supported by init's operating system. Using these public keys bypasses the need for x.509 certificates.

In this example, we assume that init is also running OpenBSD, and you have root access on both init and resp. By default, an ECDSA public key is generated in /etc/iked/local.pub. We need to copy this public key to the peer's /etc/iked/pubkeys/ folder.

First, copy the public key from resp to init:

Note: init sits behind a NAT, so init must initiate the copying.

init$ ssh 198.51.100.1 'doas cat /etc/iked/local.pub' | doas tee /etc/iked/pubkeys/fqdn/resp.example.com

Replace resp.example.com with resp's fully qualified domain name.

Next, copy the public key from init to resp:

init$ cat /etc/iked/local.pub | ssh 198.51.100.1 'doas tee /etc/iked/pubkeys/fqdn/init.example.com'

init probably lacks a FQDN, so give it a unique name that can be used for the srcid.

If you lack shell or doas access on both servers, you may need to find some other method to transfer the public keys. Or, consider using another authentication method listed below.

Configure iked.conf:

init# cat /etc/iked.conf
gateway = "198.51.100.1"
srcname = "init.example.com"
destname = "resp.example.com"
pool = "10.0.5.0/24"
dns = "198.51.100.1"

ikev2 $destname active esp \
        from dynamic to any \
        peer $gateway \
        srcid $srcname dstid $destname \
        request address any \
        iface lo1

resp# cat /etc/iked.conf
gateway = "198.51.100.1"
hostname = "resp.example.com"
pool = "10.0.5.0/24"
dns = "198.51.100.1"

ikev2 $hostname passive esp \
	from any to dynamic \
	local $gateway peer any \
	srcid $hostname \
	config address $pool \
	config name-server $dns \
	tag "ROADW"

resp will allow flows from any to dynamic, meaning any IP address to a dynamically assigned IP lease from the address pool 10.0.5.0/24. The packets will be tagged with ROADW for easy filtering with pf?.

init will request address any, meaning it will request any IP address from resp. It allows flows from dynamic to any, that is, flows starting from the dynamically assigned IP from the pool (@10.0.5.0/24@@) to any IP address on the Internet.

Add a lo1 interface for init:

init# cat /etc/hostname.lo1
up

X.509 Certificate

Many operating systems require init use X.509 certificates to authenticate properly.

First, resp can create a Certificate Authority (CA) and export its certificate:

resp# ikectl ca resp.example.com create
resp# ikectl ca resp.example.com install
certificate for CA 'resp.example.com' installed into /etc/iked/ca/ca.crt
CRL for CA 'resp.example.com' installed to /etc/iked/crls/ca.crl
resp# ikectl ca resp.example.com certificate resp.example.com create
resp# ikectl ca resp.example.com certificate resp.example.com install
writing RSA key
resp# cp /etc/iked/ca/ca.crt /var/www/htdocs/
resp# chmod o+r /var/www/htdocs/ca.crt

You may need to change the destination /var/www/htdocs/ depending upon the htdocs folder set in httpd.conf(5) on resp. See the openhttpd guide.

On init's device, download the file from resp's webserver. Depending on the configuration, this may be https://resp.example.com/ca.crt. Import the CA certificate into the device. Consult the vpn guides for instructions for most common operating systems.

Next, we will use EAP with username/password. Configure iked.conf for resp:

resp# cat /etc/iked.conf
gateway = "198.51.100.1"
hostname = "resp.example.com"
pool = "10.0.5.0/24"
dns = "198.51.100.1"

ikev2 $hostname passive esp \
	from any to dynamic \
	local $gateway peer any \
	srcid $hostname \
	eap "mschap-v2" \
	config address $pool \
	config name-server $dns \
	tag "ROADW"

user 'username' 'password'

Replace username and password; replace 198.51.100.1 with resp's public IP address; and replace resp.example.com with its actual hostname (it must resolve to a valid IP address).

from any to dynamic allows any user to connect. $dns must provide the IP address for the name server that vpn clients will use. This example assumes you have a valid caching name server configured and listening on IP 198.51.100.1.

These packets will get tagged as ROADW for easy filtering with pf?.

X.509 certificate

resp# pkg_add zip
resp# ikectl ca vpn certificate client1.domain create
resp# cp /etc/ssl/vpn/client1.domain.crt /etc/iked/certs/
resp# ikectl ca vpn certificate client1.domain export
resp# tar -C /tmp -xzf client1.domain.tgz *pfx
resp# cp /tmp/export/client1.domain.pfx /var/www/htdocs/client1.domain.pfx

Configure resp

Add a vether(4) interface for resp:

resp# cat /etc/hostname.vether0
inet 10.0.5.1 0xffffff00

Edit sysctl.conf(5) to include these directives on resp:

resp# cat /etc/sysctl.conf
net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
net.inet.ipcomp.enable=1
net.inet.esp.enable=1
net.inet.ah.enable=1

resp# sysctl net.inet.ip.forwarding=1
resp# sysctl net.inet6.ip6.forwarding=1
resp# sysctl net.inet.ipcomp.enable=1
resp# sysctl net.inet.esp.enable=1
resp# sysctl net.inet.ah.enable=1

IP forwarding must be enabled for resp to forward the user's packets to its final destination. esp and ah are the protocols for ipsec(4).

[https://man.openbsd.org/iked.8|iked(8)]] depends upon packet filter being enabled. First, enable packet filter? on resp if it is turned off:

resp# pfctl -e

Add rules similar to the following for /etc/pf.conf?:

resp="198.51.100.1"
ext_if="vio0"
pass in on $ext_if proto udp to $resp port {isakmp, ipsec-nat-t} tag IKED
pass in on $ext_if proto esp to $resp tag IKED
pass on enc0 inet tagged ROADW
match out on $ext_if inet tagged ROADW nat-to $ext_if
match in quick on enc0 inet proto { tcp, udp } to port 53 rdr-to 127.0.0.1 port 53

Make sure to define the macro $ext_if with your external interface, such as vio0 for virtio(4).

Reload packet filter:

resp# pfctl -f /etc/pf.conf

Starting iked

iked(8) refuses to start if permissions are too loose:

init# chmod 0600 /etc/iked.conf

resp# chmod 0600 /etc/iked.conf

Start iked on resp (and init if init is also running OpenBSD):

init# iked -dv
...
spi=0x3d1dc86e07c0cb15: send IKE_SA_INIT req 0 peer 198.51.100.1:500 local 0.0.0.0:5
00, 518 bytes
spi=0x3d1dc86e07c0cb15: recv IKE_SA_INIT res 0 peer 198.51.100.1:500 local 192.0.2.1:500, 235 bytes, policy 'resp.example.com'
spi=0x3d1dc86e07c0cb15: send IKE_AUTH req 1 peer 198.51.100.1:500 local 192.0.2.1:500, 589 bytes
spi=0x3d1dc86e07c0cb15: recv IKE_AUTH res 1 peer 198.51.100.1:500 local 192.0.2.1:500, 464 bytes, policy 'resp.example.com'
spi=0x3d1dc86e07c0cb15: ikev2_ike_auth_recv: obtained lease: 10.0.5.117
spi=0x3d1dc86e07c0cb15: ikev2_ike_auth_recv: obtained DNS: 198.51.100.1
spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded SPIs: 0xc61f7d48, 0x11f53136 (enc aes-128-gcm esn)
spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded flows: ESP-10.0.5.117/32=0.0.0.0/0(0)
spi=0x3d1dc86e07c0cb15: established peer 198.51.100.1:500[FQDN/resp.example.com] local 192.0.2.1:500[FQDN/init.example.com] policy 'resp.example.com' as initiator (enc aes-128-gcm group curve25519 prf hmac-sha2-256)
resp# iked -dv
...
spi=0x3d1dc86e07c0cb15: recv IKE_SA_INIT req 0 peer 192.0.2.1:500 local 198.51.100.1:500, 518 bytes, policy 'resp.example.com'
spi=0x3d1dc86e07c0cb15: send IKE_SA_INIT res 0 peer 192.0.2.1:500 local 198.51.100.1:500, 235 bytes
spi=0x3d1dc86e07c0cb15: recv IKE_AUTH req 1 peer 192.0.2.1:500 local 198.51.100.1:500, 589 bytes, policy 'resp.example.com'
spi=0x3d1dc86e07c0cb15: assigned address 10.0.5.16 to FQDN/init.example.com
spi=0x3d1dc86e07c0cb15: send IKE_AUTH res 1 peer 192.0.2.1:500 local 198.51.100.1:500, 464 bytes
spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded SPIs: 0xc61f7d48, 0x11f53136 (enc aes-128-gcm esn)
spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded flows: ESP-0.0.0.0/0=10.0.5.16/32(0)
spi=0x3d1dc86e07c0cb15: established peer 192.0.2.1:500[FQDN/init.example.com] local 198.51.100.1:500[FQDN/resp.example.com] assigned 10.0.5.16 policy 'resp.example.com' as responder (enc aes-128-gcm group curve25519 prf hmac-sha2-256)

If you see IPsec flows and security associations (SAs) properly established like above, then you can enable and start iked with rcctl(8):

init# rcctl enable iked
init# rcctl start iked
iked(ok)

resp# rcctl enable iked
resp# rcctl start iked
iked(ok)

Note: Ignore commands for init if init is not running OpenBSD.

From init, ping an external IP address and confirm that all packets pass through resp's enc(4) interface:

init# ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=52 time=8.297 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=52 time=8.191 ms

resp# tcpdump -ne -i enc0
tcpdump: listening on enc0, link-type ENC
01:44:24.830185 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp: echo request (encap)
01:44:24.837528 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp: echo reply (encap)
01:44:25.830193 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp: echo request (encap)
01:44:25.837375 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp: echo reply (encap)

Confirm IPsec flows with ipsecctl and verify security associations with ikectl:

init# ipsecctl -sa
FLOWS:
flow esp in from 0.0.0.0/0 to 10.0.5.117 peer 198.51.100.1 srcid FQDN/init.example.com dstid FQDN/resp.example.com type require
flow esp out from 10.0.5.117 to 0.0.0.0/0 peer 198.51.100.1 srcid FQDN/init.example.com dstid FQDN/resp.example.com type require

SAD:
esp tunnel from 198.51.100.1 to 192.0.2.1 spi 0x24ce827e enc aes-128-gcm
esp tunnel from 192.0.2.1 to 198.51.100.1 spi 0x7c42210d enc aes-128-gcm

resp# ikectl show sa
iked_sas: 0xceb3d56a780 rspi 0x964bd34b809388a9 ispi 0x9b48ee25a0b56672 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[10.0.5.117] ESTABLISHED r udpecap nexti 0x0 pol 0xcebae8df000
  sa_childsas: 0xceb3d55e780 ESP 0x7c42210d in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xceb3d556c00 @0xceb3d56a780
  sa_childsas: 0xceb3d556c00 ESP 0x24ce827e out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xceb3d55e780 @0xceb3d56a780
  sa_flows: 0xceb3d571400 ESP out 0.0.0.0/0 -> 10.0.5.117/32 [0]@-1 (L) @0xceb3d56a780
  sa_flows: 0xceb3d57c800 ESP in 10.0.5.117/32 -> 0.0.0.0/0 [0]@-1 (L) @0xceb3d56a780
iked_activesas: 0xceb3d556c00 ESP 0x24ce827e out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xceb3d55e780 @0xceb3d56a780
iked_activesas: 0xceb3d55e780 ESP 0x7c42210d in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xceb3d556c00 @0xceb3d56a780
iked_flows: 0xceb3d57c800 ESP in 10.0.5.117/32 -> 0.0.0.0/0 [0]@-1 (L) @0xceb3d56a780
iked_flows: 0xceb3d571400 ESP out 0.0.0.0/0 -> 10.0.5.117/32 [0]@-1 (L) @0xceb3d56a780
iked_dstid_sas: 0xceb3d56a780 rspi 0x964bd34b809388a9 ispi 0x9b48ee25a0b56672 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[10.0.5.117] ESTABLISHED r udpecap nexti 0x0 pol 0xcebae8df000

From init, ping an external IP address and confirm that all packets pass through

 resp's enc0 interface:
init# ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=52 time=8.297 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=52 time=8.191 ms

resp# tcpdump -ne -i enc0
tcpdump: listening on enc0, link-type ENC
01:44:24.830185 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp
: echo request (encap)
01:44:24.837528 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp
: echo reply (encap)
01:44:25.830193 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp
: echo request (encap)
01:44:25.837375 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp
: echo reply (encap)

IPsec Statistics

init# netstat -s -p esp
esp:
        144 input ESP packets
        150 output ESP packets
        0 packets from unsupported protocol families
....
        0 raw ESP packets for encapsulating TDB received
        19976 input bytes
        20406 output bytes
init# netstat -s -p ah
ah:
        0 input AH packets
        0 output AH packets
        0 packets from unsupported protocol families
...
        0 output packets could not be sent
        0 input bytes
        0 output bytes

As expected, there are no AH packets being sent because the IPsec flows are configured to use ESP for confidentiality. For more information, see ipsec(4).

Configuring DNS

We will use unbound as the caching DNS resolver. Please make sure to read the unbound configuration guide before proceeding.

Change the following values in resp's /var/unbound/etc/unbound.conf:

outgoing-interface: 198.51.100.1
access-control: 10.0.0.0/8 allow

Make sure to enable and start unbound:

resp# rcctl enable unbound
resp# rcctl start unbound

You may want to configure domain blacklists to block unwanted traffic.

If init is running OpenBSD, you can verify if DNS lookup is working properly:

init# host ircnow.org 
ircnow.org has address 198.251.82.194
ircnow.org has IPv6 address 2605:6404:2d3::
ircnow.org mail is handled by 10 mail.ircnow.org.

resp# tcpdump -ne -i enc0 
tcpdump: listening on enc0, link-type ENC
01:25:54.119770 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.33324 > 8.8.8.8
.53: 63988+ A? ircnow.org.(28) (encap)
01:25:54.119882 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.33
324: 63988 1/0/0 A 198.251.82.194(44) (encap)
01:25:55.139849 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.37113 > 8.8.8.8
.53: 10591+ AAAA? ircnow.org.(28) (encap)
01:25:55.139942 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.37
113: 10591 1/0/0 AAAA 2605:6404:2d3::(56) (encap)
01:25:56.159754 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.1698 > 8.8.8.8.
53: 43204+ MX? ircnow.org.(28) (encap)
01:25:56.159846 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.16
98: 43204 1/0/0 MX mail.ircnow.org. 10(49) (encap)

Troubleshooting

Running iked in debug mode can provide valuable info about errors in configuration.

First, turn off iked if it is running:

# rcctl stop iked

Check to make sure no iked processes are running:

# ps ax | grep iked

Then, run iked in debug mode:

# iked -dv

-d will cause iked to not daemonize, and -v will report errors verbosely.