Site-to-site 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 two gateways to create a virtual tunnel across the Internet. A new virtual private network can then be created out of both of the networks connected to the gateways.

This setup allows us to provide a secure tunnel between two trusted networks across an untrusted Internet. In this guide, we assume that both devices are gateways for local area networks, both of them have public IP addresses, and at least one of them is running OpenBSD.

This is based on the VPN guide from the OpenBSD FAQ

            192.0.2.1           198.51.100.1
                  <-----IPsec-----> 
initlan <--> init                   resp <--> resplan
10.0.1.2    10.0.1.1              10.0.2.1    10.0.2.2

Note: If you are trying to tunnel traffic directly from end-user devices (like phones, desktops, or laptops) through a server to the Internet, you will instead want to use a road warrior configuration.

Connecting Two Gateways

We start with two servers: init, which will initiate the connection to resp, which will respond.

In this example, init has IP address 192.0.2.1 and resp has address 198.51.100.1. init and resp both act as gateways for their respective LANs (10.0.1.0/24 and 10.0.2.0/24). We will set up a site-to-site vpn so devices on either subnet appear to be in the same network.

Exchanging Public Keys

iked can authenticate peers with RSA and ECDSA public keys, EAP MSCHAPv2, and X.509 certificates. In this site-to-site configuration, we will use public keys because the transfer of keys is quick and simple, allowing us to skip the need for certificates.

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 init to resp:

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

Next, copy the public key from resp to init:

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

Replace init.example.com and resp.example.com with init's and resp's fully qualified domain names.

If you lack shell or doas access on both servers, you may need to find some other method to transfer the public keys.

Configuring iked.conf

Configure iked.conf(5) for both resp and init:

init# cat /etc/iked.conf
init="192.0.2.1"
resp="198.51.100.1"
initlan="10.0.1.0/24"
resplan="10.0.2.0/24"

ikev2 'init' active esp \
	from $initlan to $resplan \
	from $initlan to $resp \
	local $init peer $resp \
	srcid init.example.com

resp# cat /etc/iked.conf
init="192.0.2.1"
resp="198.51.100.1"
initlan="10.0.1.0/24"
resplan="10.0.2.0/24"

ikev2 'resp' passive esp \
        from $resplan to $initlan \
        from $resplan to $init \
        local $resp peer $init \
        srcid resp.example.com

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

init# chmod 0600 /etc/iked.conf

resp# chmod 0600 /etc/iked.conf

Appropriate sysctls should be enabled:

init# cat /etc/sysctl.conf
net.inet.ip.forwarding=1
net.inet.esp.enable=1
net.inet.ah.enable=1

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

(Both esp and ah sysctls are enabled by default, so they can normally be omitted)

resp will need to open the isakmp UDP port and possibly the ipsec-nat-t UDP port if the init is behind NAT. It also needs to allow the esp protocol. In /etc/pf.conf?, these rules may be needed:

init="192.0.2.1"
resp="198.51.100.1"
pass in log on $ext_if proto udp from $init to $resp port {isakmp, ipsec-nat-t} tag IKED
pass in log on $ext_if proto esp from $init to $resp tag IKED

Make sure to define a macro for ext_if to refer to the actual external interface.

Afterwards, reload packet filter:

resp# pfctl -f /etc/pf.conf

Running iked

First, we run iked in the foreground to check security associations (SAs) can be exchanged properly:

