Roadwarrior 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 a server to provide ipsec for devices in a road warrior setup: an end-user wants to relay all his traffic from a single device through the VPS to the Internet. The server acts as an IPsec gateway, and only the server needs a public IP address. The end-user can be behind a NAT.
This is based on the VPN guide from the OpenBSD FAQ.
192.0.2.1 (NAT) 198.51.100.1 init <-----IPsec-----> resp <--> Internet 10.0.5.0/24 10.0.5.1
Note: If you are trying to connect two networks together, consult the site-to-site ipsec guide.
Initiator and Responder Addressing
We start with two machines: init
, which will initiate the connection to
resp
, which will respond. resp
will be running OpenBSD; init
can
run any operating system that supports IPsec.
In this example, init
sits behind a NAT, with IP address 192.0.2.1
.
resp
has the public IP address 198.51.100.1
. init
will be
dynamically assigned an IP address in the range 10.0.5.0/24
by resp
,
who itself will use the IP address 10.0.5.1
internally.
Authentication
iked can authenticate peers with RSA and ECDSA public keys, EAP MSCHAPv2, and X.509 certificates.
Exchanging Public Keys
Using RSA and ECDSA public keys is quick and simple if supported by init
's
operating system. Using these public keys bypasses the need for x.509
certificates.
In this example, we assume that init
is also running OpenBSD, and you have
root access on both init
and resp
. 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 resp
to init
:
Note: init
sits behind a NAT, so init
must initiate the copying.
init$ ssh 198.51.100.1 'doas cat /etc/iked/local.pub' | doas tee /etc/iked/pubkeys/fqdn/resp.example.com
Replace resp.example.com
with resp's fully qualified domain name.
Next, 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'
init
probably lacks a FQDN, so give it a unique name that can be used for
the srcid
.
If you lack shell or doas access on both servers, you may need to find some other method to transfer the public keys. Or, consider using another authentication method listed below.
Configure iked.conf:
init# cat /etc/iked.conf gateway = "198.51.100.1" srcname = "init.example.com" destname = "resp.example.com" pool = "10.0.5.0/24" dns = "198.51.100.1" ikev2 $destname active esp \ from dynamic to any \ peer $gateway \ srcid $srcname dstid $destname \ request address any \ iface lo1 resp# cat /etc/iked.conf gateway = "198.51.100.1" hostname = "resp.example.com" pool = "10.0.5.0/24" dns = "198.51.100.1" ikev2 $hostname passive esp \ from any to dynamic \ local $gateway peer any \ srcid $hostname \ config address $pool \ config name-server $dns \ tag "ROADW"
resp
will allow flows from any to dynamic
, meaning any IP address to a
dynamically assigned IP lease from the address pool 10.0.5.0/24. The packets
will be tagged with ROADW
for easy filtering with pf?.
init
will request address any
, meaning it will request any IP address
from resp
. It allows flows from dynamic to any
, that is, flows starting
from the dynamically assigned IP from the pool (@10.0.5.0/24@@) to any IP
address on the Internet.
Add a lo1 interface for init
:
init# cat /etc/hostname.lo1 up
X.509 Certificate
Many operating systems require init
use X.509 certificates to authenticate
properly.
First, resp
can create a Certificate Authority (CA) and export its
certificate:
resp# ikectl ca resp.example.com create resp# ikectl ca resp.example.com install certificate for CA 'resp.example.com' installed into /etc/iked/ca/ca.crt CRL for CA 'resp.example.com' installed to /etc/iked/crls/ca.crl resp# ikectl ca resp.example.com certificate resp.example.com create resp# ikectl ca resp.example.com certificate resp.example.com install writing RSA key resp# cp /etc/iked/ca/ca.crt /var/www/htdocs/ resp# chmod o+r /var/www/htdocs/ca.crt
You may need to change the destination /var/www/htdocs/
depending upon the
htdocs folder set in httpd.conf(5) on
resp
. See the openhttpd guide.
On init
's device, download the file from resp
's webserver. Depending on
the configuration, this may be https://resp.example.com/ca.crt. Import the CA
certificate into the device. Consult the vpn guides for
instructions for most common operating systems.
Next, we will use EAP with username/password. Configure
iked.conf for resp
:
resp# cat /etc/iked.conf gateway = "198.51.100.1" hostname = "resp.example.com" pool = "10.0.5.0/24" dns = "198.51.100.1" ikev2 $hostname passive esp \ from any to dynamic \ local $gateway peer any \ srcid $hostname \ eap "mschap-v2" \ config address $pool \ config name-server $dns \ tag "ROADW" user 'username' 'password'
Replace username
and password
; replace 198.51.100.1
with resp
's public IP address; and replace resp.example.com
with its actual hostname (it must resolve to a valid IP address).
from any to dynamic
allows any user to connect. $dns must provide the IP
address for the name server that vpn clients will use. This example assumes you
have a valid caching name server configured and listening
on IP 198.51.100.1
.
These packets will get tagged as ROADW
for easy filtering with
pf?.
X.509 certificate
resp# pkg_add zip resp# ikectl ca vpn certificate client1.domain create resp# cp /etc/ssl/vpn/client1.domain.crt /etc/iked/certs/ resp# ikectl ca vpn certificate client1.domain export resp# tar -C /tmp -xzf client1.domain.tgz *pfx resp# cp /tmp/export/client1.domain.pfx /var/www/htdocs/client1.domain.pfx
Configure resp
Add a vether(4) interface for resp
:
resp# cat /etc/hostname.vether0 inet 10.0.5.1 0xffffff00
Edit sysctl.conf(5) to include these
directives on resp
:
resp# cat /etc/sysctl.conf net.inet.ip.forwarding=1 net.inet6.ip6.forwarding=1 net.inet.ipcomp.enable=1 net.inet.esp.enable=1 net.inet.ah.enable=1 resp# sysctl net.inet.ip.forwarding=1 resp# sysctl net.inet6.ip6.forwarding=1 resp# sysctl net.inet.ipcomp.enable=1 resp# sysctl net.inet.esp.enable=1 resp# sysctl net.inet.ah.enable=1
IP forwarding must be enabled for resp
to forward the user's packets to
its final destination. esp
and ah
are the protocols for
ipsec(4).
[https://man.openbsd.org/iked.8|iked(8)]] depends upon packet filter being
enabled. First, enable packet filter? on resp
if it is turned
off:
resp# pfctl -e
Add rules similar to the following for /etc/pf.conf?:
resp="198.51.100.1" ext_if="vio0" pass in on $ext_if proto udp to $resp port {isakmp, ipsec-nat-t} tag IKED pass in on $ext_if proto esp to $resp tag IKED pass on enc0 inet tagged ROADW match out on $ext_if inet tagged ROADW nat-to $ext_if match in quick on enc0 inet proto { tcp, udp } to port 53 rdr-to 127.0.0.1 port 53
Make sure to define the macro $ext_if with your external interface, such as vio0 for virtio(4).
Reload packet filter:
resp# pfctl -f /etc/pf.conf
Starting iked
iked(8) refuses to start if permissions are too loose:
init# chmod 0600 /etc/iked.conf resp# chmod 0600 /etc/iked.conf
Start iked on resp
(and init
if
init
is also running OpenBSD):
init# iked -dv ... spi=0x3d1dc86e07c0cb15: send IKE_SA_INIT req 0 peer 198.51.100.1:500 local 0.0.0.0:5 00, 518 bytes spi=0x3d1dc86e07c0cb15: recv IKE_SA_INIT res 0 peer 198.51.100.1:500 local 192.0.2.1:500, 235 bytes, policy 'resp.example.com' spi=0x3d1dc86e07c0cb15: send IKE_AUTH req 1 peer 198.51.100.1:500 local 192.0.2.1:500, 589 bytes spi=0x3d1dc86e07c0cb15: recv IKE_AUTH res 1 peer 198.51.100.1:500 local 192.0.2.1:500, 464 bytes, policy 'resp.example.com' spi=0x3d1dc86e07c0cb15: ikev2_ike_auth_recv: obtained lease: 10.0.5.117 spi=0x3d1dc86e07c0cb15: ikev2_ike_auth_recv: obtained DNS: 198.51.100.1 spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded SPIs: 0xc61f7d48, 0x11f53136 (enc aes-128-gcm esn) spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded flows: ESP-10.0.5.117/32=0.0.0.0/0(0) spi=0x3d1dc86e07c0cb15: established peer 198.51.100.1:500[FQDN/resp.example.com] local 192.0.2.1:500[FQDN/init.example.com] policy 'resp.example.com' as initiator (enc aes-128-gcm group curve25519 prf hmac-sha2-256) resp# iked -dv ... spi=0x3d1dc86e07c0cb15: recv IKE_SA_INIT req 0 peer 192.0.2.1:500 local 198.51.100.1:500, 518 bytes, policy 'resp.example.com' spi=0x3d1dc86e07c0cb15: send IKE_SA_INIT res 0 peer 192.0.2.1:500 local 198.51.100.1:500, 235 bytes spi=0x3d1dc86e07c0cb15: recv IKE_AUTH req 1 peer 192.0.2.1:500 local 198.51.100.1:500, 589 bytes, policy 'resp.example.com' spi=0x3d1dc86e07c0cb15: assigned address 10.0.5.16 to FQDN/init.example.com spi=0x3d1dc86e07c0cb15: send IKE_AUTH res 1 peer 192.0.2.1:500 local 198.51.100.1:500, 464 bytes spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded SPIs: 0xc61f7d48, 0x11f53136 (enc aes-128-gcm esn) spi=0x3d1dc86e07c0cb15: ikev2_childsa_enable: loaded flows: ESP-0.0.0.0/0=10.0.5.16/32(0) spi=0x3d1dc86e07c0cb15: established peer 192.0.2.1:500[FQDN/init.example.com] local 198.51.100.1:500[FQDN/resp.example.com] assigned 10.0.5.16 policy 'resp.example.com' as responder (enc aes-128-gcm group curve25519 prf hmac-sha2-256)
If you see IPsec flows and security associations (SAs) properly established like above, then you can enable and start iked with rcctl(8):
init# rcctl enable iked init# rcctl start iked iked(ok) resp# rcctl enable iked resp# rcctl start iked iked(ok)
Note: Ignore commands for init
if init
is not running OpenBSD.
From init
, ping an external IP address and confirm that all packets pass
through resp
's enc(4) interface:
init# ping 1.1.1.1 PING 1.1.1.1 (1.1.1.1): 56 data bytes 64 bytes from 1.1.1.1: icmp_seq=0 ttl=52 time=8.297 ms 64 bytes from 1.1.1.1: icmp_seq=1 ttl=52 time=8.191 ms resp# tcpdump -ne -i enc0 tcpdump: listening on enc0, link-type ENC 01:44:24.830185 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp: echo request (encap) 01:44:24.837528 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp: echo reply (encap) 01:44:25.830193 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp: echo request (encap) 01:44:25.837375 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp: echo reply (encap)
Confirm IPsec flows with ipsecctl and verify security associations with ikectl:
init# ipsecctl -sa FLOWS: flow esp in from 0.0.0.0/0 to 10.0.5.117 peer 198.51.100.1 srcid FQDN/init.example.com dstid FQDN/resp.example.com type require flow esp out from 10.0.5.117 to 0.0.0.0/0 peer 198.51.100.1 srcid FQDN/init.example.com dstid FQDN/resp.example.com type require SAD: esp tunnel from 198.51.100.1 to 192.0.2.1 spi 0x24ce827e enc aes-128-gcm esp tunnel from 192.0.2.1 to 198.51.100.1 spi 0x7c42210d enc aes-128-gcm resp# ikectl show sa iked_sas: 0xceb3d56a780 rspi 0x964bd34b809388a9 ispi 0x9b48ee25a0b56672 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[10.0.5.117] ESTABLISHED r udpecap nexti 0x0 pol 0xcebae8df000 sa_childsas: 0xceb3d55e780 ESP 0x7c42210d in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xceb3d556c00 @0xceb3d56a780 sa_childsas: 0xceb3d556c00 ESP 0x24ce827e out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xceb3d55e780 @0xceb3d56a780 sa_flows: 0xceb3d571400 ESP out 0.0.0.0/0 -> 10.0.5.117/32 [0]@-1 (L) @0xceb3d56a780 sa_flows: 0xceb3d57c800 ESP in 10.0.5.117/32 -> 0.0.0.0/0 [0]@-1 (L) @0xceb3d56a780 iked_activesas: 0xceb3d556c00 ESP 0x24ce827e out 198.51.100.1:500 -> 192.0.2.1:500 (L) B=0x0 P=0xceb3d55e780 @0xceb3d56a780 iked_activesas: 0xceb3d55e780 ESP 0x7c42210d in 192.0.2.1:500 -> 198.51.100.1:500 (LA) B=0x0 P=0xceb3d556c00 @0xceb3d56a780 iked_flows: 0xceb3d57c800 ESP in 10.0.5.117/32 -> 0.0.0.0/0 [0]@-1 (L) @0xceb3d56a780 iked_flows: 0xceb3d571400 ESP out 0.0.0.0/0 -> 10.0.5.117/32 [0]@-1 (L) @0xceb3d56a780 iked_dstid_sas: 0xceb3d56a780 rspi 0x964bd34b809388a9 ispi 0x9b48ee25a0b56672 198.51.100.1:500->192.0.2.1:500<FQDN/init.example.com>[10.0.5.117] ESTABLISHED r udpecap nexti 0x0 pol 0xcebae8df000
From init
, ping an external IP address and confirm that all packets pass through
resp
'senc0
interface:
init# ping 1.1.1.1 PING 1.1.1.1 (1.1.1.1): 56 data bytes 64 bytes from 1.1.1.1: icmp_seq=0 ttl=52 time=8.297 ms 64 bytes from 1.1.1.1: icmp_seq=1 ttl=52 time=8.191 ms resp# tcpdump -ne -i enc0 tcpdump: listening on enc0, link-type ENC 01:44:24.830185 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp : echo request (encap) 01:44:24.837528 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp : echo reply (encap) 01:44:25.830193 (authentic,confidential): SPI 0x2b3e01ae: 10.0.5.117 > 1.1.1.1: icmp : echo request (encap) 01:44:25.837375 (authentic,confidential): SPI 0x41576324: 1.1.1.1 > 10.0.5.117: icmp : echo reply (encap)
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 flows are configured to use ESP for confidentiality. For more information, see ipsec(4).
Configuring DNS
We will use unbound as the caching DNS resolver. Please make sure to read the unbound configuration guide before proceeding.
Change the following values in resp
's /var/unbound/etc/unbound.conf
:
outgoing-interface: 198.51.100.1 access-control: 10.0.0.0/8 allow
Make sure to enable and start unbound:
resp# rcctl enable unbound resp# rcctl start unbound
You may want to configure domain blacklists to block unwanted traffic.
If init
is running OpenBSD, you can verify if DNS lookup is working properly:
init# host ircnow.org ircnow.org has address 198.251.82.194 ircnow.org has IPv6 address 2605:6404:2d3:: ircnow.org mail is handled by 10 mail.ircnow.org. resp# tcpdump -ne -i enc0 tcpdump: listening on enc0, link-type ENC 01:25:54.119770 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.33324 > 8.8.8.8 .53: 63988+ A? ircnow.org.(28) (encap) 01:25:54.119882 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.33 324: 63988 1/0/0 A 198.251.82.194(44) (encap) 01:25:55.139849 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.37113 > 8.8.8.8 .53: 10591+ AAAA? ircnow.org.(28) (encap) 01:25:55.139942 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.37 113: 10591 1/0/0 AAAA 2605:6404:2d3::(56) (encap) 01:25:56.159754 (authentic,confidential): SPI 0xb5cda039: 10.0.5.117.1698 > 8.8.8.8. 53: 43204+ MX? ircnow.org.(28) (encap) 01:25:56.159846 (authentic,confidential): SPI 0x6f31fa50: 8.8.8.8.53 > 10.0.5.117.16 98: 43204 1/0/0 MX mail.ircnow.org. 10(49) (encap)
Troubleshooting
Running iked in debug mode can provide valuable info about errors in configuration.
First, turn off iked if it is running:
# rcctl stop iked
Check to make sure no iked processes are running:
# ps ax | grep iked
Then, run iked in debug mode:
# iked -dv
-d will cause iked to not daemonize, and -v will report errors verbosely.