-
-
Save xpertdev/ee3b1e079250c7d6b5737ed9d3daee09 to your computer and use it in GitHub Desktop.
Setting up a secure VPN with strongSwan on debian
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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](https://www.mythic-beasts.com/). This uses [strongSwan](https://strongswan.org/) and certificate-based IKEv2 authentication. | |
| Assumptions: | |
| - Debian Jessie server already set up and accessible via `debian.example.com`, a public IPv4 of `203.0.113.1` and a public IPv6 of `2001: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](https://github.com/trailofbits/algo). I used that project as a basis for my configuration. | |
| Install strongSwan | |
| ================== | |
| 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 | |
| Build the public key infrastructure | |
| =================================== | |
| 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](https://wiki.strongswan.org/projects/strongswan/wiki/SimpleCA#Certificate-Revocation-Lists-CRL) 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. | |
| Generate an IPv6 ULA | |
| ==================== | |
| Before getting started with configuring strongSwan, you'll want to generate an IPv6 [unique local address](https://en.wikipedia.org/wiki/Unique_local_address) block. The addresses are within the fc00::/7 block and contain a pseudo-random component according to [RFC 4193](https://tools.ietf.org/html/rfc4193). 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 strongSwan and iptables configuration below with what you have. | |
| Configure strongSwan | |
| ==================== | |
| 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=2620:0:ccc::2,2620:0:ccd::2 | |
| 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 use `ike=aes256-sha2_256-prfsha256-ecp256!` and `esp=aes256-sha2_256-ecp256!`. | |
| - I have used the OpenDNS IPv6 resolvers | |
| Next, copy the following `/etc/ipsec.secrets`. | |
| : RSA debian.pem | |
| Then restart strongswan. | |
| $ systemctl restart strongswan | |
| Enable packet forwarding | |
| ======================== | |
| 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 | |
| Configure iptables | |
| ================== | |
| 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 | |
| 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 | |
| COMMIT | |
| *nat | |
| [...] | |
| -A POSTROUTING -s fdf3:5237:bf63::/64 -m policy --pol none --dir out -j MASQUERADE | |
| COMMIT | |
| Configure the clients | |
| ===================== | |
| 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](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW36). | |
| 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. | |
| Conclusion | |
| ========== | |
| 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](https://github.com/trailofbits/algo): a similar setup based on deploying to an Ubuntu cloud server using ansible | |
| - [Andy Smith's blog](http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/): getting IPv6 packet forwarding and SLAAC to work together | |
| - [The strongSwan wiki](https://wiki.strongswan.org/projects/strongswan) | |
| - [Apple's Configuration Profile Reference](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010206-CH1-SW4) | |
| If you have any suggestions for how this guide or setup could be improved, then please [let me know](https://andrewho.co.uk/). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/zsh | |
| CLIENT="me" | |
| SERVER="debian" | |
| FQDN="debian.example.com" | |
| CA="ca" | |
| # WiFi SSIDs that do not require automatic connection to VPN on network change | |
| TRUSTED_SSIDS=("SSID1" "SSID2") | |
| PAYLOADCERTIFICATEUUID=$( cat /proc/sys/kernel/random/uuid ) | |
| PKCS12PASSWORD=$( cat /proc/sys/kernel/random/uuid ) | |
| cat << EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>PayloadDisplayName</key> | |
| <string>${SERVER} VPN</string> | |
| <key>PayloadIdentifier</key> | |
| <string>${(j:.:)${(Oas:.:)FQDN}}</string> | |
| <key>PayloadUUID</key> | |
| <string>$( cat /proc/sys/kernel/random/uuid )</string> | |
| <key>PayloadType</key> | |
| <string>Configuration</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| <key>PayloadContent</key> | |
| <array> | |
| <dict> | |
| <key>PayloadDisplayName</key> | |
| <string>${SERVER} VPN</string> | |
| <key>PayloadDescription</key> | |
| <string>Configure VPN</string> | |
| <key>UserDefinedName</key> | |
| <string>${SERVER}</string> | |
| <key>VPNType</key> | |
| <string>IKEv2</string> | |
| <key>IKEv2</key> | |
| <dict> | |
| <key>RemoteAddress</key> | |
| <string>${FQDN}</string> | |
| <key>RemoteIdentifier</key> | |
| <string>${FQDN}</string> | |
| <key>LocalIdentifier</key> | |
| <string>${CLIENT}</string> | |
| <key>AuthenticationMethod</key> | |
| <string>Certificate</string> | |
| <key>PayloadCertificateUUID</key> | |
| <string>${PAYLOADCERTIFICATEUUID}</string> | |
| <key>CertificateType</key> | |
| <string>RSA</string> | |
| <key>ServerCertificateIssuerCommonName</key> | |
| <string>${FQDN}</string> | |
| <key>EnablePFS</key> | |
| <integer>1</integer> | |
| <key>IKESecurityAssociationParameters</key> | |
| <dict> | |
| <key>EncryptionAlgorithm</key> | |
| <string>AES-128-GCM</string> | |
| <key>IntegrityAlgorithm</key> | |
| <string>SHA2-256</string> | |
| <key>DiffieHellmanGroup</key> | |
| <integer>19</integer> | |
| </dict> | |
| <key>ChildSecurityAssociationParameters</key> | |
| <dict> | |
| <key>EncryptionAlgorithm</key> | |
| <string>AES-128-GCM</string> | |
| <key>IntegrityAlgorithm</key> | |
| <string>SHA2-256</string> | |
| <key>DiffieHellmanGroup</key> | |
| <integer>19</integer> | |
| </dict> | |
| <key>OnDemandEnabled</key> | |
| <integer>1</integer> | |
| <key>OnDemandRules</key> | |
| <array> | |
| <dict> | |
| <key>InterfaceTypeMatch</key> | |
| <string>WiFi</string> | |
| <key>SSIDMatch</key> | |
| <array> | |
| `for x in ${TRUSTED_SSIDS}; echo " <string>$x</string>"` | |
| </array> | |
| <key>Action</key> | |
| <string>Disconnect</string> | |
| </dict> | |
| <dict> | |
| <key>InterfaceTypeMatch</key> | |
| <string>Cellular</string> | |
| <key>Action</key> | |
| <string>Disconnect</string> | |
| </dict> | |
| <dict> | |
| <key>Action</key> | |
| <string>Connect</string> | |
| </dict> | |
| </array> | |
| </dict> | |
| <key>PayloadType</key> | |
| <string>com.apple.vpn.managed</string> | |
| <key>PayloadIdentifier</key> | |
| <string>com.apple.vpn.managed.${SERVER}</string> | |
| <key>PayloadUUID</key> | |
| <string>$( cat /proc/sys/kernel/random/uuid )</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| </dict> | |
| <dict> | |
| <key>PayloadDisplayName</key> | |
| <string>${CLIENT}.p12</string> | |
| <key>PayloadDescription</key> | |
| <string>Add PKCS#12 certificate</string> | |
| <key>PayloadCertificateFileName</key> | |
| <string>${CLIENT}.p12</string> | |
| <key>Password</key> | |
| <string>${PKCS12PASSWORD}</string> | |
| <key>PayloadContent</key> | |
| <data> | |
| $( openssl pkcs12 -export -inkey /etc/ipsec.d/private/${CLIENT}.pem -in /etc/ipsec.d/certs/${CLIENT}.pem -name "${CLIENT}" -certfile /etc/ipsec.d/cacerts/${CA}.pem -password pass:${PKCS12PASSWORD} | base64 ) | |
| </data> | |
| <key>PayloadType</key> | |
| <string>com.apple.security.pkcs12</string> | |
| <key>PayloadIdentifier</key> | |
| <string>com.apple.security.pkcs12.${CLIENT}</string> | |
| <key>PayloadUUID</key> | |
| <string>${PAYLOADCERTIFICATEUUID}</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| </dict> | |
| <dict> | |
| <key>PayloadDisplayName</key> | |
| <string>${SERVER} CA</string> | |
| <key>PayloadDescription</key> | |
| <string>Add CA root certificate</string> | |
| <key>PayloadCertificateFileName</key> | |
| <string>ca.pem</string> | |
| <key>PayloadContent</key> | |
| <data> | |
| $( cat /etc/ipsec.d/cacerts/${CA}.pem | base64 ) | |
| </data> | |
| <key>PayloadType</key> | |
| <string>com.apple.security.root</string> | |
| <key>PayloadIdentifier</key> | |
| <string>com.apple.security.root.${SERVER}</string> | |
| <key>PayloadUUID</key> | |
| <string>$( cat /proc/sys/kernel/random/uuid )</string> | |
| <key>PayloadVersion</key> | |
| <integer>1</integer> | |
| </dict> | |
| </array> | |
| </dict> | |
| </plist> | |
| EOF |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment