VPN and Cryptographic Tunneling

VPN and Cryptographic Tunneling

Synopsis #

This chapter covers site-to-site virtual private networks (VPNs) and host-to-host cryptographic tunnels on OpenBSD. It focuses on IKEv2 using the base system iked(8) and the kernel IPsec stack ipsec(4) , with practical patterns for NAT traversal, selector design, and observability. It also shows a minimal pattern for WireGuard-style tunnels with the in-kernel interface wg(4) . Runtime management uses ikectl(8) , packet filtering is handled by pf.conf(5) and pfctl(8) , and link inspection uses tcpdump(8) . The encrypted interface for IPsec is enc(4) .

Use these patterns to interconnect sites over untrusted networks, to publish internal services across providers securely, or to replace legacy tunnels with modern cryptography.

Design Considerations #

  • Selectors and scope. Define traffic by networks (for example, 10.0.0.0/2410.20.0.0/24) rather than any. Keep selectors minimal and symmetric on both peers.
  • Authentication. Pre-shared key (PSK) is simplest for site-to-site. Certificates scale better and are preferred where third-party trust or revocation is needed.
  • NAT traversal. Permit UDP ports 500 and 4500 and protocol ESP on WAN. IKEv2 NAT-T is automatic when address translation is detected.
  • Routing. For routed sites, enable IPv4/IPv6 forwarding and add static routes (or dynamic routing inside the tunnel).
  • Performance. Prefer AES-GCM proposals to offload integrity to the cipher. Keep MTU and MSS in mind where encapsulation crosses constrained links.
  • Operations. Ensure time sync on all peers (for example, with ntpd(8) ). Log IKE and PF events during rollout.

Configuration #

The examples assume:

  • Site A WAN: 198.51.100.10, LAN: 10.0.0.0/24
  • Site B WAN: 203.0.113.10, LAN: 10.20.0.0/24

Adjust interface names and addresses to match your environment.

Baseline system settings (both sites) #

# printf '%s\n' \
    'net.inet.ip.forwarding=1' \
    'net.inet6.ip6.forwarding=1' \
    >> /etc/sysctl.conf
  # Enable L3 forwarding for routed traffic through the firewall/router

# sysctl net.inet.ip.forwarding=1 net.inet6.ip6.forwarding=1
  # Apply immediately

PF allowances for IKEv2 and ESP (both sites) #

Permit IKE (UDP 500/4500) and ESP on the WAN, and allow traffic between the protected subnets on the inside. Syntax is defined in pf.conf(5) .

## /etc/pf.conf — minimal allowances for IKEv2/IPsec

set skip on lo

wan     = "em0"           # adjust
lan_net = "{ 10.0.0.0/24, 10.20.0.0/24 }"

block all

# IKE and IPsec on the WAN
pass in on $wan proto udp to ($wan) port { 500, 4500 } keep state
pass in on $wan proto esp keep state
pass out on $wan proto { udp, esp } keep state

# Permit protected subnets after decryption
pass on enc0 from 10.0.0.0/24 to 10.20.0.0/24 keep state
pass on enc0 from 10.20.0.0/24 to 10.0.0.0/24 keep state

Reload and confirm with pfctl(8) .

# pfctl -f /etc/pf.conf
# pfctl -sr | egrep 'udp .* (500|4500)|proto esp|enc0'

Site-to-site IKEv2 with PSK #

Configuration is in iked.conf(5) . One side initiates (active), the other listens (passive). Proposals below use AES-GCM.

Site A (initiator) #

## /etc/iked.conf — Site A

ikev2 "s2s-a2b" active esp from 10.0.0.0/24 to 10.20.0.0/24 \
    peer 203.0.113.10 \
    ikesa enc aes-256-gcm prf sha256 group 14 \
    childsa enc aes-256-gcm \
    psk "change-this-shared-secret"

Site B (responder) #

## /etc/iked.conf — Site B

ikev2 "s2s-b2a" passive esp from 10.20.0.0/24 to 10.0.0.0/24 \
    ikesa enc aes-256-gcm prf sha256 group 14 \
    childsa enc aes-256-gcm \
    psk "change-this-shared-secret"

Enable and start iked(8) on both peers with rcctl(8) ):

# rcctl enable iked
  # Start at boot
# rcctl start iked
  # Launch now (initiator will dial the responder)

If either peer is behind NAT, IKEv2 will encapsulate ESP in UDP 4500 automatically (NAT-T). Ensure the upstream allows those ports.

Minimal WireGuard-style tunnel with wg(4) #

The wg(4) interface provides a compact, static-key tunnel. Keys are Curve25519; generate them per the wg(4) documentation and place the private key on each host (for example, /etc/wg/private.key). Interface attributes are set with ifconfig(8) .

Example: point-to-point /30 between hosts with inside addresses 10.99.0.1/30 (Site A) and 10.99.0.2/30 (Site B), UDP port 51820.

Site A #

# ifconfig wg0 create
  # Create the interface
# ifconfig wg0 inet 10.99.0.1/30
  # Assign inside address
# ifconfig wg0 wgport 51820
  # Listen on UDP/51820
# ifconfig wg0 wgkey `cat /etc/wg/private.key`
  # Load this host's private key
# ifconfig wg0 wgpeer <SITE_B_PUBLIC_KEY> wgendpoint 203.0.113.10 51820
  # Configure peer's public key and endpoint
# ifconfig wg0 wgpeer <SITE_B_PUBLIC_KEY> wgaip 10.99.0.2/32
  # Allowed-IPs for the peer (peer's inside address)
# route add -net 10.20.0.0/24 10.99.0.2
  # Route a remote LAN via the tunnel if needed

Site B #

# ifconfig wg0 create
# ifconfig wg0 inet 10.99.0.2/30
# ifconfig wg0 wgport 51820
# ifconfig wg0 wgkey `cat /etc/wg/private.key`
# ifconfig wg0 wgpeer <SITE_A_PUBLIC_KEY> wgendpoint 198.51.100.10 51820
# ifconfig wg0 wgpeer <SITE_A_PUBLIC_KEY> wgaip 10.99.0.1/32
# route add -net 10.0.0.0/24 10.99.0.1

Permit the WireGuard UDP port on each WAN and allow the inside prefixes on wg0 in PF as appropriate.

Verification #

$ ikectl show sa
  # IKE_SA and CHILD_SA state, SPIs, lifetimes
$ ikectl show flows
  # Traffic selectors currently installed
  • Encapsulated traffic on the wire with tcpdump(8) :
# tcpdump -ni em0 port 500 or port 4500 or proto esp
  # IKE_SA negotiation and ESP/NAT-T on the WAN
# tcpdump -ni enc0 host 10.20.0.50
  # Plaintext (post-decrypt) traffic on enc0 for IPsec
# tcpdump -ni em0 udp port 51820
  # WireGuard UDP transport
  • Path testing:
$ ping -n -c 3 10.20.0.1
  # From a host behind Site A to a host behind Site B (IKEv2)
$ ping -n -c 3 10.99.0.2
  # Between tunnel endpoints (WireGuard)
  • PF and routes:
$ pfctl -vvsr | egrep 'udp .* (500|4500)|proto esp|udp .* 51820'
  # Confirm allowances
$ netstat -rn | egrep '10\.99\.0\.|10\.20\.0\.'
  # Confirm inside/tunnel routes

Troubleshooting #

  • No IKE negotiation. Verify UDP 500/4500 reachability and that the responder is in passive mode. Inspect logs in /var/log/daemon for iked.
  • Selector mismatch. If one side uses 10.0.0.0/24 ↔ 10.20.0.0/24 and the other uses 10.0.0.0/24 ↔ 10.0.0.0/24, CHILD_SA will not install. Align from/to networks. Check ikectl show flows.
  • NAT or path asymmetry. Ensure upstreams carry UDP 4500 unchanged and that return traffic follows the same egress. For IPsec, enc0 rules should allow the protected subnets.
  • MTU black holes. If large transfers stall, clamp MSS on the WAN during testing in PF (scrub in max-mss ...) and refine after measuring path MTU.
  • Clock skew. Certificates and IKE lifetimes are time-sensitive. Ensure NTP is working with ntpd(8) .
  • WireGuard handshake fails. Confirm the correct public keys, matching wgport, reachable wgendpoint, and that each side has the other’s inside /32 as an allowed IP. Use ifconfig wg0 to view current peers and statistics.

See Also #