init# iked -dv
...
spi=0x30d3299fdb266d78: send IKE_SA_INIT req 0 peer 198.51.100.1:500 local 192.0.2.1:500, 518 bytes
spi=0x30d3299fdb266d78: recv IKE_SA_INIT res 0 peer 198.51.100.1:500 local 192.0.2.1:500, 235 bytes, policy 'init'
spi=0x30d3299fdb266d78: send IKE_AUTH req 1 peer 198.51.100.1:500 local 192.0.2.1:500, 491 bytes
spi=0x30d3299fdb266d78: recv IKE_AUTH res 1 peer 198.51.100.1:500 local 192.0.2.1:500, 369 bytes, policy 'init'
spi=0x30d3299fdb266d78: ikev2_childsa_enable: loaded SPIs: 0x6a275df5, 0x9cfd8ba5 (enc aes-128-gcm esn)
spi=0x30d3299fdb266d78: ikev2_childsa_enable: loaded flows: ESP-10.0.1.0/24=10.0.2.0/24(0), ESP-10.0.1.0/24=198.51.100.1/32(0)
spi=0x30d3299fdb266d78: established peer 198.51.100.1:500[FQDN/init.example.com] local 192.0.2.1:500[FQDN/resp.example.com] policy 'init' as initiator (enc aes-128-gcm group curve25519 prf hmac-sha2-256)

resp# iked -dv
...
spi=0x30d3299fdb266d78: recv IKE_SA_INIT req 0 peer 192.0.2.1:500 local 198.51.100.1:500, 518 bytes, policy 'resp'
spi=0x30d3299fdb266d78: send IKE_SA_INIT res 0 peer 192.0.2.1:500 local 198.51.100.1:500, 235 bytes
spi=0x30d3299fdb266d78: recv IKE_AUTH req 1 peer 192.0.2.1:500 local 198.51.100.1:500, 491 bytes, policy 'resp'
spi=0x30d3299fdb266d78: send IKE_AUTH res 1 peer 192.0.2.1:500 local 198.51.100.1:500, 369 bytes
spi=0x30d3299fdb266d78: ikev2_childsa_enable: loaded SPIs: 0x6a275df5, 0x9cfd8ba5 (enc aes-128-gcm esn)
spi=0x30d3299fdb266d78: ikev2_childsa_enable: loaded flows: ESP-10.0.2.0/24=10.0.1.0/24(0), ESP-10.0.2.0/24=192.0.2.1/32(0)
spi=0x30d3299fdb266d78: established peer 192.0.2.1:500[FQDN/resp.example.com] local 198.51.100.1:500[FQDN/init.example.com] policy 'resp' as responder (enc aes-128-gcm group curve25519 prf hmac-sha2-256)

If successful, you should see an IKE SA init request, followed by an IKE AUTH response, followed by an establishing connection to the peer.

If keys aren't set up properly, you may see error messages like below:

spi=0xc1a541e28fb2a7a3: ikev2_dispatch_cert: peer certificate is invalid
spi=0xc1a541e28fb2a7a3: ikev2_send_auth_failed: authentication failed for FQDN/init.example.com
...
spi=0xc1a541e28fb2a7a3: sa_free: authentication failed notification from peer

Once iked is confirmed to work, use rcctl on both servers:

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

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

tcpdump? can verify if the key exchange succeeded:

init# tcpdump -ne -i vio0
tcpdump: listening on vio0, link-type EN10MB                                        23:40:49.446068 e8:8b:11:11:11:11 fe:e1:ba:dc:40:69 0800 560: 192.0.2.1.500 > 198.51.100.1.500: isakmp v2.0 exchange IKE_SA_INIT                                                cookie: 9dc861be02b4775d->0000000000000000 msgid: 00000000 len: 518
23:40:49.451306 fe:e1:ba:dc:40:69 e8:8b:11:11:11:11 0800 277: 198.51.100.1.500 > 192.0.2.1.500: isakmp v2.0 exchange IKE_SA_INIT
        cookie: 9dc861be02b4775d->e477a8efd0aab8c3 msgid: 00000000 len: 235         23:40:49.455770 e8:8b:11:11:11:11 fe:e1:ba:dc:40:69 0800 581: 192.0.2.1.500 > 198.51.100.1.500: isakmp v2.0 exchange IKE_AUTH                                                   cookie: 9dc861be02b4775d->e477a8efd0aab8c3 msgid: 00000001 len: 539
