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 sudo privileges 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:

ValueSite ASite B
Public IP of the gateway203.0.113.10198.51.100.20
IKE identity (certificate name)site-a.example.comsite-b.example.com
LAN behind the gateway192.168.1.0/24192.168.2.0/24
Gateway LAN interfaceeth1eth1

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:

  • proposals and esp_proposals fix the algorithms to AES-256-GCM with the P-384 curve. Both sides must share at least one proposal.
  • remote.id pins the identity that Site B must present. A certificate signed by the same CA but with a different name is rejected.
  • start_action = trap installs the IPsec policy immediately and brings the tunnel up the first time traffic matches it.
  • dpd_delay and dpd_action = restart detect 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

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.