Setting up a Let's Encrypt signed on-demand IKEv2 VPN on Debian/Ubuntu for iOS with username and password authentication

Here’s how to set up a VPN that has all these features:

  • Works natively on iOS and macOS
  • Doesn't require you to manage certificates or keys
  • Automatically connects when your device is online

It should also work on Windows*, and Android via the StrongSwan app, but I haven’t tested it. Please post about your experiences using this guide with other devices in the comments!

*EDIT: unfortunately the settings as provided below don’t seem to work with Windows. If you manage to get it working, please post a comment with details below, to help other people who come to this page.

As with most of my posts, there are plenty of other places online that describe parts of this process, but nowhere that collects it all together. I’ve wanted to set up a VPN for some time because since last year, British ISPs have been legally required to collect information about all the web addresses that their customers visit and send it to a large number of government organisations, with little to no transparency or oversight. Paid VPN services exist, but they essentially mean that you’re trusting all of your internet traffic to a private company. Free VPN services also exist, but it’s pretty safe to say that they are scanning all of your internet traffic and selling information about it.

These instructions were tested with Debian Stretch, Buster and Bullseye but should work on recent versions of Ubuntu too. I’m going to assume that you’ve already done the base install of your server, sorted out IP addresses and DNS, and secured it properly. After all, there’s not much point in setting up a VPN if the server it runs on is wide open. The VPN settings I’m using here aren’t super-secure (they probably won’t protect from the NSA, for example), but they will protect you from being monitored by default. Someone with know-how and resources will have to make an active effort to spy on your traffic.

First of all, let’s sort out Let’s Encrypt.

apt-get install certbot
certbot certonly --standalone -d vpn.example.com --register-unsafely-without-email

Replace the domain with yours, obviously. Standalone mode requires that (a) you’re not running a web server on this server/IP address and (b) port 80 is not firewalled off. If you have a web server running then you probably want the --webroot mode instead. If port 80 is firewalled, you might want to check out the --pre-hook and --post-hook options for certbot renewals. Alternatively, you could try something like Mythic Beasts’ DNS plugin. If you want to get emails about failed renewals of your VPN certificate, skip the --register-unsafely-without-email. You obviously lose some anonymity in that case. I just monitor the certificate from elsewhere, so I get a warning if it gets close to expiring. You might also want to add a renewal hook to restart StrongSwan each time the certificate is updated. On older versions of Debian you need /bin/systemctl restart ipsec.service. On Bullseye and up it’s /bin/systemctl restart strongswan-starter.service.

Next, let’s install StrongSwan and configure it.

apt-get install strongswan libcharon-extra-plugins moreutils iptables-persistent
nano /etc/ipsec.conf

Put these contents into the config file:

config setup
    charondebug="ike 1, knl 1, cfg 0"
    uniqueids=no

conn ikev2-vpn
    auto=add
    compress=no
    type=tunnel
    keyexchange=ikev2
    fragmentation=yes
    forceencaps=yes
    ike=aes256-sha256-modp2048!
    esp=aes256-sha256-modp2048!
    dpdaction=clear
    dpddelay=300s
    rekey=no
    left=%any
    leftid=@vpn.example.com
    leftcert=/etc/letsencrypt/live/vpn.example.com/fullchain.pem
    leftsendcert=always
    leftsubnet=0.0.0.0/0
    right=%any
    rightid=%any
    rightauth=eap-mschapv2
    rightdns=2001:1608:10:25::1c04:b12f,2001:1608:10:25::9249:d69b,84.200.69.80,84.200.70.40
    rightsourceip=10.11.12.0/24
    rightsendcert=never
    eap_identity=%identity

Make sure you replace the parts in bold. You might also want to change the rightsourceip and rightdns. I’m using the DNS.WATCH servers because I (maybe foolishly) trust them more with my privacy than Google. They also correctly return NXDOMAIN when you try to look up a record that doesn’t exist, which is one of my bugbears with many free DNS servers.

Now you want to set your login details.

nano /etc/ipsec.secrets

Put these contents in:

# This file holds shared secrets or RSA private keys for authentication.
#
# RSA private key for this host, authenticating it to any other host
# which knows the public part.

vpn.example.com : RSA /etc/letsencrypt/live/vpn.example.com/privkey.pem
username %any% : EAP "password"

Again, replace the parts in bold.

Now it’s time to configure the firewall. This is the main part where all of the guides I found I fell down. I still haven’t found an ideal solution (the firewall rules below set the default policy to REJECT instead of DROP), but it’s good enough. The rules below assume that you have no firewall up to start with. If you’ve already got a firewall, you’ll need to figure out how to integrate these rules with your existing ones. I don’t recommend trying to do this with ufw as its support for advanced options is a lot more limited than raw iptables.

iptables -t nat -A POSTROUTING -s 10.11.12.0/24 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.11.12.0/24 -o eth0 -j MASQUERADE
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 500 -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 4500 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 111 -j REJECT --reject-with icmp-port-unreachable
iptables -A FORWARD -s 10.11.12.0/24 -m policy --dir in --pol ipsec --proto esp -j ACCEPT
iptables -A FORWARD -s 10.11.12.0/24 -m policy --dir out --pol ipsec --proto esp -j ACCEPT
iptables -A FORWARD -s 10.11.12.0/24 -o eth0 -p tcp -m policy --dir in --pol ipsec -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360
netfilter-persistent save

