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 |
| Packaged on Debian and Ubuntu, available on RHEL through EPEL |
Libreswan |
| 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 |
|---|---|
|
|
|
|
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-releasesudo dnf install strongswanThe 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:
proposalsandesp_proposalsset 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 = transportkeeps 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.10and198.51.100.20is encrypted. start_action = trapinstalls 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_delayanddpd_actiondetect 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 |
|---|---|---|
| The two sides offer no common algorithm set | Make |
| PSK mismatch, or the | Compare the secret byte for byte and check each |
IKE establishes but no traffic passes | ESP (protocol 50) blocked by a firewall or security group | Allow ESP, or add |
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 |
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.