crumbles.blog computers are bad and you shouldn’t use them

HOWTO: Set up an IPsec VPN on NixOS and connect to it with Mac OS and iOS

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:

  1. Download the ca-cert.pem file from /etc/ipsec.d/cacerts

  2. 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.)

  3. 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.

  4. 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

Comments

Post your comment by replying to this post on Mastodon.