Your own VPN with OpenIKED & OpenBSD

Well, it’s been quite some time since I blogged! I figure I’ll jump back in with a short ‘n sweet post.

Remote connectivity to your home network is something I think a lot of people find desirable. Over the years, I’ve just established an SSH tunnel and use it as a SOCKS proxy, sending my traffic through that. It’s a nice solution for a “poor man’s VPN”, but it can be a bit clunky, and it’s not great having to expose SSH to the world, even if you make sure to lock everything down 🤔

I set out the other day to finally do it properly. I’d come across this great post by Gordon Turner:

Whilst it was exactly what I was looking for, it outlined how to set up an L2TP VPN. Really, I wanted IKEv2 for performance and security reasons (I won’t elaborate on this here, if you’re curious about the differences, there’s a lot of content out on the web explaining this).

The client systems I’d be using have native support for IKEv2 (iOS, macOS, other BSD systems). But, I couldn’t find any tutorials in the same vein.

So, let’s get stuck in!


A quick note ✍️

This guide will walk through the set up of an IKEv2 VPN using OpenIKED on OpenBSD. It will detail a “road warrior” configuration, and use a PSK (pre-shared-key) for authentication. I’m sure it can be easily adapted to work on any other platforms that OpenIKED is available on, but keep in mind my steps are specifically for OpenBSD.

Server Configuration

As with all my home infrastructure, I crafted this set-up declaratively. So, I had the deployment of the VM setup in Terraform (deployed on my private Triton cluster), and wrote the configuration in Ansible, then tied them together using radekg/terraform-provisioner-ansible.

One of the reasons I love Ansible is that its syntax is very simplistic, yet expressive. As such, I feel it fits very well into explaining these steps with snippets of the playbook I wrote.

I’ll link the full playbook a bit further down for those interested.

sysctl parameters

First off, we need to alter the kernel state so it’s fit to manage VPN traffic. Naturally, the parameters we’re going to be setting are in the net.inet namespace:

- name: Ensure sysctl params are set
sysctl:
name: net.inet.{{item}}
value: 1
with_items:
- ip.forwarding
- esp.enable
- ah.enable
- ipcomp.enable

Hopefully this is clear enough. For those unfamiliar with Ansible, I’ll show what it looks like in the shell:

$ sysctl net.inet.ip.forwarding=1
$ sysctl net.inet.esp.enable=1
$ sysctl net.inet.ah.enable=1
$ sysctl net.inet.ipcomp.enable=1

These should also be persisted to /etc/sysctl.conf :

Note: this is already handled by the above Ansible task

net.inet.ip.forwarding=1
net.inet.esp.enable=1
net.inet.ah.enable=1
net.inet.ipcomp.enable=1

The naughty list (optional) 😈

If you’d like to maintain a list of those out there trying to constantly hit SSH on your network which you can use to outright block them, follow this step. We’ll use this in our pf configuration a bit further down.

- name: Ensure /etc/badguys exists
copy:
dest: /etc/badguys
owner: root
group: wheel
mode: 0640
force: false
content: ""

Configure the VPN network interface

You can use whatever subnet configuration you desire, here. But, if you choose to do something outside of 10.0.1.0/24 (used in these examples); you’ll need to make sure you substitute it elsewhere throughout the post.

- name: Ensure the enc0 interface is configured
copy:
dest: /etc/hostname.enc0
owner: root
group: wheel
mode: 0640
content: |
inet 10.0.1.1 255.255.255.0 10.0.1.255
up
notify: Reload network configuration

Note: the notify expression here is just running $ sh /etc/netstart

Configure the firewall

If you didn’t bother with the “naughty list” step earlier, leave out any lines containing “bad” or “badguys”.

Also, you’ll want to make sure you set the intra macro to the interface your system uses for network connectivity (presumably to the internet). Mine is vio0 , but yours may be something different. You can find this out using good old ifconfig.

- name: Ensure pf is configured
copy:
dest: /etc/pf.conf
owner: root
group: wheel
mode: 0644
content: |
intra = "vio0"
vpn = "enc0"
      set reassemble yes
set block-policy return
set loginterface egress
set skip on { lo, enc }
      match in all scrub (no-df random-id max-mss 1440)
      table <ossec_fwtable> persist
table <badguys> persist file "/etc/badguys"
      block in quick  on egress from <badguys> label "bad"
block out quick on egress to <badguys> label "bad"
block in quick on egress from <ossec_fwtable> label "bad"
block out quick on egress to <ossec_fwtable> label "bad"
block in quick from urpf-failed label uRPF
block return log
      pass out all modulate state
      pass in on egress proto { ah, esp }
pass in on egress proto udp to (egress) port { isakmp, ipsec-nat-t }
pass out on egress from 10.0.1.0/24 to any nat-to (egress)
pass out on $intra from 10.0.1.0/24 to $intra:network nat-to ($intra)
      pass in quick inet proto icmp icmp-type { echoreq, unreach }
      pass in on egress proto tcp from any to (egress) port 22 keep state (max-src-conn 40, max-src-conn-rate 10/30, overload <badguys> flush global)
pass in on $intra proto { udp tcp } from any to ($intra) port 53
notify: Reload pf configuration

Note: the notify expression here is just running $ pfctl -f /etc/pf.conf

Configure the iked service

For this step, you’ll need to have generated a secure pre-shared-key. Represented here in the contents of /etc/iked.conf as {{iked_psk}} (Ansible variable syntax).

This is the secret you’ll be using on your clients to connect. Make sure the string itself is secure and stored securely!

Personally; I’ve used Ansible Vault to store an encrypted variable in my Ansible configuration for deployment, and use pass to carry it around with me on my client devices.

