Although the enterprise WAN connectivity market is increasingly focused on SD-WAN nowadays, there are still plenty of scenarios which call for more standards-based VPN technologies. In particular, inter-organisational connectivity (such as between partner companies).

Not only does the proprietary nature of the SD-WAN solutions available today mean they are unsuitable for such environments, but their architecture generally involves a single central controller / point of management. Admittedly I don’t have much direct experience of SD-WAN at this point, but I would be very surprised if the permissions model in such a design is capable of catering for environments in which no single person can have admin rights to the entire deployment, making them unsuitable for spanning multiple autonomous systems - where each AS wants full control over their own network.

For as long as I can remember, the de-facto standard VPN technology has been IPSec. In particular, for a site-to-site scenario that means IPSec tunnel mode in conjunction with IKEv1. As such, it is the only technology that can be relied upon to be supported by both parties in an inter-AS VPN.

In recent years IKEv2 has superseded IKEv1, and become sufficiently commonplace to be usable in the vast majority of scenarios. Having said that, OpenBSD does have a frustrating limitation whereby you can only run one or other of isakmpd (IKEv1) or iked (IKEv2) at a time, making it essentially impossible to migrate an OpenBSD which handles numerous IPSec VPNs to other organisations from IKEv1 to IKEv2.

This post will cover establishing an IKEv2 VPN between OpenBSD and Cisco IOS. But rather than IPSec tunnel mode, this example shows GRE-over-IPSec. Although it can’t be relied upon to the same extent to be supported by any arbitrary VPN router, GRE-over-IPSec does have fairly broad support, and offers significantly more flexibility in terms of the traffic that it can carry.

Scenario

The configurations below have proven to work well for me for over a year now (I use it for a VPN between my home network (the OpenBSD end) and my Mum’s home network (the Cisco end). In case you’re wondering, unsurprisingly I set up my Mum’s home network… she has no idea how any of this works, but FWIW the Cisco router she’s using is a C887VA-WD-E-K9, and I’m currently running OpenBSD 7.0.

OpenBSD

As always, OpenBSD’s documentation is excellent, so for the definitive information on how to configure iked I could simply say “RTFM” (or at least, “man iked.conf”). But I’ll provide my example configuration (/etc/iked.conf) below:

ikev2 quick active transport esp inet proto gre from <openbsd_ip> to <cisco_ip> local \
<openbsd_ip> peer <cisco_ip> ikesa enc aes-256-gcm prf hmac-sha2-384 \
group ecp384 childsa enc aes-256-gcm group ecp384 srcid <openbsd_ip> dstid <cisco_ip> \
ikelifetime 86400 lifetime 3600 bytes 524288000 psk "ThisShouldBeAStrongPassword"

In this scenario, both VPN routers have static IPs and either side can initiate the tunnel (thus the “active” keyword). The only traffic directly encapsulated by IPSec will be GRE between the two VPN endpoint IPs themselves, so the traffic selector explicitly specifies “gre” as the protocol. This is important because the configuration we use for Cisco IOS does the same thing implicitly, and it needs to match on OpenBSD in order for the VPN to function. I have selected some strong ciphers that are supported by both devices, and am using a pre-shared-key for authentication (other forms of authentication are available, but I selected PSK for simplicity).

With the configuration in place, enabling and starting iked is simple:

rcctl enable iked
rcctl start iked

Now we need to configure the GRE tunnel. That involves defining the interface via the /etc/hostname.gre0 configuration file:

inet <openbsd_tunnel_ip> 255.255.255.252 <cisco_tunnel_ip>
inet6 <openbsd_tunnel_ipv6> 127
tunnel <openbsd_ip> <cisco_ip>
mtu 1442

Assuming PF is enabled, the following rules in /etc/pf.conf permit establishment of the VPN and set the TCP MSS values appropriately given the tunnel interface’s MTU:

# TCP MSS
match on { gre0 } inet scrub (max-mss 1422)
match on { gre0 } inet6 scrub (max-mss 1402)
# Permit IKEv2 / IPSec
pass out quick on { egress } proto { udp } from { egress } to { $cisco_ip } port { \
    500, 4500 } user { _iked }
pass in quick on { egress } proto { udp } from { $cisco_ip } to { egress } port { \
    500, 4500 }
pass out quick on { egress } proto { esp } from { egress } to { $cisco_ip }
pass in quick on { egress } proto { esp } from { $cisco_ip } to { egress }
pass out quick on { enc0 } proto { ipencap } from { egress } to { $cisco_ip } keep \
    state (if-bound)
pass in quick on { enc0 } proto { ipencap } from { $cisco_ip } to { egress } keep \
    state (if-bound)
pass out quick on { enc0 } proto { gre } from { egress } to { $cisco_ip }
pass in quick on { enc0 } proto { gre } from { $cisco_ip } to { egress }

PF rules to permit transit traffic through the VPN aren’t covered above, but they simply need to be defined such that they operate on the gre0 interface.

It is also necessary to enable IP forwarding, and GRE via sysctl, as those things are disabled by default. Here’s the appropriate configuration to be added to /etc/sysctl.conf:

net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
net.inet.gre.allow=1

To make those changes live without a reboot, also run the following commands:

sysctl net.inet.ip.forwarding=1
sysctl net.inet6.ip6.forwarding=1
sysctl net.inet.gre.allow=1

The gre0 interface can then be brought up, and the PF ruleset reloaded, by running:

sh /etc/netstart gre0
pfctl -f /etc/pf.conf

Cisco IOS

In comparison, the equivalent IOS configuration is significantly lengthier:

crypto ikev2 proposal VPN-IKEv2-Proposal
 encryption aes-gcm-256
 prf sha384
 group 20
!
crypto ikev2 policy VPN-IKEv2-Policy
 proposal VPN-IKEv2-Proposal
!
crypto ikev2 keyring VPN-IKEv2-Keyring
 peer openbsd
  address <openbsd_ip>
  pre-shared-key local ThisShouldBeAStrongPassword
  pre-shared-key remote ThisShouldBeAStrongPassword
 !
!
crypto ikev2 profile VPN-IKEv2-Profile
 match identity remote address <openbsd_ip> 255.255.255.255
 authentication remote pre-share
 authentication local pre-share
 keyring local VPN-IKEv2-Keyring
 dpd 60 10 on-demand
!
crypto isakmp aggressive-mode disable
!
crypto ipsec security-association replay window-size 1024
!
crypto ipsec transform-set ESP-AESGCM-256 esp-gcm 256
 mode transport
crypto ipsec fragmentation after-encryption
!
crypto ipsec profile VPN-IKEv2-IPsec-Profile
 set security-association lifetime kilobytes 524288
 set transform-set ESP-AESGCM-256
 set pfs group20
 set ikev2-profile VPN-IKEv2-Profile
!
interface Tunnel0
 ip address <cisco_tunnel_ip> 255.255.255.252
 ip tcp adjust-mss 1402
 ipv6 address <cisco_tunnel_ip_ipv6>/127
 ipv6 tcp adjust-mss 1382
 tunnel source <cisco_ip>
 tunnel destination <openbsd_ip>
 tunnel path-mtu-discovery
 tunnel protection ipsec profile VPN-IKEv2-IPsec-Profile

A few things are worth pointing out:

  • Similarly IOS also supports having different PSKs in each direction, but OpenBSD does not, so we have to stick with just a single PSK
  • I recently discovered that aggressive mode is enabled by default in IOS, and needs to be explicitly disabled
  • I believe the various MTU and MSS values in the configurations above should be accurate for the chosen encapsulations (assuming a 1500 Byte MTU over the internet for the transport network)

As far as ACL entries to permit the VPN-related traffic are concerned, IOS doesn’t require rules to permit the GRE traffic through the IPSec tunnel, however the following are still needed for inbound & outbound ACLs on the router’s physical interface (if defined):

ip access-list extended Inbound
 permit udp host <openbsd_ip> host <cisco_ip> eq isakmp
 permit udp host <openbsd_ip> host <cisco_ip> eq non500-isakmp
 permit esp host <openbsd_ip> host <cisco_ip>
!
ip access-list extended Outbound
 permit udp host <cisco_ip> host <openbsd_ip> eq isakmp
 permit udp host <cisco_ip> host <openbsd_ip> eq non500-isakmp
 permit esp host <cisco_ip> host <openbsd_ip>

ACLs to restrict transit traffic flowing through VPN should be applied to the Tunnel0 interface. Unlike OpenBSD (where the default behaviour is for traffic to be blocked if PF is enabled), IOS will permit traffic over the VPN by default unless ACLs are defined and applied.

Initially I tried enabling GRE keepalives, but failed to get them to work. Later I discovered they are unsupported in conjunction with tunnel protection in IOS, as per https://www.cisco.com/c/en/us/support/docs/ip/generic-routing-encapsulation-gre/118370-technote-gre-00.html.

Routing

Now that the VPN itself is established, as it is a point-to-point GRE tunnel we can enable and configure any standard routing protocols (or just static routes) to direct traffic across the tunnel, rather than having to mess around with IPSec encryption domains as would be true for native IPSec tunnelling.