Skip to content

Instantly share code, notes, and snippets.

@borisovonline
Last active March 2, 2024 23:27
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save borisovonline/955b7c583c049464c878bbe43329a521 to your computer and use it in GitHub Desktop.
Save borisovonline/955b7c583c049464c878bbe43329a521 to your computer and use it in GitHub Desktop.
Create a personal VPN server based on Linux Debian and strongSwan

Tutorial: how to create a personal VPN server based on Linux Debian, strongSwan, certificates authentification and ready to use .mobileconfig profiles to use on iPhone, iPad and Mac

This is a part of a big article I posted on Medium. Here is only a tutorial.

We're going to create a personal VPN server, using the following technologies:

  • IKEv2 as a VPN protocol
  • Linux Debian as a server OS
  • strongSwan as a VPN server
  • Certificates as an authentication method

You can use this tutorial on any hosting you prefer.

The tutorial has been checked for errors and has been applied many times so it will work for sure on a clean system with no errors. Configuring will take from 15 min, depending on your work speed.

Configuring Debian

We're going to make all manipulations under a root user. Let’s go:

# sudo su

First of all, update packets indexes in repositories, probably there might be updates:

# apt-get update

And then install these updates:

# apt-get upgrade

Installing strongSwan

Let’s install stongSwan:

# apt-get install strongswan

We’ll get back to the detailed configure of strongSwan a little bit later, but now let’s create certificates for our devices so they will be able to connect by VPN.

Generating access certificates

We'll use self-signed certificates since the VPN server will be used only by us. To create certificates we need a strongswan-pki packet. Let’s install it:

# apt-get install strongswan-pki

Moving to certificates creation.

First of all we have to create a root certificate CA (Certificate Authority), which will issue other certificates. Create it in a ca.pem file:

# 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=YOUR_EXTERNAL_VPS_IP" \
> --outform pem > cacerts/ca.pem

Then create server’s private key certificate in the debian.pem file:

# 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=YOUR_EXTERNAL_VPS_IP" \
> --san YOUR_EXTERNAL_VPS_IP \
> --flag serverAuth --outform pem > certs/debian.pem

And now create the certificate for our devices in the device.pem file:

# 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

Let’s remove the ca.pem file for safety as we don’t need it anymore:

# rm /etc/ipsec.d/private/ca.pem

Certificates creation is completed.

If certificates are being created too long

If your certificates are being created too long, more than five seconds, for example, it could evidence about the low level of entropy. I've encountered the same situation in my time with Hetzner’s cloud servers. The entropy was too low and certificates creation time was stretching to 40-50 minutes, so I decided to refuse their services.

You can check the level of the entropy, running one more session in a new tab:

$ cat /proc/sys/kernel/random/entropy_avail

This command will show you the level of the entropy for the moment of a request. To monitor the entropy in real time, execute a command:

$ watch -n 0.25 cat /proc/sys/kernel/random/entropy_avail

If the entropy is less than 200, I would recommend you to change a hosting company. Or you can install haveged packet, which allegedly generates the entropy, but you do that at your own risk.

To exit a request press Ctrl+Z.

Configuring strongSwan itself

Clear a default strongSwan config with a command:

# > /etc/ipsec.conf

And create a new one using nano text editor:

# nano /etc/ipsec.conf

Paste the following text into it, replacing YOUR_EXTERNAL_VPS_IP to an external static IP of your VPS machine:

	include /var/lib/strongswan/ipsec.conf.inc
	
	config setup
	        uniqueids=never
	        charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2,  mgr 2"
	
	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
	        leftsourceip=YOUR_EXTERNAL_VPS_IP
	        leftid=YOUR_EXTERNAL_VPS_IP
	        leftcert=debian.pem
	        leftsendcert=always
	        leftsubnet=0.0.0.0/0
	        right=%any
	        rightauth=pubkey
	        rightsourceip=10.10.10.0/24
	        rightdns=8.8.8.8,8.8.4.4
	
	conn ikev2-pubkey
	        auto=add

Caution! strongSwan is meticulous about spacing on its config file, so check that each parameter of config parts is space-separated with Tab as it shown in the example, or at least with one space, otherwise strongSwan won't run.

Save the file with Ctrl+X and let's move forward.

Add a point to the server certificate to ipsec.secrets file, which is a repository of certificates links and auth keys:

# nano /etc/ipsec.secrets

include /var/lib/strongswan/ipsec.secrets.inc


: RSA debian.pem

With that, we have finished strongSwan setup, let’s restart the service:

# ipsec restart

If everything is ok, the server will restart:

Starting strongSwan 5.5.1 IPsec [starter]...

