Cycle's hosted MCP is live. Your AI tooling just learned DevOps.Read the announcement

Configuring IPSec.

IPsec encrypts traffic at the IP layer, before a packet leaves the machine. Applications don't need to know it's there. A database replica, a message queue and an internal API all get the same protection, because the kernel encrypts the packets instead of each app bringing its own TLS.

That makes IPsec the standard answer to one specific problem: servers that talk to each other over the public internet. The usual cases are VMs in two different clouds, a bare metal server in a colo talking to a node in AWS, or a cluster that spans regions with no private link between them. Without encryption at the network layer, each of those connections is either plaintext on the wire or relying on every application to get TLS right.

IPsec by default on Cycle

Hand-built IPsec works for a pair of servers. It can get more expensive as the fleet grows, because every pair of hosts needs its own tunnel, credentials and firewall rules.

If that's not something you want to manage yourself, Cycle handles it in the control plane. When a server joins a cluster, Cycle maps the network paths to its neighbors. Nodes with a private link use it. Nodes without one get an IPsec-encrypted link over the public internet, with keys generated and rotated by Cycle. When a private link appears later, the route upgrades automatically.

The result is one flat, encrypted network across AWS, GCP, bare metal and anything else in the cluster, with no tunnel configs to write.

If you run your own servers, or want to understand what the control plane is doing for you, the rest of this page walks through configuring IPsec by hand.

How IPsec works

IPsec is a suite of protocols. Three of them do the work:

Component

What it does

On the wire

IKEv2 (Internet Key Exchange)

Authenticates the peers, agrees on algorithms and derives keys

UDP 500, or UDP 4500 when NAT is in the path

Security Associations (SAs)

The agreements IKE produces. The IKE SA protects the negotiation channel. CHILD SAs carry the actual traffic. Both expire and rekey on a timer.

Kernel state, not packets

ESP (Encapsulating Security Payload)

Encrypts and authenticates each packet using the CHILD SA's keys

IP protocol 50, or wrapped in UDP 4500 behind NAT

