"Perilous to us all are the devices of an art deeper than we possess ourselves." -- Gandalf
This is a mindless walk-through that shows one way of setting up an IKEv2 certificate-based VPN server on Linux that works with Windows, iOS, Android, Linux, and MacOS clients. It's not a tutorial or explanation of the technology. References are cited where you can learn more.
This procedure has been tested on Fedora 32-39 with Libreswan 4.11.x and OpenSSL 3. It works with all the clients described as of the revision date displayed at the top of this page. (Recipes like this tend to be ephemeral, so take note of that date.)
The arrival of openss 3.x caused problems with Apple products: The regular method of exporting .p12 certificates produced files that couldn't be imported on iOS and MacOS. (Don't worry about this if this is your first visit to this site.)
This kind of VPN is easy to configure and easy to use. No nagging about passwords once it's all working. And unless you've made some special friends at the NSA, it's reasonably secure.
But this security and ease of use comes at a price: If someone steals your laptop or phone and manages to log in, they can waltz around your LAN like they own the place. It's your responsibility to secure all mobile devices because there's no other barrier to your home or office.
We begin on the VPN server which is named "magoo."
dnf install libreswan
More than one server process is involved when hosting a VPN. The service that deals with cryptography and the associated certificates is called "ipsec" - The ipsec service expects to find its certificates in an sqlite database located in:
/var/lib/ipsec/nss/cert9.db
The filename "cert9.db" is conventional for nss certificate databases. A typical Linux system will have several cert9.db files associated with other packages.
We will be working with two utiltites: "certutil" and "pk12util". Both need to know where the certificate database is located. Deal with that by defining an environment variable that points to this directory:
export mynss=/var/lib/ipsec/nss
List contents of $mynss. If you don't see a cert9.db file, initialize the database using:
certutil -N -d $mynss
In this example, "MagooCorp" is the organization name. "Magoo CA" is the common name of the certificate authority.
certutil -S -x -n "Magoo CA" -s "O=MagooCorp,CN=Magoo CA" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t "CT,," -2
You will be prompted for answers:
Q: Is this a CA certificate?
A: y
Q: Enter the path length constraint, enter to skip...
A: <Enter>
Q: Is this a critical extension?
A: N
The following configuration uses the IP number of the VPN server instead of the IP name. My VPN server is also my internet gateway and it provides services to clients on the LAN that I don't want to make publicly available. The DNS server (bind) is configured to use "views" - My server IP name resolves to its local IP address when used by internal clients and to the public IP number for external clients. The firewall is more permissive for numbers on the internal LAN. But the VPN and associated certificates need a consistent way of refering to the server. So I used the number.
Assign a temporary shell variable for the server's public IP address
PUBLIC_IP=12.34.56.68
This is only needed when creating the server certificate.
certutil -S -c "Magoo CA" -n "Magoo VPN" -s "O=MagooCorp,CN=Magoo VPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth \
--extSAN "ip:$PUBLIC_IP,dns:$PUBLIC_IP"
Create the file:
/etc/ipsec.d/magoo.conf
That contains:
conn magoo
left=12.34.56.68
leftcert="Magoo VPN"
leftid=%fromcert
leftsendcert=always
leftsubnet=0.0.0.0/0
leftrsasigkey=%cert
leftmodecfgserver=yes
right=%any
rightid=%fromcert
rightaddresspool=192.168.1.200-192.168.1.254
rightca=%same
rightrsasigkey=%cert
rightmodecfgclient=yes
modecfgdns=192.168.1.2
narrowing=yes
dpddelay=30
dpdtimeout=120
dpdaction=clear
auto=add
ikev2=insist
rekey=no
fragmentation=yes
mobike=yes
authby=rsasig
pfs=no
Note: On my system, the "left" parameter can't be %defaultroute. I think this is because I have multiple IP numbers on the same adapter. It also can't be the IP name of the server because that gets resolved to a local IP inside the LAN. So the number must be used literally.
The following expressions need to be worked into your existing firewall script. I assume you have a functioning firewall that lets your LAN clients access the internet. If you're creating a firewall from scratch, these rules are not sufficient.
Do not execute these commands if your system uses firewalld. See references at the end of this article for complete examples using iptables or firewalld.
iptables -A INPUT -p udp --dport 500 -j ACCEPT
iptables -A INPUT -p udp --dport 4500 -j ACCEPT
iptables -A INPUT -p esp -j ACCEPT
iptables -A INPUT -p ah -j ACCEPT
iptables -I INPUT -m policy --pol ipsec --dir in -j ACCEPT
iptables -I FORWARD -m policy --pol ipsec --dir in -j ACCEPT
iptables -t nat -I POSTROUTING -m policy --pol ipsec --dir out -j ACCEPT
Enable port forwarding in your firewall script with the line:
echo 1 > /proc/sys/net/ipv4/ip_forward
This could also be done by adding another kernel parameter:
net.ipv4.ip_forward = 1
Installing the libreswan package takes care of setting many kernel parameters. I found it necessary to add two more:
Create this file:
/etc/sysctl.d/51-libreswan.conf
Containing:
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
These expressions set the reverse path forwarding filter policy to "loose=2". The default setting in Fedora 31 is "strict=1". The VPN described here won't work with that setting.
See: Need for loose reverse path filtering
Assign the new kernel parameters without rebooting by executing:
sysctl -p /etc/sysctl.d/51-libreswan.conf
systemctl enable --now ipsec
The server is now ready to accept connections.
The following sections show how to connect clients to your new VPN. For each client, we must create a certificate, copy it to the client and install it in the client's certificate store. Details of this procedure vary depending on the client's operating system.
In this example, the phone belongs to "Hugh."
certutil -S -c "Magoo CA" -n "Hugh iPhone to MagooVPN" -s "O=MagooCorp,CN=Hugh iPhone to MagooVPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth,clientAuth -8 "Hugh iPhone to MagooVPN"
As of openssl 3, iOS won't import certificates exported with the standard linux utility pk12util.
The standard way: (does not work for any Apple product)
pk12util -d $mynss -n "Hugh iPhone to MagooVPN" -o hugh_iphone.p12
The new way (requires this short script):
pk12extract -d $mynss -n "Hugh iPhone to MagooVPN" -o hugh_iphone.p12
You will be prompted for a password. The iPhone won't import a certificate with a null password.
certutil -L -d $mynss -n "Magoo CA" -a -o magoo_ca.cer
Note: Only iOS needs a separate CA file. The .p12 file contains this information but Apple "thinks different."
These files will appear in the current directory:
magoo_ca.cer
hugh_iphone.p12
Enclose them in an email and send it to the iphone owner. There are other ways using the iOS Files application.
On the phone, open the mail and click on "magoo_ca.cer" It will transfer the certificate to the certificate store on the phone. The certificate is in a pending state - not yet imported.
Go the the certificate store here:
Settings -> General -> Profiles
Click on the new certificate and click a series of more-or-less obvious buttons to get it "really imported."
Go back to the email and do the same thing with the "hugh_iphone.p12" client certificate. This time while it's in the pending state, it will be displayed as an anonymous "Identity Certificate" - The phone can't read the actual CA name inside until the encryption is removed. You'll be prompted to supply the password when the import step occurs. When the process is finished it will appear in the store as "Hugh iPhone".
It is best to delete the exported certificate files.
Go to:
Settings -> General -> About -> Certificate Trust Settings ->
ENABLE FULL TRUST FOR ROOT CERTIFICATES
Next to MagooCA, turn on the switch.
Go to:
Settings -> General -> VPN
Click "Add VPN connection" and fill in the form:
You are ready to make a connection.
In this example, the Windows box is named "Asus."
certutil -S -c "Magoo CA" -n "Asus to MagooVPN" -s "O=MagooCorp,CN=Asus to MagooVPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth,clientAuth -8 "Asus to MagooVPN"
pk12util -d $mynss -n "Asus to MagooVPN" -o asus_magoo.p12
If you're on a LAN, just copy it to the client's desktop. Otherwise mail it as an enclosure and save it on the client desktop.
It's a good idea to delete "asus_magoo.p12" now.
This is not an option: Modern version of Librswan won't work with Microsoft's default 1024 bit encryption.
Create a plain text file:
High Security VPN - Install.reg
Containing:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RasMan\Parameters]
"NegotiateDH2048_AES256"=dword:00000001
Right-click on the file and select "Merge"
A sad development circa 2023: Microsoft insists on using SHA1 for the certificate signature algorithm. This is conformant to the RFC for IKEv2, but modern clients (anything but Windows) will now offer a stronger algorithm if SHA1 is rejected. Fedora Linux 39, for example, rejects SHA1 signatures by default. When a Windows client tries to connect, the user will see the message:
Can't connect to MagooVPN
Error processing Signature payload
On the linux side, syslog will show the message:
NSS: SGN_Digest(SHA-1) function failed:
SEC_ERROR_SIGNATURE_ALGORITHM_DISABLED:
Could not create or verify a signature
using a signature algorithm that is disabled
because it is not secure.
The only recourse I've discovered so far is to relax security on the server side. In a root shell windows execute:
update-crypto-policies --set LEGACY
This also works:
update-crypto-policies --set DEFAULT:SHA1
In either case, you are prompted to reboot the server.
Now you're ready to connect.
In this example, the Android device is named "Hughpad."
certutil -S -c "Magoo CA" -n "Hughpad to MagooVPN" -s "O=MagooCorp,CN=Hughpad to MagooVPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth,clientAuth -8 "Hughpad to MagooVPN"
Newer versions of Android OS that use openssl 3 won't import certificates exported with the standard linux utility pk12util.
The standard way: (does not work for newer Android devices)
pk12util -d $mynss -n "Hugh iPhone to MagooVPN" -o hugh_iphone.p12
The new way (requires this short script):
pk12extract -d $mynss -n "Hughpad to MagooVPN" -o hughpad_magoo.p12
You will be prompted for a password.
Email it as an attachment and save it on the client. The default location will be something like:
/storage/emulated/0/Download/hughpad_magoo.p12
Run the google play store app, search for and install "strongSwan VPN Client"
When you start the connection, a number of nags, warnings and prompts will appear. One will rant about "Unknown beings are intercepting your internet traffic" or something like that. Just say yes to everything.
You are ready to connect.
In this example, the Linux box is named "Robor."
certutil -S -c "Magoo CA" -n "Robor2MagooVPN" -s "O=MagooCorp,CN=Robor2MagooVPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth,clientAuth -8 "Robor2MagooVPN"
Note: When using the NetworkManager GUI, the certificate nickname cannot contain spaces or underscore characters.
pk12util -d $mynss -n "Robor2MagooVPN" -o robor_magoo.p12
You'll figure something out.
ipsec initnss
pk12util -d $mynss -i robor_magoo.p12
You'll be prompted for the password. The certificate contains the nickname specified when the certificate was created.
From the GUI run:
Network Connections
OR from a console window run:
nm-connection-editor
Connect or disconnect using the network icon on the task bar.
Create the file:
/etc/ipsec.d/magoo.conf
Containing:
conn magoo
left=%defaultroute
leftcert=Robor2MagooVPN
leftmodecfgclient=yes
right=12.34.56.68
rightsubnet=0.0.0.0/0
rightmodecfgserver=yes
narrowing=yes
ikev2=insist
rekey=yes
fragmentation=yes
auto=add
mobike=yes
systemctl enable --now ipsec
ipsec auto --up magoo
ipsec auto --down magoo
In this example, the MacOS box is named "MyMac"
certutil -S -c "Magoo CA" -n "MyMac2MagooVPN" -s "O=MagooCorp,CN=MyMac2MagooVPN" \
-z <(head -c 1024 /dev/urandom) \
-k rsa -g 4096 -v 120 \
-d $mynss \
-t ",," \
--keyUsage digitalSignature,keyEncipherment \
--extKeyUsage serverAuth,clientAuth -8 "MyMac2MagooVPN"
Apple can't deal with certificates exported with the standard pk12util. Instead download and use this short script.:
pk12extract -d $mynss -n "MyMac2MagooVPN" -o MyMac2MagooVPN.p12
You will be prompted for a password. MacOS won't import a certificate with a null password.
You'll figure something out.
Double-click on the MyMac2MagooVPN.p12 file. You'll be prompted for the certificate password. The Keychain Access utility will launch and display two new certificates in the Login keychain area:
MagooCA
MyMac2MagooVPN
I prefer to drag both of these (one at a time) the the System keychain so they can be shared with all users. When you do that, you will be pestered endlessly for the certificate password and your administrator credentials on the mac. Just persist.
The MagooCA certificate will have a red X on the icon because it's untrusted. To fix that, right-click on "CSparksCA"
Get Info -> Trust -> When using this certificate -> Always trust
You may close the Keychain utility.
Run:
System Preferences -> Network -> "+"
PAY ATTENTION: You might think the Authentication Settings should be "Certificate." You thought wrong. After selecting "None", the window will refresh and display two radio buttons: "Shared Secret" and "Certificate":
In the future, you can connect or disconnect using the VPN icon on the task bar.
/usr/libexec/ipsec/addconn --config /etc/ipsec.conf --checkconfig
ipsec verify
ipsec auto --add magoo
journalctl -f -t pluto
ipsec whack --trafficstatus
The trafficstatus command can be used to see if you're actually using the VPN:. Execute the command several times and confirm that the I/O counts are stationary. Then ping an internet site and run the command again. This time, the numbers should go up.
For a continuous view, create a new console window and run the command:
watch -n 1 ipsec whack --trafficstatus
That command will run every second so you can see the I/O counts changing while you browse the web or execute pings in another console window.
When working with NSS, define a shell variable:
export mynss=/var/lib/ipsec/nss
certutil -L -d $mynss
certutil -L -n "Nickname" -d $mynss
certutnil --rename -n OldNickname --new-n NewNickname -d $mynss
certutil -D -n "Nickname" -d $mynss
pk12util -n "Hugh iPhone to MagooVPN" -o hugh_iphone.p12 -d $mynss
The utility will prompt for a password.
pk12util -d $mynss -i robor_magoo.p12
The utitlity will prompt for the password specified when the certificate was exported.
openssl pkcs12 -nokeys -info -in robor_magoo.p12
Some clients want the certificate authority certificate as well as the client certificate. (iPhone is an example.)
certutil -L -d $mynss -n "Magoo CA" -a -o magoo_ca.cer
If someone steals your phone or laptop that has a client certificate, they can access your office LAN if they guess or circumvent the user account login credentials.
The quick and easy thing to do is simply delete the client certificate from the store on the server. But if the phone or laptop is found, a new certificate will have to be installed at both ends.
A more sophisticated approach is to revoke the client certificate. A revoked certificate can be reinstated if the phone or laptop was simply misplaced.
Certificates are revoked by adding them to a certificate revocation list "CRL" - This is an optional feature that must be added to the certificate authority. You only need to do this once: the same CRL can be used to revoke any client certificate signed by the CA.
For the "Magoo CA":
crlutil -G -d $mynss -n "Magoo CA" -c /dev/null
The output will show some information about the new CRL.
Don't accidentally create another CRL for the same CA: You can check for a CLR using the expression:
crlutil -L -d $mynss -n "Magoo CA" -c /dev/null
If there's no output, the CRL doesn't exist.
List the store and note the nickname of the client certificate you want to revoke:
certutil -L -d $mynss
Show the certifcate details and note the serial number:
certutil -L -d $mynss -n "Hugh iPhone"
The serial number appears in the first few lines of the dump. It will look like this:
...
Serial Number:
00:b5:1a:f2:61
Convert the serial number to a decimal value:
echo $((0xb51af261))
Result: (example)
3038442081
Specify the time you want the revocation to occur in generalized time format YYYYMMDDhhmmssZ - The time is in UTC and the Z at the end is required. Example:
20200624053200Z
Add the client certificate to the CRL:
crlutil -M -d $mynss -n "Magoo CA" <<EOF
addcert 3446275956 20200606220100Z
EOF
Tell ipsec to reload the CRL:
ipsec crls
The deed is done.
After a reasonable time, revoked certificates can be removed from the CRL (so it doesn't get too big) and simply deleted from the server certificate store. At that point there is no way to reinstate them.
If you want to reinstate a certificate, simply remove it from the CRL. You need the serial number and the current time:
crlutil -M -d $mynss -n "Magoo CA" <<EOF
rmcert 3446275956 20200606220100Z
EOF
firewall-cmd --zone=public --permanent --add-port=500/udp
firewall-cmd --zone=public --permanent --add-port=4500/udp
firewall-cmd --zone=public --permanent --add-service="ipsec"
firewall-cmd --zone=public --permanent --add-rich-rule='rule protocol value="esp" accept'
firewall-cmd --zone=public --permanent --add-rich-rule='rule protocol value="ah" accept'
firewall-cmd --zone=public --permanent --add-masquerade
(You've probably already done this if your running a server.)
firewall-cmd --reload
/etc/sysctl.d/52-ipsec.conf
Add the following expressions:
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
sysctl -p
systemctl restart ipsec