strongSwan is an open source IPsec implementation for Linux that speaks IKEv2, the standard used by most firewalls and cloud VPN gateways. That makes it the right choice when you need an encrypted tunnel that also interoperates with appliances such as pfSense, FortiGate or Cisco routers. In this tutorial you will connect two private networks through an IKEv2 site-to-site tunnel on Ubuntu 24.04, authenticated with certificates from your own small certificate authority, and configured with the modern swanctl interface.
Prerequisites
To follow this tutorial you need:
- Two servers running Ubuntu 24.04 LTS that act as the VPN gateway of each site, for example two CubePath VPS.
- A non-root user with
sudoprivileges on both gateways. - A static public IP on each gateway.
- UDP ports 500 and 4500 and the ESP protocol (IP protocol 50) allowed between the gateways.
- Two private networks whose address ranges do not overlap.
This guide uses these example values:
| Value | Site A | Site B |
|---|---|---|
| Public IP of the gateway | 203.0.113.10 | 198.51.100.20 |
| IKE identity (certificate name) | site-a.example.com | site-b.example.com |
| LAN behind the gateway | 192.168.1.0/24 | 192.168.2.0/24 |
| Gateway LAN interface | eth1 | eth1 |
The identities only need to match the certificates; they do not have to resolve in DNS.
Step 1 - Installing strongSwan
strongSwan has two configuration front ends: the legacy ipsec.conf with the ipsec command, and swanctl with swanctl.conf. This guide uses swanctl, which is the one upstream develops and documents today. Install the IKE daemon for systemd, the swanctl tool and the pki utility on both gateways:
sudo apt update
sudo apt install charon-systemd strongswan-swanctl strongswan-pki
The charon-systemd package provides the strongswan service. Check that it is running:
sudo systemctl status strongswan
● strongswan.service - strongSwan IPsec IKEv1/IKEv2 daemon using swanctl
Loaded: loaded (/usr/lib/systemd/system/strongswan.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-25 10:04:11 UTC; 12s ago
Step 2 - Enabling IP forwarding
The gateways forward packets between their LAN and the tunnel. Enable IPv4 forwarding on both of them:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ipsec.conf
sudo sysctl --system
Verify it:
sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
Step 3 - Creating a certificate authority and gateway certificates
Certificates are stronger than a pre-shared key and let you add more sites later without sharing a secret between all of them. You will create a private CA and one certificate per gateway. Do this on Site A in a private working directory, so the CA key never touches Site B.
Create the directory:
mkdir -m 700 ~/vpn-pki && cd ~/vpn-pki
Generate the CA key and a self-signed CA certificate valid for 10 years:
pki --gen --type ecdsa --size 384 --outform pem > ca.key
pki --self --ca --lifetime 3652 --in ca.key --dn "CN=Example VPN CA" --outform pem > ca.crt
Generate a key and a certificate for Site A. The --san value is the identity the gateway will present during IKE:
pki --gen --type ecdsa --size 384 --outform pem > site-a.key
pki --pub --in site-a.key | pki --issue --lifetime 1825 --cacert ca.crt --cakey ca.key \
--dn "CN=site-a.example.com" --san site-a.example.com --outform pem > site-a.crt
Repeat for Site B:
pki --gen --type ecdsa --size 384 --outform pem > site-b.key
pki --pub --in site-b.key | pki --issue --lifetime 1825 --cacert ca.crt --cakey ca.key \
--dn "CN=site-b.example.com" --san site-b.example.com --outform pem > site-b.crt
Inspect one of the certificates to confirm the subject, the alternative name and the issuer:
pki --print --in site-b.crt
subject: "CN=site-b.example.com"
issuer: "CN=Example VPN CA"
validity: not before Sep 25 10:10:02 2026, ok
not after Sep 24 10:10:02 2031, ok (expires in 1825 days)
altNames: site-b.example.com
Step 4 - Installing the certificates
swanctl loads credentials from fixed directories under /etc/swanctl. On Site A, copy the CA certificate, its own certificate and its own key:
sudo cp ca.crt /etc/swanctl/x509ca/
sudo cp site-a.crt /etc/swanctl/x509/
sudo cp site-a.key /etc/swanctl/private/
sudo chmod 600 /etc/swanctl/private/site-a.key
Copy ca.crt, site-b.crt and site-b.key to Site B over SSH:
scp ca.crt site-b.crt site-b.key [email protected]:~
Then, on Site B, move them into place and remove the copies from the home directory:
sudo mv ~/ca.crt /etc/swanctl/x509ca/
sudo mv ~/site-b.crt /etc/swanctl/x509/
sudo mv ~/site-b.key /etc/swanctl/private/
sudo chown root:root /etc/swanctl/x509ca/ca.crt /etc/swanctl/x509/site-b.crt /etc/swanctl/private/site-b.key
sudo chmod 600 /etc/swanctl/private/site-b.key
Finally, on Site A, remove the Site B key from the working directory, and move ca.key somewhere offline. Anyone who holds it can issue certificates that your gateways will trust.
rm ~/vpn-pki/site-b.key
Step 5 - Configuring the tunnel on Site A
A swanctl connection has two layers: the IKE SA, which authenticates the gateways, and one or more CHILD SAs, which carry the actual traffic between the networks listed in local_ts and remote_ts (traffic selectors).
Create the connection file on Site A:
sudo nano /etc/swanctl/conf.d/site-to-site.conf
connections {
site-b {
version = 2
local_addrs = 203.0.113.10
remote_addrs = 198.51.100.20
proposals = aes256gcm16-prfsha384-ecp384
dpd_delay = 30s
local {
auth = pubkey
certs = site-a.crt
id = site-a.example.com
}
remote {
auth = pubkey
id = site-b.example.com
}
children {
lan {
local_ts = 192.168.1.0/24
remote_ts = 192.168.2.0/24
esp_proposals = aes256gcm16-ecp384
start_action = trap
dpd_action = restart
}
}
}
}
What the important settings do:
proposalsandesp_proposalsfix the algorithms to AES-256-GCM with the P-384 curve. Both sides must share at least one proposal.remote.idpins the identity that Site B must present. A certificate signed by the same CA but with a different name is rejected.start_action = trapinstalls the IPsec policy immediately and brings the tunnel up the first time traffic matches it.dpd_delayanddpd_action = restartdetect a dead peer and rebuild the tunnel.
Step 6 - Configuring the tunnel on Site B
Site B gets the mirrored configuration:
sudo nano /etc/swanctl/conf.d/site-to-site.conf
connections {
site-a {
version = 2
local_addrs = 198.51.100.20
remote_addrs = 203.0.113.10
proposals = aes256gcm16-prfsha384-ecp384
dpd_delay = 30s
local {
auth = pubkey
certs = site-b.crt
id = site-b.example.com
}
remote {
auth = pubkey
id = site-a.example.com
}
children {
lan {
local_ts = 192.168.2.0/24
remote_ts = 192.168.1.0/24
esp_proposals = aes256gcm16-ecp384
start_action = trap
dpd_action = restart
}
}
}
}
Step 7 - Opening the firewall
IKE uses UDP 500, and UDP 4500 when NAT is detected. Without NAT, the encrypted payload travels as ESP packets. The gateways must also forward the decrypted traffic between the LANs, which UFW blocks by default.
On Site A:
sudo ufw allow from 198.51.100.20 to any port 500,4500 proto udp
sudo ufw allow proto esp from 198.51.100.20 to any
sudo ufw route allow from 192.168.1.0/24 to 192.168.2.0/24
sudo ufw route allow from 192.168.2.0/24 to 192.168.1.0/24
On Site B, use the same commands with 203.0.113.10 as the source address.
If UFW is not enabled yet, run sudo ufw allow OpenSSH before sudo ufw enable. Verify the rules:
sudo ufw status
To Action From
-- ------ ----
500,4500/udp ALLOW 198.51.100.20
Anywhere/esp ALLOW 198.51.100.20
192.168.2.0/24 ALLOW FWD 192.168.1.0/24
192.168.1.0/24 ALLOW FWD 192.168.2.0/24
ImportantIf the gateway also masquerades (NATs) its LAN to the internet, the NAT rule must not match traffic going to the remote LAN, or the packets are rewritten before IPsec sees them and never enter the tunnel. Exclude the remote subnet from any
MASQUERADErule.
Step 8 - Loading the configuration and bringing up the tunnel
Load the certificates, keys and connections into the running daemon on both gateways:
sudo swanctl --load-all
loaded certificate from '/etc/swanctl/x509/site-a.crt'
loaded certificate from '/etc/swanctl/x509ca/ca.crt'
loaded ecdsa key from '/etc/swanctl/private/site-a.key'
no authorities found, 0 unloaded
no pools found, 0 unloaded
loaded connection 'site-b'
successfully loaded 1 connections, 0 unloaded
The daemon also loads everything in /etc/swanctl at boot, so no extra service configuration is needed. Now start the tunnel from Site A:
sudo swanctl --initiate --child lan
The output ends with initiate completed successfully. Check the security associations:
sudo swanctl --list-sas
site-b: #1, ESTABLISHED, IKEv2, 7c1b2e0f9a3d4c55_i* 1e4f6a8b2c9d0e71_r
local 'site-a.example.com' @ 203.0.113.10[500]
remote 'site-b.example.com' @ 198.51.100.20[500]
AES_GCM_16-256/PRF_HMAC_SHA2_384/ECP_384
established 21s ago, rekeying in 13847s
lan: #1, reqid 1, INSTALLED, TUNNEL, ESP:AES_GCM_16-256
installed 21s ago, rekeying in 3291s, expires in 3939s
in c8a31f02, 504 bytes, 6 packets
out ce5b7a19, 504 bytes, 6 packets
local 192.168.1.0/24
remote 192.168.2.0/24
ESTABLISHED confirms that both gateways authenticated each other, and INSTALLED means the CHILD SA carrying LAN traffic is active.
Step 9 - Testing traffic between the networks
IPsec policies match by source and destination address, so the test must use LAN addresses. Hosts on each LAN also need a route to the remote LAN through their local gateway: if the strongSwan server is not their default gateway, add a static route on the LAN router or on the hosts, for example sudo ip route add 192.168.2.0/24 via 192.168.1.1 on a Site A host.
From a host in Site A, ping a host in Site B:
ping -c 3 192.168.2.50
64 bytes from 192.168.2.50: icmp_seq=1 ttl=62 time=21.3 ms
64 bytes from 192.168.2.50: icmp_seq=2 ttl=62 time=21.1 ms
64 bytes from 192.168.2.50: icmp_seq=3 ttl=62 time=21.2 ms
To test from the gateway itself, set the source to its LAN address (192.168.1.1 in this example), otherwise the packet leaves with the public IP and does not match the policy:
ping -c 3 -I 192.168.1.1 192.168.2.50
Run sudo swanctl --list-sas again: the in and out packet counters of the lan CHILD SA should increase.
Troubleshooting
Read the daemon log first. It records every IKE exchange and the reason for any failure:
sudo journalctl -u strongswan -f
NO_PROPOSAL_CHOSEN. The two sides do not share an algorithm set. Make proposals and esp_proposals identical on both gateways, or add the peer's algorithms when connecting to a third-party appliance.
constraint check failed: identity '...' required or AUTHENTICATION_FAILED. The remote.id does not match the peer's certificate SAN, or the peer's certificate was not signed by the CA in /etc/swanctl/x509ca. Check both with pki --print --in.
TS_UNACCEPTABLE. The traffic selectors do not mirror each other. Site A's local_ts must equal Site B's remote_ts and the other way round.
The tunnel is up but no packets flow. Confirm IP forwarding, the UFW route allow rules, the return route on the LAN hosts, and that no NAT rule rewrites traffic to the remote subnet.
After any change to a file in /etc/swanctl/conf.d, run sudo swanctl --load-all again.
Conclusion
You built an IKEv2 site-to-site VPN with strongSwan, authenticated by certificates from a private CA, with strict algorithm proposals, dead peer detection and a firewall that only forwards traffic between the two LANs. Next, you can add more sites by issuing another certificate and adding a connection per peer, set up remote access for laptops with an IKEv2 road warrior configuration, or plan certificate renewal well before the five-year lifetime expires.
