With heightening concern regarding the state of internet privacy (fuelled in part by the passing of the Investigatory Powers Act in the UK), I have set up a VPN server on the virtual server I have hosted with Mythic Beasts. This uses strongSwan and certificate-based IKEv2 authentication.
Assumptions:
- Debian Jessie server already set up and accessible via
debian.example.com
, a public IPv4 of203.0.113.1
and a public IPv6 of2001:db8::1
- Client username of
me
- Clients are running the latest versions of macOS and iOS (Sierra and 10 respectively at the time of writing)
- No need to support any other operating systems (although the setup is easily translated)
For automated deployment of a similar setup, albeit Ubuntu-based and using ansible for deployment, I recommend you take a look at Algo VPN. I used that project as a basis for my configuration.
Most of the rest of this guide assumes that you are on the server with root permissions, so:
% ssh debian.example.com
% sudo -s
$ apt-get install strongswan
All of the certificates are stored in /etc/ipsec.d
. Unfortunately, macOS Sierra does not seem to like PKI built using ECDSA certificates for reasons which are not clear to me so I have used 4096-bit RSA keys. The first step is to create a self-signed root CA certificate.
$ cd /etc/ipsec.d
$ ipsec pki --gen --type rsa --size 4096 --outform pem > private/ca.pem
$ ipsec pki --self --ca --lifetime 3650 --in private/ca.pem \
> --type rsa --digest sha256 \
> --dn "CN=debian.example.com" \
> --outform pem > cacerts/ca.pem
Next, generate a private key and signed certificate for the server. macOS requires the hostname/address in subjectAltName. Versions of Mac OS X prior to 10.7.4 required --flag ikeIntermediate
but I have not added it here.
$ ipsec pki --gen --type rsa --size 4096 --outform pem > private/debian.pem
$ ipsec pki --pub --in private/debian.pem --type rsa |
> ipsec pki --issue --lifetime 3650 --digest sha256 \
> --cacert cacerts/ca.pem --cakey private/ca.pem \
> --dn "CN=debian.example.com" \
> --san debian.example.com --san 203.0.113.1 --san 2001:db8::1
> --flag serverAuth --outform pem > certs/debian.pem
The last certificate is for the client. You use one certificate for a single user on multiple devices hence I have only generated one.
$ ipsec pki --gen --type rsa --size 4096 --outform pem > private/me.pem
$ ipsec pki --pub --in private/me.pem --type rsa |
> ipsec pki --issue --lifetime 3650 --digest sha256 \
> --cacert cacerts/ca.pem --cakey private/ca.pem \
> --dn "CN=me" --san me \
> --flag clientAuth \
> --outform pem > certs/me.pem
Now that certificate generation is complete, you should remove the CA private key (private/ca.pem
) from the server. You can either store it offline, or delete it entirely. If you choose the latter, you will not be able to sign any more peer certificates or revoke a certificate if a client were compromised. In the latter scenario, you would need to recreate a fresh PKI and disseminate to all peers. Failure to remove the CA private key means that anyone who gains access to it could MitM your TLS connections.
Before getting started with configuring strongSwan, you'll want to generate an IPv6 unique local address block. The addresses are within the fc00::/7 block and contain a pseudo-random component according to RFC 4193. I've cobbled together a short script which will generate a ULA for you:
% curl -s https://raw.githubusercontent.com/andrewlkho/ulagen/master/ulagen.py | python
Prefix: fdf3:5237:bf63::/48
First subnet: fdf3:5237:bf63::/64
Last subnet: fdf3:5237:bf63:ffff::/64
Clearly your prefix will not be the same as that, so substitute fdf3:5237:bf63::/64
in the dnsmasq, strongSwan and iptables configuration below with what you have.
To prevent DNS cache poisoning, I run a local DNS server on debian
which the VPN clients use. This consists of a dnscrypt-proxy client bound to [::1]:5353 which transparently proxies queries through to a dnscrypt-aware upstream resolver. dnscrypt-proxy itself is upstream of dnsmasq which provides features like DNSSEC validation and is what the VPN clients have configured as their DNS server.
Install dnscrypt-proxy.
$ sudo apt-get install libsodium-dev libldns-dev
$ cd /usr/local/src
$ curl -LO https://download.dnscrypt.org/dnscrypt-proxy/dnscrypt-proxy-1.9.2.tar.bz2
$ gpg --recv-keys 2B6F76DA
$ curl https://download.dnscrypt.org/dnscrypt-proxy/dnscrypt-proxy-1.9.2.tar.gz.sig | \
> gpg --verify - ./dnscrypt-proxy-1.9.2.tar.gz
$ tar xjf dnscrypt-proxy-1.9.2.tar.gz && cd dnscrypt-proxy-1.9.2
$ ./configure --prefix=/usr/local
$ make
$ make install
Create an unprivileged user.
$ useradd --system -d /usr/local/lib/dnscrypt-proxy -s /usr/sbin/nologin dnscrypt
Edit /usr/local/etc/dnscrypt-proxy.conf
. You can choose whichever dnscrypt-enabled upstream server you want; I have chosen to use DNSCrypt.eu of which their Netherlands server has the fewest hops to debian
. dnscrypt-proxy is bound to ::1 port 5353.
ProviderName 2.dnscrypt-cert.resolver1.dnscrypt.eu
ProviderKey 67C0:0F2C:21C5:5481:45DD:7CB4:6A27:1AF2:EB96:9931:40A3:09B6:2B8D:1653:1185:9C66
ResolverAddress [2a00:d880:3:1::a6c1:2e89]:443
Daemonize no
LocalAddress [::1]:5353
LocalCache off
User dnscrypt
EphemeralKeys on
BlockIPv6 no
Copy across the systemd service (amending the paths) and start dnscrypt-proxy.
$ sed -e 's:^ExecStart=.*$:ExecStart=/usr/local/sbin/dnscrypt-proxy /usr/local/etc/dnscrypt-proxy.conf:' \
> /usr/local/src/dnscrypt-proxy-1.9.2/dnscrypt-proxy.service > /etc/systemd/system/dnscrypt-proxy.service
$ systemctl enable dnscrypt-proxy.service
$ systemctl start dnscrypt-proxy
You can check that it works by querying resolver.dnscrypt.org
which returns the address of the nameserver you are using.
$ dig @::1 -p 5353 +short -x $( dig @::1 -p 5353 +short resolver.dnscrypt.org A )
resolver1.dnscrypt.eu
The next step is to install dnsmasq.
$ apt-get install dnsmasq
dnsmasq should bind to ::1 and also a unique local address on the loopback interface. Use the ULA prefix generated previously, but a different subnet, by putting the following into /etc/network/interfaces
.
auto lo:1
iface lo:1 inet6 static
address fdf3:5237:bf63:1::1
netmask 64
autoconf 1
Then bring it up with ifup lo:1
. Edit /etc/dnsmasq.conf
.
no-resolv
no-hosts
server=::1#5353
listen-address=::1
listen-address=fdf3:5237:bf63:1::1
bind-interfaces
dnssec
trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
dnssec-check-unsigned
bogus-priv
domain-needed
stop-dns-rebind
rebind-localhost-ok
cache-size=2000
Restart dnsmasq.
$ systemctl restart dnsmasq
Disable any plugins that you do not need.
$ plugins=(aes gcm hmac kernel-netlink nonce openssl pem pgp \
> pkcs12 pkcs7 pkcs8 pubkey random revocation sha2 socket-default stroke x509)
$ cd /etc/strongswan.d/charon
$ for x in *.conf; do
> if [[ ${plugins[(i)${x%.conf}]} -le ${#plugins} ]]; then
> sed -E 's/^([[:space:]]+)load = no/\1load = yes/' ${x}
> else
> sed -E 's/^([[:space:]]+)load = yes/\1load = no/' ${x}
> fi
> done
Copy the following /etc/ipsec.conf
(don't forget to change the ULA).
config setup
uniqueids=never
conn %default
keyexchange=ikev2
ike=aes128gcm16-sha2_256-prfsha256-ecp256!
esp=aes128gcm16-sha2_256-ecp256!
fragmentation=yes
rekey=no
compress=yes
dpdaction=clear
left=%any
leftauth=pubkey
leftid=debian.example.com
leftcert=debian.pem
leftsendcert=always
leftsubnet=0.0.0.0/0,::/0
right=%any
rightauth=pubkey
rightsourceip=172.16.0.0/24,fdf3:5237:bf63::/64
rightdns=fdf3:5237:bf63:1::1
conn ikev2-pubkey
auto=add
A few points to note on that:
- I have only allowed a single IKE cipher suite. If you want to support clients other than macOS and iOS, you may need to adjust this.
- macOS/iOS will only propose 128-bit AES-GCM if you configure the VPN using a
.mobileconfig
configuration profile. If you use the GUI to configure the VPN on the client then you will need to useike=aes256-sha2_256-prfsha256-ecp256!
andesp=aes256-sha2_256-ecp256!
. - I have used the local DNS server bound to the loopback interface.
Next, copy the following /etc/ipsec.secrets
.
: RSA debian.pem
Then restart strongswan.
$ systemctl restart strongswan
Configure the kernel to enable packet forwarding by putting the following lines in /etc/sysctl.conf
.
net.ipv4.ip_forward = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.eth0.accept_ra = 2
The penultimate line enables forwarding for IPv6, but has the unfortunate side-effect of therefore not accepting router advertisements. These are needed for SLAAC, without which the server cannot get routing information (at least, with how Mythic Beasts configure their virtual servers). This would break IPv6 networking on the server. The last line therefore adjusts this for the interface eth0
.
Load the settings.
% sysctl -p
Here is a snippet of my /etc/iptables/rules.v4
.
*filter
[...]
-A INPUT -p esp -j ACCEPT
-A INPUT -p ah -j ACCEPT
-A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -m conntrack --ctstate NEW -s 172.16.0.0/24 -m policy --pol ipsec --dir in -j ACCEPT
COMMIT
*nat
[...]
-A POSTROUTING -s 172.16.0.0/24 -m policy --pol none --dir out -j MASQUERADE
# If you have a static IP address you can do:
# -A POSTROUTING -s 172.16.0.0/24 -m policy --pol none --dir out -j SNAT --to-source 203.0.113.1
COMMIT
Here is a snippet of my /etc/iptables/rules.v6
(don't forget to change the ULA).
*filter
[...]
-A INPUT -p esp -j ACCEPT
-A INPUT -m ah -j ACCEPT
-A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT
-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -m conntrack --ctstate NEW -s fdf3:5237:bf63::/64 -m policy --pol ipsec --dir in -j ACCEPT
# If you are running a local DNS server as above
-A INPUT -p udp --dport 53 -d fdf3:5237:bf63:1::1 -j ACCEPT
COMMIT
*nat
[...]
-A POSTROUTING -s fdf3:5237:bf63::/64 -m policy --pol none --dir out -j MASQUERADE
COMMIT
To configure the macOS and iOS clients, you need to generate a configuration profile. I wrote a shell script to do this (mobileconfiggen.sh
) which is attached to this gist. As well as the VPN settings, this pulls in the root CA certificate and client key/certificate pair.
Aside from the variables at the start of the script, you will probably also want to amend the OnDemandRules
array. This is an array of dictionaries, each of which specifies whether or not your iOS device will automatically connect to the VPN in certain scenarios. The rules I have specified mean that unless you are on a mobile connection or using one of the pre-specified trusted WiFi networks ($TRUSTED_SSIDS
) it will connect to the VPN. If you want to edit this, then take a look at Apple's Configuration Profile Reference.
The script takes no arguments, and will output the configuration profile to STDOUT.
$ ./mobileconfiggen.sh > debian.mobileconfig
On macOS, you can install this with:
$ profiles -I -F debian.mobileconfig
On iOS, the easiest way to install it is to send the file to the device over AirDrop.
That's it. Reboot your server to check it all comes up automatically and you should be done. A few links which were helpful to me:
- Algo VPN: a similar setup based on deploying to an Ubuntu cloud server using ansible
- Andy Smith's blog: getting IPv6 packet forwarding and SLAAC to work together
- The strongSwan wiki
- Apple's Configuration Profile Reference
If you have any suggestions for how this guide or setup could be improved, then please let me know.
I had this setup working previously (back in January 2020) but it seems to have stopped working on MacOS 10.15.6 and iOS 13.6.1. This manifests as "User authentication failed" in the iOS GUI when attempting to connect, and various errors in logs from
NEIKEV2Provider
saying "Authentication: Certificate authentication data could not be verified". There are no errors in StrongSwan logs - as far as it's concerned, the connection is fine.It would seem to be complaining about not being able to build a full chain with the self-signed cert. I've tried regenerating the root CA and all certs then redeploying, but nothing seems to work. I can't configure the connection manually via the iOS GUI either. Anyone else experiencing a similar issue and/or found a solution?
Edit: This turned out to be a GCC bug, partly due to the fact that I'm running Fedora rather than Debian. There's a bug with the way that the
pki
tool gets compiled - see https://wiki.strongswan.org/issues/3249. My CA cert had invalid key usage flags as a result of this bug and it seems that as part of Apple's new certificate checking process, the key usage flags were checked and found to be invalid. This appears in MacOS/iOS logs asTrust evaluate failure: [root KeyUsage]
underNEIKEv2Provider
.After I reran the
pki
commands on a Debian-based distribution everything now works as expected.