OpenBSD iked and sec interfaces

Posted on Dec 26, 2023
tl;dr: copy pub keys around and magic happens. needs OpenBSD 7.4 & above

Setting up sec interfaces with iked

This article is mostly around how to get IKEv2 setup on OpenBSD. It took me a while, as I was ‘windmilling in’ and working on it without correctly sitting down and thinking what I wanted to achieve.

On OpenBSD, the iked daemon configures and handles all this stuff. It’s nice enough to generate you some rsa keys on startup, so that saves you a trip to the hell that is the openssl command.

As is usual, this is all now built by ansible. I’ll drop in the ansible jinga templates as they are possibly easier to understand, and the blocks of ansible code. There’s nothing to stop you simply doing this manually.

We’ll be using the new sec interfaces, which come with OpenBSD 7.4

Architecture

For our architecture, we have 3 nodes. Two are remotely hosted in a cloud provider, one is in our home/office.

┌──────────────┐         ┌──────────────┐
│              │         │              │
│ reachout     │         │ outreach     │
└──────▲───────┘         └──────▲───────┘
       │                        │
       │                        │
       │                        │
       │                        │
       │                        │
       │                        │
       │                        │
       │   ┌───────────────┐    │
       └───┤               ├────┘
           │    fw0        │
           └───────────────┘

We’re going to start with the configuration that we lay down on the reachout host, and then the resulting config from both reachout and fw0 hosts. The outreach node we’ll consider out of scope - this is configured in almost the same way, however we also create a secondary connection between reachout <–> outreach.

sec interfaces

So to start with, let’s set up some sec interfaces, using ansible and a template.

# ansible role -> task -> main.yaml
- name: OCI - OpenBSD - create /etc/hostname.sec0
  ansible.builtin.template:
    src: templates/oci_sec0.conf
    dest: /etc/hostname.sec0
    mode: '0640'
  notify: OCI - start sec0 network interface
# ansible role -> handler -> main.yaml
- name: OCI - start sec0 network interface
  ansible.builtin.shell: /bin/sh /etc/netstart sec0

and here’s the referenced template:

# sourced from templates/oci_sec0.conf
inet {{ sec0_my_addr }} {{ sec0_netmask }} {{ sec0_remote_addr }}
inet6 fe80::bbbb%sec0
inet6 {{ sec0_my_ip6_addr }} 
up

These variables are defined via the ‘host_vars’, for example:

# host_vars/reachout.dev.kaizo.org
sec0_my_addr: 100.64.0.1
sec0_remote_addr: 100.64.0.2
sec0_netmask: 255.255.255.252
sec0_my_ip6_addr: fd1f:abc9:4ab:a000::1/64

You’ll notice that theres a few `extra’ ipv6 addresses in here - in a future post, we’ll cover why we’ve got them in there. For now, either ignore them, or simply leave them.

If you were to run this via ansible, you’ll end up with a file which looks like this:

# /etc/hostname.sec0
inet 100.64.0.1 255.255.255.252 100.64.0.2
inet6 fe80::bbbb%sec0
inet6 fd1f:abc9:4ab:a000::1/64 
up

and a corresponding mirror configuration on the fw0 host, which looks like this:

# /etc/hostname.sec0
inet 100.64.0.2 255.255.255.252 100.64.0.1
inet6 fe80::aaaa
inet6 fd1f:abc9:4ab:a000::2/64
up

The corresponding devices comes up like this:

sec0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280
        index 5 priority 0 llprio 3
        groups: sec
        inet 100.64.0.1 --> 100.64.0.2 netmask 0xfffffffc
        inet6 fe80::17ff:fe00:f193%sec0 -->  prefixlen 64 scopeid 0x5
        inet6 fe80::bbbb%sec0 -->  prefixlen 64 scopeid 0x5
        inet6 fd1f:abc9:4ab:a000::1 -->  prefixlen 64

However, all we have here is some defined sec interfaces - we’re not actually using them. Next, let’s configure iked.

iked

This is where we’ll actually get some connectivity up and running. Again, using ansible, we’ll lay down the populated template:

- name: OCI - OpenBSD - create /etc/iked.conf
  ansible.builtin.template:
    src: templates/oci_iked.conf
    dest: /etc/iked.conf
    mode: '0600'
  notify: OCI - restart iked

and the corresponding template file.

ikev2 "home-sec" tunnel esp  \
        from {{ sec0_my_addr }} to {{ sec0_remote_addr }} \
        local egress peer any \
        ikesa enc aes-256-gcm prf hmac-sha2-384 \
        group ecp384 childsa enc aes-256-gcm group ecp384 \
        srcid {{ ansible_host }} \
        iface sec0 \
        tag "HOME"

Again, when we run this via ansible we will end up with the populated file on disk. Here’s the one from reachout:

ikev2 "home-sec" tunnel esp  \
        from 100.64.0.1 to 100.64.0.2 \
        local egress peer any \
        ikesa enc aes-256-gcm prf hmac-sha2-384 \
        group ecp384 childsa enc aes-256-gcm group ecp384 \
        srcid reachout.dev.kaizo.org \
        iface sec0 \
        tag "HOME"

and here’s the configuration from the fw0 host. At this point, it’s worth mentioning that the initiator in this will be fw0, which is a host on DHCP via an ISP. This configuration was manually created, with a hardcoded IP address of reachout's externally accessible IP address. Pay particular attention to the differences in this configuratione file:

  • line#1, we include the work ‘active’ to indicate this node should initiate a connection
  • line#2, we specify a flow, but this is unnecessary
  • line#3, we explicitly state the source interface to connect from
  • line#4, we define the IP address of the target
  • line#7, we state who we are - this is important later.
ikev2 "reachout" active tunnel esp \
        from 100.64.0.2 to 100.64.0.1 \
        local pppoe0 \
        peer 111.222.333.444 \
        ikesa enc aes-256-gcm prf hmac-sha2-384 \
        group ecp384 childsa enc aes-256-gcm group ecp384 \
        srcid fw0.kaizo.lan \
        dstid reachout.dev.kaizo.org \
        iface sec0

Lastly, we’ll bring up the iked process on the reachout host:

- name: Ensure iked service is running/enabled
  ansible.builtin.service:
    name: iked
    state: started
    enabled: true

At this point, assuming no errors, the remote tunnel endpoint will start.

Authentication

It would be nice if we could perhaps have some form of authentication. As mentioned previously, when iked starts it creates some RSA keys. These are much easier to manage than certificates, provide the same level of protection, and thankfully don’t expire. They live in the file /etc/iked/local.pub are just pem encoded files.

You can now copy and paste the keys onto each of the hosts - they go into a logical place. If you remember when we created our configs, we had the following two lines:

        srcid fw0.kaizo.lan \
        dstid reachout.dev.kaizo.org
  • The key from reachout:/etc/iked/local.pub needs to be placed into fw0:/etc/iked/pubkeys/fqdn/reachout.dev.kaizo.org
  • The corresponding key from fw0:/etc/iked/local.pub needs to be placed into reachout:/etc/iked/pubkeys/fqdn/fw0.kaizo.lan

These hosts don’t need to be dns resolvable - the line srcid identifies to the remote endpoint which key it needs to look for.

At this point, you should be able to start iked. Assuming the service is already started (via the ansible role) on the remote host, run the iked daemon from a command line. As is usual (and awesome!) on OpenBSD, the flag -d runs it in the foreground, and -v shows us more of whats going on. I’ve left the complete architecture of all 3 nodes below, note the second host is using a different (sec1) interface, but is configured in the same way.

# iked -dv
fw0# iked -dv  
ikev2 "reachout" active tunnel esp inet from 100.64.0.2 to 100.64.0.1 local 1.1.1.1 peer 2.2.2.2 ikesa enc aes-256-gcm prf hmac-sha2-384 group ecp384 childsa enc aes-256-gcm group ecp384 esn noesn srcid fw0.kaizo.lan dstid reachout.dev.kaizo.org lifetime 10800 bytes 4294967296 signature iface sec0
ikev2 "outreach" active tunnel esp inet from 100.64.1.2 to 100.64.1.1 local 1.1.1.1 peer 3.3.3.3 ikesa enc aes-256-gcm prf hmac-sha2-384 group ecp384 childsa enc aes-256-gcm group ecp384 esn noesn srcid fw0.kaizo.lan dstid outreach.dev.kaizo.org lifetime 10800 bytes 4294967296 signature iface sec1
ikev2_init_ike_sa: initiating "reachout"
spi=0x63fbcad4ccf983cc: send IKE_SA_INIT req 0 peer 2.2.2.2:500 local 1.1.1.1:500, 294 bytes
ikev2_init_ike_sa: initiating "outreach"
spi=0x01b4e495a3b94b24: send IKE_SA_INIT req 0 peer 3.3.3.3:500 local 1.1.1.1:500, 294 bytes
spi=0x63fbcad4ccf983cc: recv IKE_SA_INIT res 0 peer 2.2.2.2:500 local 1.1.1.1:500, 299 bytes, policy 'reachout'
spi=0x01b4e495a3b94b24: recv IKE_SA_INIT res 0 peer 3.3.3.3:500 local 1.1.1.1:500, 299 bytes, policy 'outreach'
spi=0x63fbcad4ccf983cc: send IKE_AUTH req 1 peer 2.2.2.2:4500 local 1.1.1.1:4500, 760 bytes, NAT-T
spi=0x01b4e495a3b94b24: send IKE_AUTH req 1 peer 3.3.3.3:4500 local 1.1.1.1:4500, 760 bytes, NAT-T
spi=0x63fbcad4ccf983cc: recv IKE_AUTH res 1 peer 2.2.2.2:4500 local 1.1.1.1:4500, 726 bytes, policy 'reachout'
spi=0x63fbcad4ccf983cc: ikev2_childsa_enable: loaded SPIs: 0xb55b4093, 0xe6fa4e44 (enc aes-256-gcm esn)
spi=0x63fbcad4ccf983cc: established peer 2.2.2.2:4500[FQDN/reachout.dev.kaizo.org] local 1.1.1.1:4500[FQDN/fw0.kaizo.lan] policy 'reachout' as initiator (enc aes-256-gcm group ecp384 prf hmac-sha2-384)
spi=0x01b4e495a3b94b24: recv IKE_AUTH res 1 peer 3.3.3.3:4500 local 1.1.1.1:4500, 726 bytes, policy 'outreach'
spi=0x01b4e495a3b94b24: ikev2_childsa_enable: loaded SPIs: 0x8dda75e2, 0x791420e7 (enc aes-256-gcm esn)
spi=0x01b4e495a3b94b24: established peer 3.3.3.3:4500[FQDN/outreach.dev.kaizo.org] local 1.1.1.1:4500[FQDN/fw0.kaizo.lan] policy 'outreach' as initiator (enc aes-256-gcm group ecp384 prf hmac-sha2-384)
spi=0x4f4ba00a1c4b5adb: recv INFORMATIONAL req 461 peer 2.2.2.2:4500 local 1.1.1.1:4500, 61 bytes, policy 'reachout'
spi=0x4f4ba00a1c4b5adb: recv INFORMATIONAL req 461 peer 2.2.2.2:4500 local 1.1.1.1:4500, 61 bytes, policy 'reachout'
spi=0x9953205faea445f9: recv INFORMATIONAL req 690 peer 3.3.3.3:4500 local 1.1.1.1:4500, 61 bytes, policy 'outreach'
spi=0x9953205faea445f9: recv INFORMATIONAL req 690 peer 3.3.3.3:4500 local 1.1.1.1:4500, 61 bytes, policy 'outreach'
spi=0x9953205faea445f9: recv INFORMATIONAL req 690 peer 3.3.3.3:4500 local 1.1.1.1:4500, 61 bytes, policy 'outreach'

At this point, you should be able to ping the remote IP address of the tunnel, i.e. you should be able to ping 100.64.0.1 and 100.64.0.2 from both of those hosts.

We can see the flows via the ipsecctl command:

zsh 565 % sudo ipsecctl -sa   
FLOWS:
No flows

SAD:
esp tunnel from 1.1.1.1 to 2.2.2.2 spi 0xb55b4093 enc aes-256-gcm
esp tunnel from 2.2.2.2 to 1.1.1.1 spi 0xe6fa4e44 enc aes-256-gcm

Adding -v to the above command gives us more information:

zsh 566 % sudo ipsecctl -sa -v 
FLOWS:
No flows

SAD:
esp tunnel from 1.1.1.1 to 2.2.2.2 spi 0xb55b4093 enc aes-256-gcm
        sa: spi 0xb55b4093 auth gmac-aes-256 enc aes-gcm
                state mature replay 64 flags 0x604<tunnel,udpencap,esn>
        lifetime_cur: alloc 0 bytes 133360 add 1703583246 first 1703583248
        lifetime_hard: alloc 0 bytes 4294967296 add 10800 first 0
        lifetime_soft: alloc 0 bytes 3985729650 add 10022 first 0
        address_src: 2.2.2.2
        address_dst: 1.1.1.1
        identity_src: type fqdn id 0: FQDN/fw0.kaizo.lan
        identity_dst: type fqdn id 0: FQDN/reachout.dev.kaizo.org
        udpencap: udpencap port 4500
        lifetime_lastuse: alloc 0 bytes 0 add 0 first 1703585360
        tag: HOME
        counter: 
                1384 input packets
                338688 input bytes
                155433 input bytes, decompressed
                1384 packets dropped on input

        replay: rpl 1384
        interface: sec0 direction in
esp tunnel from 1.1.1.1 to 2.2.2.2 spi 0xe6fa4e44 enc aes-256-gcm
        sa: spi 0xe6fa4e44 auth gmac-aes-256 enc aes-gcm
                state mature replay 64 flags 0x604<tunnel,udpencap,esn>
        lifetime_cur: alloc 0 bytes 148570 add 1703583246 first 1703583249
        lifetime_hard: alloc 0 bytes 4294967296 add 10800 first 0
        lifetime_soft: alloc 0 bytes 3672197038 add 9234 first 0
        address_src: 1.1.1.1
        address_dst: 2.2.2.2
        identity_src: type fqdn id 0: FQDN/reachout.dev.kaizo.org
        identity_dst: type fqdn id 0: FQDN/fw0.kaizo.lan
        udpencap: udpencap port 4500
        lifetime_lastuse: alloc 0 bytes 0 add 0 first 1703585360
        tag: HOME
        counter: 
                1388 output packets
                237428 output bytes
                176330 output bytes, uncompressed

        replay: rpl 1389
        interface: sec0 direction out

However, although we have tunnels up now, we don’t have any routing set up for the other networks that exist connected to the endpoints. For that, you can use some static routes, but we’ll configure ospfd and ospf6d. We’ll cover that in the next article.

references

CFT sec(4) for Route Based IPSec VPNs