Things to note:

  1. If you changed the rightsourceip in /etc/ipsec.conf, replace it accordingly in the firewall rules.
  2. Don’t forget to open any other ports you need. I’ve included ssh on its default port as I’m assuming that’s how you manage your server, if you don’t need it or you use a different port then update as appropriate.
  3. Port 80 is required for Let’s Encrypt renewals.
  4. Since there is no default DROP rule, you definitely want to explicitly block access to port 111 for security (and anything else you might be running that shouldn’t be accessible to the whole internet).
  5. The final forward rule does some magic to help prevent packet fragmentation. Without it, your VPN might not work on some networks. I spent a long, long, loooong time trying to figure out why my VPN kept flaking out on one particular connection without this rule.
  6. Don’t forget to save your rules afterwards! Otherwise you’ll lose them every time you reboot the server.

Finally, a few changes to the kernel network settings. nano /etc/sysctl.conf and make sure that it includes the following lines (create the line if the option isn’t there, or update it if it’s already present):

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1
# Do not accept ICMP redirects (prevent MITM attacks)
net.ipv4.conf.all.accept_redirects = 0
# Do not send ICMP redirects (we are not a router)
net.ipv4.conf.all.send_redirects = 0
net.ipv4.ip_no_pmtu_disc = 1
# Accept IPv6 router advertisements (on by default but is disabled if you have IPv6 forwarding enabled)
net.ipv6.conf.eth0.accept_ra = 2

Reboot once you’ve finished these, and check that (a) the firewall rules are correctly set (iptables -L), and (b) the kernel options are correct (check output of sysctl --all).

If that all looks good, then your VPN should work, but you’ll need to manually configure it and it won’t automatically connect. To do that you need to create a configuration profile for your mobile device. Make a plain text file with the following contents:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Set the name to whatever you like, it is used in the profile list on the device -->
    <key>PayloadDisplayName</key>
    <string>Name</string>
    <key>PayloadIdentifier</key>
    <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
    <string>com.example.vpn</string>
    <!-- A globally unique identifier, use uuidgen on Linux/Mac OS X to generate it -->
    <key>PayloadUUID</key>
    <string>UUID</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
    <key>PayloadContent</key>
    <array>
        <!-- It is possible to add multiple VPN payloads with different identifiers/UUIDs and names -->
        <dict>
            <!-- This is an extension of the identifier given above -->
            <key>PayloadIdentifier</key>
            <string>com.example.vpn.conf</string>
            <!-- A globally unique identifier for this payload -->
            <key>PayloadUUID</key>
            <string>UUID</string>
            <key>PayloadType</key>
            <string>com.apple.vpn.managed</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>UserDefinedName</key>
            <!-- This is the name of the VPN connection as seen in the VPN application later -->
            <string>Name</string>
            <key>VPNType</key>
            <string>IKEv2</string>
            <key>IKEv2</key>
            <dict>
                <!-- This is the hostname or IP address of VPN server.
                 Chosing IP address can avoid issues with client DNS resolvers and speed up connection process. -->
                <key>RemoteAddress</key>
                <string>vpn.example.com</string>
                <!-- leftid in ipsec.conf -->
                <key>RemoteIdentifier</key>
                <string>vpn.example.com</string>
                <!--
                    OnDemand references:
                    http://www.v2ex.com/t/137653
                    https://developer.apple.com/library/mac/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html
                    Continue reading:
                    https://github.com/iphoting/ovpnmcgen.rb
                -->

                <!-- AlwaysOn OnDemand Rule -->

                <key>OnDemandEnabled</key>
                    <integer>1</integer>
                    <key>OnDemandRules</key>
                    <array>
                        <dict>
                            <key>Action</key>
                            <string>Connect</string>
                        </dict>
                    </array>
                <key>DeadPeerDetectionRate</key>
                <string>High</string>
                <key>AuthenticationMethod</key>
                <string>Certificate</string>
                <key>NATKeepAliveInterval</key>
                <integer>30</integer>
                <key>NATKeepAliveOffloadEnable</key>
                <true/>
                <key>ExtendedAuthEnabled</key>
                <integer>1</integer>
                <!-- Username and password from ipsec.secrets -->
                <key>AuthName</key>
                <string>username</string>
                <key>AuthPassword</key>
                <string>password</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

Replace the bits in bold as required. The UUIDs can be generated using uuidgen – note that the two UUIDs must be different! Everything else should be self explanatory.

Finally, save the plain text file with the extension .mobileconfig, e.g. vpn.mobileconfig. Then you can either email it to yourself, or put it on a web server and access it from the browser on your mobile device. It will prompt you to install the profile. Once that’s done, reboot the device and voilà! It should automatically connect to the VPN on startup. Make sure that you make the config file inaccessible once you’re done, as it contains your plaintext username and password.

Comments

It seems like the strongswan-plugin-eap-mschapv2 package was moved.

So just apt-get libcharon-extra-plugins instead of strongswan-plugin-eap-mschapv2.

Hi, very good your post. I want to use the VPN in Windows 7 client. How can I export the Let's Encrypt certificate to use it on Windows 7?

Thanks

To export the certificate, you just need to copy the file /etc/letsencrypt/life/example.com/fullchain.pem from your server (where example.com is the domain name you're using). Sorry to say, I don't know what the required encryption algorithm settings are to work with Windows. The settings above work with Apple devices, but when I tried them with Windows 7 a few years ago it didn't work. It's definitely possible to get it working with Windows, but I don't use Windows and don't have any Windows devices for testing so I haven't had the chance to update the instructions to work with it. If you are able to work out suitable settings, please post them here and I will update the article.

Sorry for the excessively long delay in responding! In this case, you just need to enter the location of your existing Let's Encrypt certificate in the /etc/ipsec.conf and /etc/ipsec.secrets configuration files. The VPN and the webserver will need to be on the same server for this to work.

John Doe - Sun, 17/10/2021 - 04:18

Permalink

Nice and short howto...
But shouldn't the fist two iptables rules start with:

iptables -t nat -A POSTROUTING .......

Regards, John

Add new comment

CAPTCHA