An IPsec VPN with LibreSwan
A site-to-site tunnel between two gateways, authenticated with certificates.
Contents
A VPN feels like magic until you build one. I set up a site-to-site IPsec tunnel between two gateways running LibreSwan, so that two hosts with no route to each other ended up able to talk, with every packet between them encrypted on the wire.
It’s the same kind of tunnel that connects office networks to each other and to the cloud, shrunk down to four VMs. This post covers the concepts first, since IPsec comes with a lot of vocabulary, then the build: certificates, the connection config, bringing the tunnel up, checking the encryption in a capture and restricting what the tunnel will carry.
Background: the IPsec vocabulary#
IPsec isn’t one protocol. It’s a family of them and most of the confusion around it comes from the names.
Site-to-site versus remote access. A remote access VPN connects one device (your laptop) to a network. A site-to-site VPN connects two whole networks through their gateways. The hosts behind each gateway don’t run any VPN software at all. They just send packets to their gateway as usual and the gateways do the rest. This post builds a site-to-site VPN.
IKE, the negotiation. Before any data is protected, the two gateways have to authenticate each other and agree on keys and algorithms. That’s the job of IKE (Internet Key Exchange). This lab uses IKEv2, the current version. IKE runs over UDP port 500 (or 4500 when there’s NAT in the way).
Security Associations (SAs). The result of that negotiation is a set of agreements called SAs. Each SA says, in effect, “traffic matching this gets protected with these keys and this algorithm”. IKE sets up one SA to protect itself and then separate ones for the actual data.
ESP, the data. The data itself travels as ESP (Encapsulating Security Payload), which is IP protocol 50. An ESP packet carries the original packet, encrypted and authenticated.
Tunnel mode. In tunnel mode, which is what site-to-site VPNs use, the entire original packet (headers included) is encrypted and wrapped inside a new IP packet addressed from one gateway to the other. Anyone watching the link between the gateways sees only gateway-to-gateway ESP traffic. They can’t tell which hosts are talking, or on what ports.
Traffic selectors. Each SA covers a defined set of traffic, such as “subnet A to subnet B”, or narrower, “only TCP port 80”. Packets that don’t match aren’t sent through the tunnel.
The lab#
Four VMs in a line. The two at the ends are the hosts that want to talk. The two in the middle are the VPN gateways running LibreSwan.
To begin with, the end hosts sit on different subnets with no route between them, so they can’t even ping each other. The job of the tunnel is to bridge that gap without touching the rest of the network.
Step 1: certificates in the NSS database#
The two gateways could authenticate with a shared password (a “pre-shared key”), but certificates are the stronger and more realistic choice. Each gateway proves its identity with a certificate and each one can verify the other’s because both were signed by the same CA.
So I created a certificate authority and signed a certificate for each gateway. (The OpenSSL steps are the same ones I used for mutual TLS.)
LibreSwan doesn’t read certificates from loose files. It keeps them in an NSS database, the same certificate store Firefox uses. The workflow is to initialise the database, import each gateway’s certificate and key as a PKCS#12 bundle and import the CA:
# Create the NSS database LibreSwan reads from
sudo ipsec initnss
# Import this gateway's certificate and private key
sudo ipsec import gateway-left.p12
# Check what's there. The first column is the "nickname" the config refers to.
sudo certutil -L -d sql:/var/lib/ipsec/nss
With the certificate in NSS, the config only needs to name it by its nickname. LibreSwan pulls the key material out of the database itself.
Step 2: describe the connection#
Each gateway gets a connection definition in ipsec.conf (or a file under /etc/ipsec.d/). It describes both ends: each gateway’s own address, the subnet behind it and how it proves its identity. LibreSwan uses left and right for the two sides and works out which one it is by matching its own addresses, so the same file can go on both gateways.
conn site-to-site
ikev2=insist
authby=rsasig
left=<vm2-address>
leftsubnet=<subnet-behind-vm2>
leftcert=gateway-left
leftid=%fromcert
right=<vm3-address>
rightsubnet=<subnet-behind-vm3>
rightid=%fromcert
auto=start
The gateways also need to forward packets between their interfaces and each end host needs one route telling it to send traffic for the far subnet to its local gateway:
# On both gateways
echo 1 > /proc/sys/net/ipv4/ip_forward
# On each end host, route the far subnet via the local gateway
ip route add <far-subnet> via <local-gateway>
Step 3: bring the tunnel up#
sudo ipsec auto --add site-to-site
sudo ipsec auto --up site-to-site
sudo ipsec status
Bringing the connection up runs the IKEv2 negotiation. It takes two exchanges, four messages in total:
IKE_SA_INIT: the gateways agree on algorithms and do a Diffie-Hellman exchange, which gives them a shared secret without ever sending it. Everything after this is encrypted.IKE_AUTH: each gateway proves its identity by signing with the private key behind its certificate and they set up the first ESP SA for the actual traffic.
Once ipsec status reports the tunnel as established on both sides, the two end hosts can ping each other and fetch pages from the web server. No route between their subnets was ever added to the network in the middle. The traffic is travelling inside the tunnel.
Step 4: see the encryption#
Capturing the link between the two gateways is the payoff:
sudo tcpdump -ni <gateway-link-iface> esp or udp port 500 or udp port 4500
The pings from VM1 to VM4 don’t appear as ICMP at all. They show up as ESP packets from one gateway to the other, each with an SPI (the identifier of the SA it belongs to) and a sequence number. You can see that something is flowing and how much, but not what it is, who it’s really between, or which protocol is inside.
Step 5: restrict what the tunnel carries#
A tunnel that carries everything between two subnets is a blunt tool. The last step was to narrow the traffic selector so that only web traffic to the server is allowed into the tunnel and everything else is left out.
In LibreSwan that’s done with leftprotoport and rightprotoport, which pin each side of the SA to a protocol and port:
# Only TCP, and only to port 80 on the side with the web server
leftprotoport=tcp
rightprotoport=tcp/80
With that in place, web requests to the server still travel through the tunnel, but anything else, a ping for example, no longer matches the SA and isn’t carried. It’s the same least-privilege idea as a firewall rule, written into the VPN policy itself: the tunnel exists for exactly one service.
Summary#
| Piece | Role |
|---|---|
| IKEv2 (UDP 500/4500) | Authenticates the gateways and negotiates keys |
| Certificates in NSS | How each gateway proves its identity |
| ESP in tunnel mode (IP protocol 50) | Carries each original packet, fully encrypted, between the gateways |
leftsubnet / rightsubnet |
Which networks the tunnel connects |
leftprotoport / rightprotoport |
Narrows the tunnel to specific protocols and ports |
| Routes on the end hosts | Send far-subnet traffic to the local gateway, which does the rest |
I’ve connected to plenty of VPNs without thinking about what happens at the gateway. Building both ends, watching two hosts that genuinely couldn’t reach each other start exchanging pings and then seeing those pings turn into opaque ESP packets in a capture, is what turned IPsec from a checkbox into something I understand.