Route-Based IPsec VPN with strongSwan and VTI on Linux: Secure Cloud-to-On-Premises Connectivity

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

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 LAN 192.168.1.0/24, Tunnel VTI IP 10.254.0.1/30.
  • Cloud Gateway (AWS EC2 / GCP Compute Engine / Remote VM): Public IP 203.0.113.20, Cloud VPC 10.0.0.0/16, Tunnel VTI IP 10.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.

Share: