embedded adventures

IPsec with Strongswan

While WireGuard is all the rage these days I think there is still value in rolling out a simple VPN network based on IPSec. The common argument is that IPSec is very complex and easy to misconfigure so it is useful to have a simple 'opinionated' alternative. Well, I am of an opinion that if you read the manual you can enjoy a number of benefits with IPSec such as increased flexibility, a choice of battle-tested implementations, widely deployed IPsec-aware network infrastructrure, and native support in Windows, MacOS as well as Linux. In fairness though, it is complex and easy to misconfigure, so I am mostly writing this to keep track myself.

I highly recommend the strongSwan implementation because it is robust, continuously maintained, and has an excellent test suite database where you can look up almost every supported configuration along with config files used, iptables rules, logs, and even tcpdumps of the traffic. Should you still have troubles, the issue tracker gets prompt if terse responses from an acerbic Swiss maintainer who will often correctly point out that you have no idea what you are doing. It is tremendous.

I have what is essentially a roadwarrior setup with X.509 certs with some minor changes. The gateway is a cheap hetzner cloud instance, which also hosts this blog among other stuff. The roadwarriors are two laptops, an Android phone and a media server at home. The media server is treated as a roadwarrior, since a residential DOCSIS uplink does not guarantee a static IP. We will get to this later.

Cipher suites

People like to split hairs over that one but you can just choose one of the recommended standards. RFC 6379 is the one I am using resulting in:


This follows CNSA as well. A few more alternatives to consider here.

Certificate generation

The process is well described on the strongswan wiki here. However, instead of RSA-signed certificates I am using ECDSA-signed certificates for the clients. In principle, the choice of EC for IKEv2 should be disparate from certificate validation, but on Windows it isn't. To generate an ECDSA-signed certificate and bundle it together with CA certificate (created earlier) and a private key you need to do something like:

    ipsec pki --gen --type ecdsa > LaptopKeyECDSA.der
    ipsec pki --issue --in LaptopKeyECDSA.der --type priv --cacert caCert.der --cakey caKey.der --dn "C=DE, O=heap.ovh, CN=Laptop" --san vpn.heap.ovh > LaptopCertECDSA.der
    openssl ec -inform der -outform pem -in LaptopKeyECDSA.der -out LaptopKeyECDSA.pem
    openssl x509 -inform der -outform pem -in LaptopCertECDSA.der -out LaptopCertECDSA.pem
    openssl pkcs12 -in LaptopCertECDSA.pem -inkey LaptopKeyECDSA.pem -certfile caCert.pem -export -out Laptop.p12

Such a bundle needs to be prepared for each VPN client. Note the additional requirements for gateway certificates.

Linux gateway

The home media server (t520) needs to be assigned a static virtual address. Its configuration is matched by the certificate attributes with rightid and a static IP is assigned with rightsourceip.

