· ~4 min read
A site-to-site IPsec VPN with strongSwan and my own PKI
Joining two LANs across an untrusted network with an IKEv2 tunnel, authenticating the gateways with certificates from a private CA instead of a shared key, and proving in Wireshark that only ESP crosses the wire.
Goal
Connect two isolated LANs across a network I do not trust, so that hosts on each side talk to each other as if they were local, and anyone watching the link in between sees nothing useful. The lab runs on four Linux VMs: two VPN gateways and one internal host behind each.
The configs and full steps are in the repository on GitHub.
Topology
| Machine | External | Internal |
|---|---|---|
| Gateway 1 | 10.0.3.3 | 192.168.1.1/24 |
| Gateway 2 | 10.0.3.4 | 192.168.2.1/24 |
| Host 1 | 192.168.1.10 (gateway 192.168.1.1) | |
| Host 2 | 192.168.2.10 (gateway 192.168.2.1) |
Before any tunnel, the gateways need plain routing: IP forwarding on, and a static route on each gateway to the other side's LAN via the other gateway's external address. Getting layer 3 right first means that, later, any failure is an IPsec problem and not a routing one.
Certificates instead of a pre-shared key
The quick way to authenticate two IPsec peers is a pre-shared key. It is also the weak way: one string shared between sites, often short, rarely rotated, and if it leaks every tunnel that uses it is exposed. I set up a small private CA instead:
- a CA key (RSA 4096) and a self-signed CA certificate, the trust anchor;
- a key pair and a certificate signing request per gateway, signed by the CA;
- keys stored under
/etc/swanctl/with permissions 600.
Each gateway then proves its identity with its own private key, and checks the other side's certificate against the CA, down to the exact distinguished name.
The strongSwan configuration
The tunnel is defined in swanctl.conf. Gateway 1's side, shortened:
connections {
gw1-to-gw2 {
remote_addrs = 10.0.3.4
local {
auth = pubkey
certs = g1Cert.pem
}
remote {
auth = pubkey
id = "C=GR, O=Lab, CN=gateway2"
}
children {
net {
local_ts = 192.168.1.0/24
remote_ts = 192.168.2.0/24
start_action = trap
}
}
}
}
Two settings matter most. auth = pubkey together with the exact id means the peer must hold a CA-signed certificate for gateway2, not just any certificate from the CA. And start_action = trap installs a kernel policy that brings the tunnel up on demand: the first packet that matches the two subnets triggers the IKEv2 negotiation.
Proving it works
After swanctl --load-all, a ping from one LAN to the other raised the tunnel, and swanctl --list-sas showed the security associations. Then the part I care about most, a capture on the gateway's external interface:
- no ICMP in clear text on the external segment;
- only ESP packets, with opaque payloads;
- the SPI values in Wireshark matched the ones reported by
swanctl --list-sas, so the encrypted packets on the wire belonged to this tunnel and nothing else.
What I learned
Looking at it again, these are the things I would tighten.
- Pin the algorithms. Unencrypted-looking bytes show that encryption is on, not which cipher. I would set
proposalsandesp_proposalsexplicitly (for example AES-GCM with a modern DH group) rather than rely on version-dependent defaults, and add a DH group to the ESP proposal so rekeyed child SAs get perfect forward secrecy. - The first packets of a trap policy are lost. While IKE negotiates, the packets that triggered it are dropped, so the first ping or two fail. That is expected, but it confuses monitoring checks; for links that must always be up,
start_action = startis the better choice. - A CA needs a life beyond issuance. The lab CA had no revocation (CRL or OCSP) and a ten-year lifetime, and its key lived on a lab machine. In production the CA key stays offline, gateway certificates are short-lived, and there is a way to revoke one when a gateway is lost.
- The firewall in front matters too. Between the gateways only IKE (UDP 500 and 4500) and ESP (IP protocol 50) need to pass. Anything else on the external interface is attack surface.
A personal lab on virtual machines.
Want the configs or code from this lab, or have a question? Email me at papas@viotnet.com