Context & Why
Connecting on-premises infrastructure to AWS, Azure, or Google Cloud is a routine challenge. When you set up an IPsec site-to-site tunnel on Linux, you must choose between two models: policy-based or route-based.
Older IPsec setups rely on policy-based tunnels. The Linux kernel checks every outgoing packet against a Security Policy Database (SPD). If a packet matches a configured pair of subnets—say, 192.168.1.0/24 talking to 10.0.0.0/16—the kernel encrypts it.
It works, but troubleshooting is painful. You get no network interface in ip link. Standard Linux routing tables cannot direct traffic into the tunnel. Worse, whenever a DevOps team spins up a new subnet like 10.50.0.0/16, you must update ipsec.conf and renegotiate Security Associations (SAs), causing brief connection drops.
Route-based VPNs fix this architectural headache by attaching IPsec to a Virtual Tunnel Interface (VTI). Instead of matching hardcoded IP ranges, encryption policies bind to a numeric mark such as mark = 42. The tunnel negotiates wildcard selectors (0.0.0.0/0 <=> 0.0.0.0/0) and routes traffic through a virtual interface named vti0.
Treat vti0 like any physical network card. You can forward subnets using ip route add, monitor bandwidth in real time with iftop, filter traffic through nftables, or run dynamic routing daemons like FRRouting (FRR) for BGP failover.
We will configure a working tunnel between two endpoints across the public internet:
- Local Gateway (Office / Data Center): Public IP
198.51.100.10, Local LAN192.168.1.0/24, Tunnel VTI IP10.254.0.1/30. - Cloud Gateway (AWS EC2 / GCP Compute Engine / Remote VM): Public IP
203.0.113.20, Cloud VPC10.0.0.0/16, Tunnel VTI IP10.254.0.2/30. - XFRM Mark ID:
42(links the VTI interface to the kernel IPsec state).
Installation
These commands target Ubuntu 22.04/24.04 and Debian 12. If you run RHEL or Rocky Linux, swap apt for dnf and install strongswan and strongswan-libcharon.
1. Install strongSwan Packages
Pull strongSwan and its cryptographic modules from the official repository:
sudo apt update
sudo apt install -y strongswan strongswan-pki libcharon-extra-plugins
2. Enable Linux Kernel Modules
VTI relies on two kernel modules: ip_vti and xfrm4_tunnel. Load them immediately:
sudo modprobe ip_vti
sudo modprobe xfrm4_tunnel
Keep these drivers loaded after a reboot by saving them to /etc/modules-load.d/ipsec-vti.conf:
echo "ip_vti" | sudo tee -a /etc/modules-load.d/ipsec-vti.conf
echo "xfrm4_tunnel" | sudo tee -a /etc/modules-load.d/ipsec-vti.conf
3. Tune Kernel Parameters (sysctl)
Because the gateway forwards packets between interfaces, IP forwarding must stay on. Furthermore, the Linux Reverse Path Filter (rp_filter) defaults to strict mode (1). Strict mode silently drops return packets emerging from vti0 because the routing table mismatch fails validation. Setting loose mode (2) prevents this silent packet loss.
Write these network settings into /etc/sysctl.d/99-ipsec-vti.conf:
sudo tee /etc/sysctl.d/99-ipsec-vti.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.disable_policy = 1
net.ipv4.conf.all.disable_policy = 1
EOF
sudo sysctl --system
Configuration
Step 1: Configure strongSwan IPsec Policies
Open /etc/ipsec.conf on your local gateway. Both leftsubnet and rightsubnet use 0.0.0.0/0 to capture all routed traffic. The parameter mark=42 instructs the XFRM kernel layer to encrypt only packets marked with ID 42:
sudo tee /etc/ipsec.conf <<'EOF'
config setup
charondebug="ike 2, knl 2, cfg 2, net 1, esp 1"
uniqueids=yes
conn %default
keyexchange=ikev2
ike=aes256-sha256-modp2048!
esp=aes256-sha256-modp2048!
dpdaction=restart
dpddelay=30s
dpdtimeout=120s
auto=start
conn vti-tunnel
left=198.51.100.10
leftid=198.51.100.10
leftsubnet=0.0.0.0/0
right=203.0.113.20
rightid=203.0.113.20
rightsubnet=0.0.0.0/0
mark=42
authby=secret
type=tunnel
EOF
Step 2: Store the Pre-Shared Key (PSK)
Generate a strong secret key (at least 32 characters) and save it in /etc/ipsec.secrets. Lock down file permissions so only root can read it:
sudo tee /etc/ipsec.secrets <<EOF
198.51.100.10 203.0.113.20 : PSK "K9#mQ2$zL8!vP5*xR7@wT1^bY4&cN3(e"
EOF
sudo chmod 600 /etc/ipsec.secrets
Step 3: Provision the VTI Device
Link the Linux tunnel interface to mark 42. Standard Ethernet MTU is 1500 bytes. IPsec encapsulation with ESP and SHA256 adds roughly 50 to 73 bytes of overhead, so drop the tunnel MTU to 1418 to avoid fragmented packets:
# Create the tunnel device tied to mark 42
sudo ip tunnel add vti0 mode vti local 198.51.100.10 remote 203.0.113.20 key 42
# Configure point-to-point /30 tunnel IPs
sudo ip addr add 10.254.0.1/30 peer 10.254.0.2 dev vti0
# Bring up the interface
sudo ip link set dev vti0 up
# Set safe MTU accounting for IPsec overhead
sudo ip link set dev vti0 mtu 1418
# Apply loose path filtering to the new virtual interface
sudo sysctl -w net.ipv4.conf.vti0.rp_filter=2
sudo sysctl -w net.ipv4.conf.vti0.disable_policy=1
Step 4: Route Cloud Subnets via vti0
Directing traffic toward your cloud VPC (10.0.0.0/16) requires a single route entry. Any packet sent down vti0 gets tagged with mark 42, caught by XFRM, and encrypted:
sudo ip route add 10.0.0.0/16 dev vti0
Need to add a secondary VPC (like 172.16.0.0/20) next week? Run another ip route add 172.16.0.0/20 dev vti0. No IPsec reloads. No negotiation downtime.
Step 5: Automate Interface Lifecycle
Manual commands vanish upon reboot. Use strongSwan’s leftupdown hook to bring up vti0 when Phase 2 establishes and tear it down cleanly when the tunnel drops.
Save this script as /usr/local/sbin/ipsec-vti.sh:
sudo tee /usr/local/sbin/ipsec-vti.sh <<'EOF'
#!/usr/bin/env bash
VTI_IF="vti0"
VTI_LOCAL="10.254.0.1/30"
VTI_REMOTE="10.254.0.2"
REMOTE_SUBNET="10.0.0.0/16"
VTI_KEY="42"
case "${PLUTO_VERB}" in
up-client)
ip tunnel add "${VTI_IF}" mode vti local "${PLUTO_ME}" remote "${PLUTO_PEER}" key "${VTI_KEY}"
ip addr add "${VTI_LOCAL}" peer "${VTI_REMOTE}" dev "${VTI_IF}"
ip link set dev "${VTI_IF}" mtu 1418 up
sysctl -w "net.ipv4.conf.${VTI_IF}.rp_filter=2" > /dev/null
sysctl -w "net.ipv4.conf.${VTI_IF}.disable_policy=1" > /dev/null
ip route replace "${REMOTE_SUBNET}" dev "${VTI_IF}"
;;
down-client)
ip link set dev "${VTI_IF}" down 2>/dev/null
ip tunnel del "${VTI_IF}" 2>/dev/null
;;
esac
EOF
sudo chmod +x /usr/local/sbin/ipsec-vti.sh
Add leftupdown=/usr/local/sbin/ipsec-vti.sh inside your conn vti-tunnel block in /etc/ipsec.conf.
Step 6: Start strongSwan
Restart the service to initialize the IKE daemon and establish the connection:
sudo systemctl restart strongswan-starter
Verification & Monitoring
Confirm the link in four stages: IKE/ESP status, tunnel interface metrics, end-to-end ping, and raw packet capture.
1. Verify IKE and ESP SAs
Check strongSwan’s state machine using ipsec statusall:
sudo ipsec statusall
Look for confirmed Phase 1 and Phase 2 associations:
Security Associations (1 up, 0 connecting):
vti-tunnel[1]: ESTABLISHED 3 minutes ago, 198.51.100.10[198.51.100.10]...203.0.113.20[203.0.113.20]
vti-tunnel{1}: INSTALLED, TUNNEL, reqid 1, ESP SPIs: c3a48e10_a2099df3
vti-tunnel{1}: 0.0.0.0/0 === 0.0.0.0/0, mark 42
The status INSTALLED on 0.0.0.0/0 === 0.0.0.0/0, mark 42 proves the kernel is ready to encrypt traffic matching mark 42.
2. Inspect Interface Counters
Verify that packets are moving across the virtual interface:
ip -s link show dev vti0
Check the RX and TX byte counters. If TX increments while RX remains at zero, verify your cloud firewall (AWS Security Group or GCP Firewall Rule). Ensure UDP ports 500 and 4500, plus IP protocol 50 (ESP), are open.
3. Test End-to-End Latency and MTU
Send an ICMP echo request to a host inside the cloud VPC (such as 10.0.1.15):
ping -c 4 10.0.1.15
Next, probe for Path MTU issues. Send a 1418-byte packet with the Don’t Fragment (DF) bit set (1390 bytes ICMP payload + 20 bytes IPv4 header + 8 bytes ICMP header = 1418 bytes):
ping -M do -s 1390 -c 2 10.0.1.15
If you see Frag needed and DF set (mtu = 1418), drop the payload size by 8 bytes until it passes, then lower the MTU on vti0. You can also enforce TCP MSS clamping via iptables to protect large HTTP/SSH payloads from packet drops:
sudo iptables -t mangle -A FORWARD -o vti0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
4. Real-Time Packet Capture
Verify encryption by watching cleartext traffic on vti0 and ciphertext on your WAN adapter (eth0).
On your public interface, confirm that only encrypted ESP packets appear:
sudo tcpdump -n -i eth0 host 203.0.113.20 and proto 50
In a separate terminal, monitor vti0 to see the unencrypted application payload:
sudo tcpdump -n -i vti0 icmp
If eth0 shows incoming ESP packets while vti0 shows nothing, check dmesg | grep xfrm. Reverse path filtering or an incorrect mark ID is usually the culprit.