IKE runs in userspace (strongSwan's charon daemon on Linux). ESP runs in the kernel. Once IKE installs the SAs, the daemon is out of the data path.

Transport mode vs. tunnel mode

IPsec has two modes, the choice of which depends on who the traffic belongs to.

Transport mode encrypts the packet's payload and keeps the original IP header. Use it when the two IPsec endpoints are also the two hosts talking, which is the server-to-server case. It has the lowest overhead.

Tunnel mode wraps the entire original packet inside a new IP header. Use it when a gateway encrypts traffic on behalf of a network behind it, which is the classic site-to-site VPN.

For servers talking directly over the internet, start with transport mode. This page configures that first, then shows the change for site-to-site.

Choosing an IPsec implementation

On Linux, the kernel does the encryption through its XFRM framework. What you choose is the IKE daemon that negotiates keys and installs the SAs. Two are actively maintained:

Implementation

Config

Where it fits

strongSwan

swanctl.conf

Packaged on Debian and Ubuntu, available on RHEL through EPEL

Libreswan

ipsec.conf and /etc/ipsec.d/*.conf

The IPsec implementation Red Hat supports on RHEL

Both speak IKEv2 and interoperate with each other and with cloud VPN gateways. This page uses strongSwan. On RHEL, where you want a vendor-supported stack, use Libreswan. The concepts, ports and firewall rules are the same.

Two things to avoid when reading older guides. strongSwan's legacy interface, ipsec.conf driven by the ipsec command, has been deprecated for years and is no longer built by default as of strongSwan 6.0. That applies to strongSwan only, since ipsec.conf is Libreswan's current format. Openswan and ipsec-tools (racoon) are no longer maintained, so skip guides built on them.

Before you start

The examples use two servers with public IPs:

Host

Public IP

server-a

203.0.113.10

server-b

198.51.100.20

You need root on both, and strongSwan 5.9 or newer configured through swanctl.

Step 1: Install strongSwan

On Debian and Ubuntu:

sudo apt install charon-systemd strongswan-swanctl

On RHEL, Rocky and AlmaLinux, strongSwan comes from EPEL:

sudo dnf install epel-release
sudo dnf install strongswan

The config directory differs by distribution. Debian and Ubuntu use /etc/swanctl/. EPEL packages use /etc/strongswan/swanctl/. Adjust the paths below to match.

Step 2: Open the firewall

Each server must accept IKE and ESP from its peer. On server-a, with nftables and an existing inet filter table:

sudo nft add rule inet filter input ip saddr 198.51.100.20 udp dport { 500, 4500 } accept

sudo nft add rule inet filter input ip saddr 198.51.100.20 meta l4proto esp accept

Mirror the rules on server-b with 203.0.113.10 as the source. If the servers sit behind cloud security groups, allow the same traffic there too. Security groups often permit UDP but silently drop ESP, which is the most common reason a tunnel negotiates but passes no traffic.

Step 3: Generate a pre-shared key

Two servers can authenticate each other with a pre-shared key (PSK). Generate a long random one:

openssl rand -base64 48

Both servers use the same key. Copy it over a channel you already trust, such as SSH. PSKs are fine for a pair of hosts. Past that, use certificates.

Step 4: Configure server-a

Create /etc/swanctl/conf.d/server-b.conf on server-a:

connections {
server-b {
version = 2
local_addrs = 203.0.113.10
remote_addrs = 198.51.100.20
proposals = aes256gcm16-prfsha384-ecp384
dpd_delay = 30s
local {
auth = psk
id = 203.0.113.10
}
remote {
auth = psk
id = 198.51.100.20
}
children {
server-b {
mode = transport
esp_proposals = aes256gcm16-ecp384
start_action = trap
dpd_action = trap
}
}
}
}
secrets {
ike-server-b {
id = 198.51.100.20
secret = "PASTE_THE_KEY_FROM_STEP_3"
}
}

What the key lines do:

  • proposals and esp_proposals set the algorithms for IKE and ESP. AES-256-GCM with an ECP-384 key exchange is a solid default. Both sides must offer a matching proposal.
  • mode = transport keeps the original IP headers, since the servers are the endpoints.
  • No traffic selectors are set, so strongSwan defaults to the two hosts' own addresses. Every packet between 203.0.113.10 and 198.51.100.20 is encrypted.
  • start_action = trap installs the kernel policy at load time and negotiates on the first matching packet. Traffic between the hosts never goes out in plaintext, even before the tunnel is up.
  • dpd_delay and dpd_action detect a dead peer and re-arm the trap so the tunnel comes back when the peer does.

The secret file permissions matter. Restrict the file to root:

sudo chmod 600 /etc/swanctl/conf.d/server-b.conf

Step 5: Configure server-b

Create /etc/swanctl/conf.d/server-a.conf on server-b. It's the same file with the addresses swapped:

connections {
server-a {
version = 2
local_addrs = 198.51.100.20
remote_addrs = 203.0.113.10
proposals = aes256gcm16-prfsha384-ecp384
dpd_delay = 30s
local {
auth = psk
id = 198.51.100.20
}
remote {
auth = psk
id = 203.0.113.10
}
children {
server-a {
mode = transport
esp_proposals = aes256gcm16-ecp384
start_action = trap
dpd_action = trap
}
}
}
}
secrets {
ike-server-a {
id = 203.0.113.10
secret = "PASTE_THE_KEY_FROM_STEP_3"
}
}

Step 6: Load the config and bring up the tunnel

On both servers, start the daemon and load the config:

sudo systemctl enable --now strongswan

sudo swanctl --load-all

Because of start_action = trap, the tunnel comes up on the first packet. Send one from server-a:

ping -c 3 198.51.100.20

The first ping can be lost while IKE negotiates. To bring the tunnel up explicitly instead, run sudo swanctl --initiate --child server-b.

Step 7: Verify the traffic is encrypted

List the active SAs:

sudo swanctl --list-sas

Look for an IKE SA in state ESTABLISHED and a CHILD SA in state INSTALLED with mode TRANSPORT. The kernel's view is available with ip xfrm state and ip xfrm policy.

Then confirm what's on the wire. On server-a, capture traffic to the peer while the ping runs:

sudo tcpdump -ni any host 198.51.100.20

Every packet should show as ESP(spi=...,seq=...), or as UDP on port 4500 if NAT is in the path. If you see ICMP echo requests in the clear, the policy isn't applied.

Site-to-site: switching to tunnel mode

When each server is a gateway for a private network behind it, switch the CHILD SA to tunnel mode and name the subnets. On server-a, with 10.10.0.0/16 behind it and 10.20.0.0/16 behind server-b:

children {
server-b {
mode = tunnel
local_ts = 10.10.0.0/16
remote_ts = 10.20.0.0/16
esp_proposals = aes256gcm16-ecp384
start_action = trap
dpd_action = trap
}
}

server-b gets the mirror image, with local_ts and remote_ts swapped. Both gateways need IP forwarding on (sysctl -w net.ipv4.ip_forward=1), and hosts on each subnet need a route to the remote subnet through their local gateway.

Troubleshooting

Start with the logs. journalctl -u strongswan -f shows the daemon's view, and swanctl --log streams IKE events live.

Symptom

Likely cause

Fix

NO_PROPOSAL_CHOSEN

The two sides offer no common algorithm set

Make proposals and esp_proposals identical on both servers

AUTHENTICATION_FAILED

PSK mismatch, or the id values don't match what the peer presents

Compare the secret byte for byte and check each id against the peer's local.id

IKE establishes but no traffic passes

ESP (protocol 50) blocked by a firewall or security group

Allow ESP, or add encap = yes to the connection to force UDP 4500

Small requests work, large transfers stall

ESP overhead pushes packets past the path MTU

Lower the interface MTU or clamp TCP MSS on the affected path

Tunnel never comes up from one side

Policy loaded on one server only

Run swanctl --load-all on both and confirm with swanctl --list-conns

Scaling past two servers

Everything above is per pair. A full mesh of n servers needs n(n-1)/2 tunnels: 45 for ten servers, 1,225 for fifty. Every new server means a config change on every existing one, plus new firewall rules on both ends of each link.

Credentials scale the same way. One PSK per pair gets unmanageable fast, so larger deployments move to certificates. You run a private CA (strongSwan's pki tool can issue them), give each host its own certificate and switch to auth = pubkey. That removes per-pair secrets, but adds certificate issuance, renewal and revocation to the job.

strongSwan rekeys the session keys on its own, every hour for CHILD SAs and every four hours for the IKE SA by default. Everything else is yours: the tunnel inventory, the long-term credentials, the firewall rules, the MTU tuning and the route changes when a private link becomes available between two hosts. That operational surface is what Cycle's control plane takes off your plate.

Cookies

Cookies Preferences

We run basic, anonymous analytics by default to measure site traffic. By clicking "Accept," you allow additional cookies for advanced app improvements and tailored advertising. Choose what you share by clicking "Customize."