23:40:49.463241 fe:e1:ba:dc:40:69 e8:8b:11:11:11:11 0800 459: 198.51.100.1.500 > 192.0.2.1.500: isakmp v2.0 exchange IKE_AUTH                                                   cookie: 9dc861be02b4775d->e477a8efd0aab8c3 msgid: 00000001 len: 417

ipsecctl shows the IPsec flows:

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

SAD:
esp tunnel from 192.0.2.1 to 198.51.100.1 spi 0xcb5c66a6 enc aes-128-gcm
esp tunnel from 198.51.100.1 to 192.0.2.1 spi 0xfcbab443 enc aes-128-gcm

ikectl shows the security associations:

resp# ikectl show sa
iked_sas: 0xe2401b35780 rspi 0xc0e414c2ee02f63a ispi 0x7556f8d937f9a032 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[] ESTABLISHED r nexti 0x0 pol 0xe240b59c000
  sa_childsas: 0xe2423e99900 ESP 0x7b7d374e in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xe2423e83600 @0xe2401b35780
  sa_childsas: 0xe2423e83600 ESP 0x8be2ef36 out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xe2423e99900 @0xe2401b35780
  sa_flows: 0xe2401b4b800 ESP out 10.0.2.0/24 -> 10.0.1.0/24 [0]@-1 (L) @0xe2401b35780
  sa_flows: 0xe2423e8fc00 ESP in 10.0.1.0/24 -> 10.0.2.0/24 [0]@-1 (L) @0xe2401b35780
  sa_flows: 0xe2401b3b800 ESP out 10.0.2.0/24 -> 192.0.2.1/32 [0]@-1 (L) @0xe2401b35780
  sa_flows: 0xe2401b15800 ESP in 192.0.2.1/32 -> 10.0.2.0/24 [0]@-1 (L) @0xe2401b35780
iked_activesas: 0xe2423e99900 ESP 0x7b7d374e in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xe2423e83600 @0xe2401b35780
iked_activesas: 0xe2423e83600 ESP 0x8be2ef36 out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xe2423e99900 @0xe2401b35780
iked_flows: 0xe2423e8fc00 ESP in 10.0.1.0/24 -> 10.0.2.0/24 [0]@-1 (L) @0xe2401b35780
iked_flows: 0xe2401b15800 ESP in 192.0.2.1/32 -> 10.0.2.0/24 [0]@-1 (L) @0xe2401b35780
iked_flows: 0xe2401b4b800 ESP out 10.0.2.0/24 -> 10.0.1.0/24 [0]@-1 (L) @0xe2401b35780
iked_flows: 0xe2401b3b800 ESP out 10.0.2.0/24 -> 192.0.2.1/32 [0]@-1 (L) @0xe2401b35780
iked_dstid_sas: 0xe2401b35780 rspi 0xc0e414c2ee02f63a ispi 0x7556f8d937f9a032 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[] ESTABLISHED r nexti 0x0 pol 0xe240b59c000

Testing

To test if the tunnel works, you should attempt to send from a device on one LAN to a device on another LAN. For example, on one of the machines in init's LAN, try sending ping from 10.0.1.2 to 10.0.2.2.

initlan$ ping -I 10.0.1.2 10.0.2.2
PING 10.0.2.2 (10.0.2.2): 56 data bytes
64 bytes from 10.0.2.2: icmp_seq=0 ttl=108 time=45.523 ms
64 bytes from 10.0.2.2: icmp_seq=1 ttl=108 time=45.304 ms

Run tcpdump? on an enc(4) interface to see packets prior to encapsulation and after decapsulation:

init# tcpdump -ne -i enc0
tcpdump: listening on enc0, link-type ENC
23:05:45.520270 (authentic,confidential): SPI 0xf997b9a0: 10.0.2.2 > 10.0.1.2: icmp: echo request (encap)
23:05:45.520825 (authentic,confidential): SPI 0xa484d765: 10.0.1.2 > 10.0.2.2: icmp: echo reply (encap)

If you lack devices to test with, please consult the site-to-site network lab guide for vmm.

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 gateways are using ESP for confidentiality. For more information, see ipsec(4).