The roadwarriors receive dynamic virtual IPs from their own subnet. The server will also assign them a DNS server using rightdns (I'm using Cloudflare but you can pick your own poison).

The VPN gateway (the 'responder') needs to be able to forward traffic received from the clients. For IPv4 traffic you need to add the following to sysctl:

sysctl net.ipv4.ip_forward=1

In addition, leftfirewall=yes config option will ensure that iptables rules permitting this traffic will be added. However, to access hosts on the Internet (or rather: to have those hosts get back to you) it is necessary to set up NAT for the virtual IPs as well:

iptables -t nat -A POSTROUTING -s -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s -o eth0 -j MASQUERADE

We are not quite done with sysctl and iptables though. It is fairly common to run into issues with Path MTU Discovery, especially on sketchy networks. It is a good idea to disable PMTUD and reduce the MSS.

sysctl net.ipv4.ip_no_pmtu_disc=1

The common knowledge is to keep MSS below 1360, but I sometimes had to go as low as 1280.

iptables -t mangle -A FORWARD -m policy --pol ipsec --dir in -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
iptables -t mangle -A FORWARD -m policy --pol ipsec --dir out -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360

You probably want to save these (in /etc/sysctl.conf and using iptables-save).

With auto=route, strongswan will install the necessary routes as needed to routing table 220. To view it, use ip route show table 220

In addition, we want to be able to do Dead Peer Detection (DPD) to periodically check if our roadwarriors are still there. dpdaction=clear will close the connection if it times out.

The ipsec.conf file on the server ties it all together:

conn %default

conn roadwarriors

conn t520
        rightid="C=DE, O=heap.ovh, CN=t520"

The configuration file is fairly minimal, and some options are implicit as default (e.g. fragmentation=yes or mobike=yes). Take a look at the manual for details.

Linux client

Back to our unfortunate media server. Since the residential IP is dynamic, the obvious problem is that if it goes down (power outage, router reboot etc), the VPN gateway has no way of knowing the new IP address. This may be a problem if you try to access it from a roadwarrior. You could mitigate it with updown scripts but it might not be very robust. An alternative could be DynDNS but that is a different can of worms on its own. However, in contrast to e.g. OpenVPN, IPsec is policy based. The installed kernel traps will detect network traffic matching a particular policy and try to establish a connection as required. The crude way to reestablish a connection then is to generate traffic to the virtual subnet on the client side. I ping the gateway every minute from crontab and it works well, but if you have a neater solution let me know.

Now, since the media server also serves files and streams music we want its virtual IP to be static. We can request any IP with leftsourceip=%config and the gateway will assign one as described in the previous section.

conn %default

conn ovh

        leftid="C=DE, O=heap.ovh, CN=t520"


Once the connection is established, you can see it with ipsec status (or ipsec statusall for more details):

ipsec status
Routed Connections:
         ovh{741}:  ROUTED, TUNNEL, reqid 1
         ovh{741}: === 159.69.xx.xx/32
Security Associations (1 up, 0 connecting):
         ovh[316]: ESTABLISHED 52 minutes ago,[C=DE, O=heap.ovh, CN=t520]...159.69.xx.xx[vpn.heap.ovh]
         ovh{1253}:  INSTALLED, TUNNEL, reqid 2, ESP in UDP SPIs: cc81a7c3_i cc148699_o
         ovh{1253}: === 159.69.xx.xx/32


Windows 10 features a native support of IPSec with a nice selection of cryptographic primitives, but there are still some gotchas. One was the coupling of certificate key to IKEv2 DH groups. The other is that for some reason Windows clients behind a NAT do not like rekying intiated by the responder. We have rekey=no set on the server for that reason.

But wait, how do you even begin to set a VPN connection on Windows? First of all, we need to install the machine certificate bundle generated earlier. This is once again explained on the wiki.

To set up the actual connection we need to use a PowerShell cmdlet, no wizards this time:

PS C:\WINDOWS\system32> Add-VpnConnection -Name "heap.ovh" -ServerAddress "vpn.heap.ovh" -TunnelType "IKEv2" -AuthenticationMethod "MachineCertificate" -PassThru

Name                  : heap.ovh
ServerAddress         : vpn.heap.ovh
AllUserConnection     : False
Guid                  : {A32DB97E-1A9B-4C47-961E-118564708722}
TunnelType            : Ikev2
AuthenticationMethod  : {MachineCertificate}
EncryptionLevel       : Optional
L2tpIPsecAuth         :
UseWinlogonCredential : False
EapConfigXmlStream    :
ConnectionStatus      : Disconnected
RememberCredential    : False
SplitTunneling        : False
DnsSuffix             :
IdleDisconnectSeconds : 0

Now, we need to configure the cipher suites:

PS C:\WINDOWS\system32> Set-VpnConnectionIPsecConfiguration -ConnectionName "heap.ovh"  -PassThru

cmdlet Set-VpnConnectionIPsecConfiguration at command pipeline position 1
Supply values for the following parameters:
AuthenticationTransformConstants: GCMAES256
CipherTransformConstants: GCMAES256
EncryptionMethod: AES256
IntegrityCheckMethod: SHA384
PfsGroup: ECP384
DHGroup: ECP384

Changing the Cryptography Settings. Do you want to continue?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): Y

The VPN connection is now available from the network tray icon.

vpn gif

We are not quite done yet though. Turns out that Windows 10 has an optimization 'feature' called 'smart multi-homed name resolution'. Enabled by default, it will send DNS queries across all available adapters and use whatever responds first. Since we typically want our DNS queries to be forwarded to our DNS server of choice rather than what the airport wifi DHCP server thinks is best we need to turn it off in the group policy editor. I would also consider enabling DoH and disabling WebRTC in the browser for a good measure.

You may also want to control the split tunneling behaviour. To enable split tunneling (as opposed to routing everything indiscriminately through the VPN) you need o uncheck Use default gateway on remote network option under Advanced TCP/IP settings of the VPN connection.

It may be necessary to associate the IPsec connection automatically before user log-in which can be useful for remote access. To do this, you can create a new task in the Task Scheduler as described here on stackexchange. With certificate-based authentication the only parameter you need to provide for rasdial.exe is the VPN connection name.


android jpg

Strongswan has an official android app as well. The configuration is very straightforward. First, you need to import a certificate bundle (generated as shown earlier), either directly into the app or the Android certificate store. Then, add a new VPN profile with IKEv2 Certificate chosen as the VPN type, select the user certificate you just added and name the connection.


I don't have an Apple device to test, but feel free to give it a try.


ipsec.conf/stroke vs swanctl.conf/vici