- name: Ensure iked is configured
copy:
dest: /etc/iked.conf
owner: root
group: wheel
mode: 0600
content: |
ikev2 "inet" passive ipcomp esp \
from 0.0.0.0/0 to 10.0.1.0/24 \
from 10.0.0.0/24 to 10.0.1.0/24 \
local egress peer any \
psk "{{iked_psk}}" \
config protected-subnet 0.0.0.0/0 \
config address 10.0.1.0/24 \
config name-server 10.19.3.1 \
tag "IKED" tap enc0
notify: Reload iked service

Note: the notify expression here is just running $ rcctl reload iked

You may notice the name-server configuration here. This is the address of the DNS server you want clients to query when connected to the VPN. Yours is very likely a different address, so make sure you set this appropriately!

For more information on iked configuration, see: iked.conf(5)

Finally, start/enable the iked service ⏯

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

In the shell: $ rcctl start iked && rcctl enable iked

The Ansible playbook

For those of you that would like to deploy using Ansible: here’s the playbook I wrote. You’ll want to set your own PSK in the iked_psk variable. This could be plain text if you like, but I wouldn’t advise it. If you’d like to store encrypted variable like I have, you can encrypt your PSK using ansible-vault encrypt.


Gateway configuration 🚪

I’m only going to touch on this very briefly, as guidance on this is outside of the scope of this tutorial and will largely depend on your network configuration.

Simply put, you’ll need the following to get your shiny new VPN server on the front-line:

  1. Traffic targeted at your network destined for UDP ports 500, 4500, and 1701 needs to be able to reach your VPN server. This means you’ll need to permit UDP traffic on these ports through any firewalls you have, and you will likely need to forward the ports to the server.
  2. Depending on where you’re VPN’ing in from, you’ll likely need a public IP address (or DNS record pointing to such) to reach your network. Most residential ISPs where I’m from will either offer the option for a static IP address (usually at a small extra cost) or, although they may state its “dynamic” (DHCP) — it’ll likely be “sticky”. That is to say; the address leased by their DHCP server will have a very, very long TTL, and as such will stay the same for a very long time, if not indefinitely. It really depends on your ISP!

Client configuration

With this configuration in place, you can configure your client(s) to authenticate using your PSK. You can of course configure it differently, depending on your needs and the functionality available with OpenIKED, if you so desire.

iOS 📱

If you’re using a device running a recent version of iOS, it’s got IKEv2 support natively, and is very easy to configure:

  1. Open Settings then navigate to General > VPN
  2. Select Add VPN Configuration…
  3. Type should be IKEv2
  4. Description can be whatever you want
  5. Server should be the address (DNS or IP) to reach your server
  6. Remote ID should be the hostname of your server, unless you configured it to be something else. See the note at the bottom of the srcid parameter documentation in iked.conf(5) regarding its omission. If my server’s hostname (set in /etc/myname on OpenBSD) was vpn.my.net; this is what I’d use as Remote ID
  7. Local ID can be left blank
  8. Unless configured otherwise, set User Authentication to None then select < Back at the top
  9. Toggle off Use Certificate
  10. This should show the Secret field. Here’s where you put your PSK

It should look roughly something like this:

You should now have a VPN menu item in your main Settings screen, just under Personal Hotspot. You can toggle your VPN from here. Fingers crossed it works! 🤞

macOS 💻

The configuration is largely the same, so I won’t repeat the steps. To add a VPN configuration on macOS open Settings > Network. Use the little ‘+’ symbol in the bottom left of the network list to add a new network and select VPN. The VPN Type should default to IKEv2, if not — make sure you set it as that. Give it a name, and follow the same steps as iOS for the configuration. You should be all set!


Troubleshooting 🙄

If you run into problems connecting once you’ve got everything configured, there are a few things you can check over.

Is the service running? 🏃🏻‍

Firstly, make sure the iked service is actually up and running. You can check this with $ rcctl check iked

Network connectivity 🔛

One of the first things to always check for with connectivity problems to a service you control is to make sure the network traffic can actually reach its destination. Personally, I’d follow something along these lines:

  1. Stop the iked service
  2. Now that the service is stopped, you can start a netcat process listening on the relevant ports. Check each one as you go. Let’s start with UDP port 500: $ nc -u -l 500 👈 this should start a netcat process listening to UDP port 500, bound to the address 0.0.0.0 (all interfaces).
  3. On another system, outside your network (presumably with internet connectivity), check to see if you can reach that port and see if your traffic is reaching the server: $ nc -vuz your.server.addr 500
  4. Due to the nature of UDP, netcat will always report it was successful on the client machine. But, if your netcat process on your server has spat an X or a few X’s into stdout, then traffic is reaching your server. If you don’t see anything, traffic is not making it through to your server for some reason.

Debugging iked 🐛

If you’ve confirmed that traffic can make it through to your server, the next step would be to start inspecting iked’s behaviour. With the iked service still stopped, start it in the foreground with the -d flag, and some -v flags for verbosity (the more v’s the more verbosity)

Watching traffic 🕵🏻‍

You can use trusty tcpdump to inspect the traffic of an interface. In our case, both enc0 and pflog0 are of interest:

$ tcpdump -ni enc0 or $ tcpdump -ni pflog0

The first will watch your enc0 interface which iked should be using. The second will show you realtime logs from pf .

Don’t forget! 🤔

Once you’re done with debugging, make sure you turn any services back on!


Happy Hacking!

I’m really happy with this setup, having easy, secure access to my home network wherever I am is really nice. Hopefully you’ve enjoyed reading and have your own OpenIKED server running, or at least found the insights useful!