· ~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

MachineExternalInternal
Gateway 110.0.3.3192.168.1.1/24
Gateway 210.0.3.4192.168.2.1/24
Host 1192.168.1.10 (gateway 192.168.1.1)
Host 2192.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:

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:

What I learned

Looking at it again, these are the things I would tighten.

A personal lab on virtual machines.

Want the configs or code from this lab, or have a question? Email me at papas@viotnet.com

← all posts