If it fails, we can look at what has really happened, by reading the system log. The command will show you the last 50 log lines:

# tail -n 50 > /var/log/syslog

Let’s configure a core network parameters

Now we need to add some changes to the sysctl.conf file.

# nano /etc/sysctl.conf

Using Ctrl+W find the following variables and make changes to them:

#Uncomment this variable to enable packets forwarding
net.ipv4.ip_forward = 1

#Uncomment this variable to prevent MITM-attacks
net.ipv4.conf.all.accept_redirects = 0

#Uncomment this variable to disable the sending of ICMP-redirects 
net.ipv4.conf.all.send_redirects = 0

...

#In any place of the file on a new line add this variable to permit PMTU searching
net.ipv4.ip_no_pmtu_disc = 1

Load new values:

# sysctl -p

Configuration of the core network parameters is completed.

Configuring iptables

iptables is a tool which manages Linux internal firewall called netfilter. To save and autoload iptables rules after each system restart install iptables-persistent packet:

# apt-get install iptables-persistent

We will be asked whether to save the current IPv4 and IPv6 rules or not. Answer “No”, because we have a clean system and, in fact, there is nothing to save.

Let’s move to the formation of iptables rules.

Just in case, let’s clear all chains:

# iptables -P INPUT ACCEPT
# iptables -P FORWARD ACCEPT
# iptables -F
# iptables -Z

Allow connection for a SSH at port 22 in order not to lose access to the machine:

# iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# iptables -A INPUT -p tcp --dport 22 -j ACCEPT

Allow connections on loopback-interface:

# iptables -A INPUT -i lo -j ACCEPT

Now allow incoming IPSec connections at UDP ports 500 and 4500:

# iptables -A INPUT -p udp --dport  500 -j ACCEPT
# iptables -A INPUT -p udp --dport 4500 -j ACCEPT

Allow ESP-traffic forwarding:

# iptables -A FORWARD --match policy --pol ipsec --dir in  --proto esp -s 10.10.10.0/24 -j ACCEPT
# iptables -A FORWARD --match policy --pol ipsec --dir out --proto esp -d 10.10.10.0/24 -j ACCEPT

Configure traffic masquerading since our VPN server acts as a gateway between VPN clients and the Internet:

# iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -m policy --pol ipsec --dir out -j ACCEPT
# iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o eth0 -j MASQUERADE

Adjust the packets maximum segment size:

# iptables -t mangle -A FORWARD --match policy --pol ipsec --dir in -s 10.10.10.0/24 -o eth0 -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1536 -j TCPMSS --set-mss 1360

Deny any other connections to the server:

# iptables -A INPUT -j DROP
# iptables -A FORWARD -j DROP

Save the rules to autoload them after each system restart:

# netfilter-persistent save
# netfilter-persistent reload

iptables configuration is completed.

Let’s reboot the server:

# reboot

And find out if iptables is working or not:

$ sudo su
# iptables -S

root@XX.XX.XX.XX:/home/admin# iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p udp -m udp --dport 500 -j ACCEPT
-A INPUT -p udp -m udp --dport 4500 -j ACCEPT
-A INPUT -j DROP
-A FORWARD -s 10.10.10.0/24 -m policy --dir in --pol ipsec --proto esp -j ACCEPT
-A FORWARD -d 10.10.10.0/24 -m policy --dir out --pol ipsec --proto esp -j ACCEPT
-A FORWARD -j DROP

Yes, it’s working.

Also, check if strongSwan is working or not:

# ipsec statusall

root@XX.XX.XX.XX:/home/admin# ipsec statusall
Status of IKE charon daemon (strongSwan 5.5.1, Linux 4.9.0-8-amd64, x86_64):
  uptime: 71 seconds, since Jan 23 23:22:16 2019
  ...

Yes, it’s working.

Creating .mobileconfig for iPhone, iPad and Mac

We’re going to use one VPN profile .mobileconfig for all our devices: iPhone, iPad and Mac. We’ll create such type of config called “On Demand”. It automatically initiates a VPN connection when any service or app needs the Internet. Thus, we’ll avoid the situation when we forget to initiate a VPN connection manually after device restarts, for example, and the traffic goes via main Internet channel of your provider which we really don’t want to happen.

Download the script which generates a .mobileconfig for us:

# wget https://gist.githubusercontent.com/borisovonline/955b7c583c049464c878bbe43329a521/raw/966e8a1b0a413f794280aba147b7cea0661f77a8/mobileconfig.sh

We need zsh packet for the script to work properly, install it:

# apt-get install zsh

Edit the name of the server on your taste and fill an external static IP of your VPS machine, which we used when created certificates:

