There are two options for IPsec VPNs on NixOS: Libreswan and Strongswan. Since Strongswan has much better NixOS configuration, we’ll use that.
Note! In the best tradition of howto guides on blogs, I’m not an expert on IPsec, Strongswan, VPN configuration, nor even really NixOS. However, these are the settings that worked for me, derived mostly from the DigitalOcean guide to setting up Strongswan on Ubuntu and adapted for Nix. Please try every other support forum you can think of before asking me personally for help, because I probably have no idea :-)
Generating the needed certificates, etc.
For its own very special reasons, Strongswan’s command-line tools for generating keys and certificates don’t work without an /etc/strongswan.conf file present, even though they don’t need it. Fortunately, an empty one is okay, so let’s create one:
$ echo | sudo tee /etc/strongswan.conf
(Use nix-shell -p strongswan if you don’t have flakes and nix-command enabled!)
$ nix shell nixpkgs#strongswan
Create directory to hold our keys and certificates and set some permissions for safety:
$ mkdir -p ~/pki/{cacerts,certs,private}
$ chmod 700 ~/pki
$ pki --gen --type rsa --size 4096 --outform pem > ~/pki/private/ca-key.pem
$ pki --self --ca --lifetime 3650 --in ~/pki/private/ca-key.pem --type rsa --dn "CN=VPN root CA" --outform pem > ~/pki/cacerts/ca-cert.pem
$ pki --gen --type rsa --size 4096 --outform pem > ~/pki/private/server-key.pem
For a private VPN, I suggest connecting only through its IP address rather than messing about with DNS. In the following, replace 12.34.56.78 with the IP of your own server. If you really want to connect through a domain name, you can delete the last --san @12.34.56.78 below and just use the other two with the IP address replaced by your domain name.
$ pki --pub --in ~/pki/private/server-key.pem --type rsa \
| pki --issue --lifetime 1825 \
--cacert ~/pki/cacerts/ca-cert.pem \
--cakey ~/pki/private/ca-key.pem \
--dn "CN=12.34.56.78" --san 12.34.56.78 --san @12.34.56.78 \
--flag serverAuth --flag ikeIntermediate --outform pem \
> ~/pki/certs/server-cert.pem
Now you can exit the Nix shell and copy these new keys and certificates to the right place:
$ sudo cp -r ~/pki/* /etc/ipsec.d/
Finally, delete the temporary strongswan.conf file we created; NixOS will manage all further Strongswan configuration.
$ sudo rm /etc/strongswan.conf
Configuring Strongswan in configuration.nix
services.strongswan = {
enable = true;
# Where your user authentication information will be stored:
secrets = [ "/etc/ipsec.d/ipsec.secrets" ];
setup = {
# Log daemon statuses
charondebug = "ike 1, knl 1, cfg 0";
# Allow multiple connections
uniqueids = "no";
};
connections = {
# You can change the name of the connection from `vpn` if you
# like; this is only used internally
vpn = {
auto = "add";
compress = "no";
type = "tunnel";
keyexchange = "ikev2";
fragmentation = "yes";
forceencaps = "yes";
# Detect and clear any hung connections
dpdaction = "clear";
dpddelay = "300s";
send_cert = "always";
rekey = "no";
# Accept connections on any local network interface
left = "%any";
# Set this to your domain name (prefixed with @) or your IP address
leftid = "12.34.56.78";
leftcert = "server-cert.pem";
leftsendcert = "always";
# Tell clients to use this VPN connection for connections to
# all other IP addresses
leftsubnet = "0.0.0.0/0";
# Accept connection from any remote client
right = "%any";
# Accept connection from any remote client ID
rightid = "%any";
# Authentication method `eap-mschap-v2` works on Mac OS, iOS,
# and allegedly on Android and Windows too
rightauth = "eap-mschapv2";
# Give clients local IP addresses in the 10.0.0.0/1 subnet
rightsourceip = "10.0.0.0/24";
# Set this to your preferred DNS server
rightdns = "1.1.1.1";
# Clients do not need to send certificates
rightsendcert = "never";
# Ask clients for identification when they connect
eap_identity="%identity";
# Recommended ciphersuite settings for iOS and Mac; you may need
# different ones on other platforms
esp = "aes256-sha256-modp2048";
ike = "aes256-sha256-modp2048-modpnone";
};
};
};
We also need to configure the kernel to allow IP forwarding and do some related hardening by setting the appropriate sysctls:
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
boot.kernel.sysctl."net.ipv6.all.forwarding" = 1;
boot.kernel.sysctl."net.ipv4.ip_no_pmtu_disc" = 1;
boot.kernel.sysctl."net.ipv4.conf.all.accept_redirects" = 0;
boot.kernel.sysctl."net.ipv4.conf.all.send_redirects" = 0;
boot.kernel.sysctl."net.ipv6.conf.all.accept_redirects" = 0;
boot.kernel.sysctl."net.ipv6.conf.all.send_redirects" = 0;
Finally, we need to configure the NixOS firewall to allow connections on the IPsec ports, and also to route connections through the VPN properly. (Thanks to Erik Dombi on the Strongswan issue tracker for the information on how to set this up.)
You will need to know the name of your network interface. If you don’t use a declarative, static configuration of your IP address (which for a VPN server you probably should, unless you are using Dynamic DNS or something) you may not know it. Find it with ip route; the network interface name is the word that appears after dev. (In my case, it says default via 98.76.54.32 dev ens3 proto static, so my interface is ens3.) Here I’m using ens3. Replace ens3 everywhere in the extraCommands configuration with the name of your own interface if it’s different for you.
# UDP ports 500 and 4500 are used for IPsec connections
networking.firewall.allowedUDPPorts = [ 500 4500 ];
networking.firewall.extraCommands =
''
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -F
iptables -Z
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p udp --dport 500 -j ACCEPT
iptables -A INPUT -p udp --dport 4500 -j ACCEPT
iptables -A INPUT -p udp --dport 80 -j ACCEPT
iptables -A INPUT -p udp --dport 443 -j ACCEPT
iptables -A FORWARD --match policy --pol ipsec --dir in --proto esp -s 10.0.0.0/24 -j ACCEPT
iptables -A FORWARD --match policy --pol ipsec --dir out --proto esp -d 10.0.0.0/24 -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o ens3 -m policy --pol ipsec --dir out -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o ens3 -j MASQUERADE
iptables -t mangle -A FORWARD --match policy --pol ipsec --dir in -s 10.0.0.0/24 -o ens3 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
iptables -A INPUT -j DROP
iptables -A FORWARD -j DROP
'';
Setting up users
Above in configuration.nix we told the Strongswan NixOS module that our secrets will come from the file /etc/ipsec.d/ipsec.secrets, so we need to create it, tell it where to find our server private key, and add some users:
: RSA "server-key.pem"
username : EAP "password"
Replace username and password by your chosen credentials. You can add more lines of this type if you want more users.
Run it!
nixos-rebuild will start the Strongswan service, reconfigure the firewall, and you should have a working VPN.
For some reason after a system reboot, the VPN seems to take a minute or two to start; nixos-rebuild manages to bring it up immediately.
Connecting on Mac OS and iOS
The advantage of an IPsec VPN is that it’s supported natively by Mac OS and iOS. The process is pretty similar on both:
Download the ca-cert.pem file from /etc/ipsec.d/cacerts
On Mac, open it in Keychain Access and add it to the System keychain. You then need to open the System keychain, right click on the certificate and choose ‘Get Info’, unfold the ‘Trust’ disclosure triangle, and select at least ‘Always Trust’ for IPsec.
On an iOS device, after downloading the file, you will be offered to install the new ‘profile’ by going to the Settings app. Once you install it, you still need to mark the certificate as trusted. Go to General → About → Certificate Trust Settings (right at the bottom) and enable full trust for that root certificate. (There is no fine-grained trust setting on iOS, at least for manual configuration, as far as I know.)
Add a VPN in the VPN settings pane, choosing IKEv2 as the type. Set both the ‘Server address’ and ‘Remote ID’ to the IP address or domain name of your VPN server. Under ‘Authentication’, select ‘User authentication’ method ‘Username’ and enter the username and password you put in the /etc/ipsec.d/ipsec.secrets file.
Create the VPN and turn it on. Hopefully it will work!
Improvements I want to make in a future version of this guide
- Use Agenix to store secrets instead of just dumping them in
/etc
- Understand more about what those
iptables incantations do and maybe pare them down