# nano mobileconfig.sh
...
	
SERVER="My Personal VPN"
FQDN="YOUR_EXTERNAL_VPS_IP"
	
...

Run the script and get the ready iphone.mobileconfig file:

$ ./mobileconfig.sh > iphone.mobileconfig

Take this file from the server with the help of any SFTP clients, Transmit or Cyberduck, for example, and send it to all of your devices via Airdrop. Confirm the configuration installation on your devices.

All done! VPN connection will be established automatically.

Clean up afterwards:

# rm mobileconfig.sh
# rm iphone.mobileconfig

Pumping a SSH security — optional step

Our VPN server is already working and is protected well, however I suggest pumping a SSH security a little bit more.

To prevent botnets knocking in our SSH via default port guessing passwords and leaving lots of garbage in logs, let’s change the default SSH port for anything different. Also, let’s make some other changes to SSH config.

You can choose any port to your taste starting 1024, however, I recommend finding such port which has no history of being used by viruses, trojans and also isn’t used by any popular services, applications or equipment manufacturers. Find such “clean” port on SpeedGuide or adminsubnet.

In our tutorial we’re going to use the port 45323.

Caution! Don’t restart SSH and iptables services and don’t restart the machine, until you have finished this part to the end. Otherwise, you will lose access to the machine!

Now let’s configure a SSH itself:

# nano /etc/ssh/sshd_config

#Uncomment and place the new port
Port 45323

#Uncomment and deny connection attempts with an empty password
PermitEmptyPasswords no

#Uncomment and configure automatic disconnect after 360 seconds of inactivity. This will be useful if you forget that you have an active SSH session on your computer and leave your place. Server will automatically close the SSH connection after 6 minutes.
ClientAliveInterval 360
ClientAliveCountMax 0

Now let’s update iptables rules and change the old SSH port with a new one:

# nano /etc/iptables/rules.v4

Replace “22” in this line:

-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT

to “45323”:

-A INPUT -p tcp -m tcp --dport 45323 -j ACCEPT

Save the file via Ctrl+X and restart the server:

# reboot

After server restart check the SSH connection using the new port with adding a “-p” variable:

$ ssh -i YOUR_DOWNLOADED_KEY.pem admin@YOUR_LIGHTSAIL_IP -p 45323

It all should work.

#!/bin/zsh
CLIENT="me"
SERVER="AWS Frankfurt"
FQDN="YOUR_LIGHTSAIL_IP"
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>Connect</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
@kiber-kot
Copy link

Когда процедуру сделал по созданию mobileconfig и далее загрузка на телефон, при подключении показывает ошибку
"VPN Conection An unexpected error occurred"

Так же заметил, что в данном профиле имеются два сертификата

@vBrestkin
Copy link

vBrestkin commented Jan 30, 2022

Доброго времени суток!
А подскажите, пожалуйста, а как можно сгенерировать конфиг .mobileconfig так, чтобы он не подключался в режиме "On Demand", а наоборот для подключения требовалось бы заходить в настройки и включать галочку?
И еще вопрос, я использую помимо статичного ip свой под домен на одном из доменов чтобы заходить на машину, могу я в описании во всех местах поменять YOUR_LIGHTSAIL_IP на свой домен? Или в каких-то случаях обязателен именно IP адрес?

@ArturRuZ
Copy link

Когда процедуру сделал по созданию mobileconfig и далее загрузка на телефон, при подключении показывает ошибку "VPN Conection An unexpected error occurred"

Так же заметил, что в данном профиле имеются два сертификата

Решилось перегенерацией ключей на 2048 https://serverfault.com/questions/1066598/iphone-users-does-not-connect-to-strongswan-vpn-while-android-and-windows-10-us

@MikhailLipanin
Copy link

MikhailLipanin commented Nov 27, 2023

Доброго времени суток! А подскажите, пожалуйста, а как можно сгенерировать конфиг .mobileconfig так, чтобы он не подключался в режиме "On Demand", а наоборот для подключения требовалось бы заходить в настройки и включать галочку?

в конфиге поменять вручную значение:

<key>OnDemandEnabled</key>
<integer>1</integer>
<key>OnDemandRules</key>

на

<key>OnDemandEnabled</key>
<integer>0</integer>
<key>OnDemandRules</key>

@glebboyko
Copy link

Последний этап (создание .mobileconfig) не работает на Debian 12 (почему-то openssl генерирует некорректный сертификат). Причем если сделать это на Debian 10 (файлы .pem копирую с Debian12), то генерируется корректный .mobileconfig, и становится возможно подключится к серверу с Debian12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment