Skip to content

Migrating from IPsecGateway to VPNGateway/VPNPeer

Pre-release, v0.3.0 — not yet e2e-validated

This runbook follows directly from the reconciler's documented FIP/ serviceCIDR pinning contracts (the same mechanism the VM-to-Operator Migration runbook already relies on, live-validated in v0.1.0) but has not itself been exercised against a live IPsecGateway → VPNGateway/VPNPeer cutover. Treat this as the plan, not a track record. See the index page and Known Limitations.

v0.3.0 ships the vpn.bnerd.net/v1alpha1 group (VPNGateway/VPNPeer, see The unified model) alongside, not instead of, the frozen ipsec.bnerd.net/v1alpha1 group (IPsecGateway). Both are served by the same operator/manager binary at once (cmd/main.go registers both reconcilers) — an existing IPsecGateway keeps reconciling exactly as it does today; nothing about it changes just because v0.3.0 is installed. There is no conversion webhook and no in-place upgrade path between the two kinds: moving a client from IPsecGateway to VPNGateway/VPNPeer is a manual, per-CR recreate, following the same pinned-FIP discipline the VM-to-Operator Migration runbook already established.

Why per-CR, not a bulk cutover

Every IPsecGateway has its own floating IP, its own service CIDR, and its own tunnel/PSK configuration — none of that is shared infrastructure that could be cut over once for every client. Migrate one client's CR pair at a time, at a maintenance window that suits that client, using the same pinned-value contract the VM migration runbook uses: the values that must stay byte-for-byte identical across the recreate are exactly the ones the client's own firewall/parameter sheet references.

What must stay pinned

Old (IPsecGateway) New (VPNGateway/VPNPeer) Why it must match
spec.endpoint.floatingIP (the associated FIP) VPNGateway.spec.endpoint.floatingIP The client's firewall dials this IP — an unpinned VPNGateway allocates a new FIP from the pool, which is a client-side config change you don't want mid-migration.
status.serviceCIDR VPNGateway.spec.serviceCIDR Any exposures[]/reverseExposures[] virtual IPs the client's side already has hardcoded (firewall rules, DNS) only keep working if the same /24 is re-pinned; an unpinned VPNGateway allocates the next free /24, not necessarily the same one.
spec.tunnels[].peer.address / .id VPNPeer.spec.ipsec.peer.address / .id The IKE peer identity/address the client's own gateway expects.
spec.tunnels[].remoteSubnets[] VPNPeer.spec.ipsec.remoteSubnets[] Traffic selectors — a mismatch here fails IKE Phase 2 negotiation, not just a cosmetic drift.
spec.auth.pskSecretRef (content, not necessarily the Secret name) VPNPeer.spec.ipsec.pskSecretRef Same pre-shared key value — the new model moves the PSK reference from gateway-wide (IPsecGateway.spec.auth) to per-peer (VPNPeer.spec.ipsec.pskSecretRef), but the value the client authenticates with must be unchanged.
spec.ike/spec.tunnels[].esp proposals VPNGateway.spec.ipsec.ike / VPNPeer.spec.ipsec.esp Must still be in the approved allowlist (unchanged between groups) and should match what the client peer is configured to propose, to avoid a re-negotiation surprise.

Everything else (exposure names, target addresses/ports, DPD timers) can be copied straight across — the ExposureSpec/TargetSpec shapes are identical between the two groups.

Runbook

1. Draft the new CRs alongside the old one

Write the equivalent VPNGateway (protocol: ipsec) and one VPNPeer per spec.tunnels[] entry the old IPsecGateway had, with the pinned values from the table above. Do not apply them yet — the old IPsecGateway still owns the floating IP, so applying now would just fail at FIPAttached (already associated with a different port), the same safe pre-stage state the VM-migration runbook's step 1 describes.

Since this is a single-tunnel-per-client migration in the common case, the resulting VPNPeer will have exactly one peer per gateway and no authorization block needed — it lands in the default-allow shape automatically, reaching every exposure the old IPsecGateway exposed, with no grant list to write. Only add an authorization block if you're intentionally adding a second peer to the same gateway as part of this migration (which flips the gateway to default-deny — see the unified model).

2. At the maintenance window: delete the old CR, apply the new ones

kubectl delete ipsecgateway <name>
kubectl apply -f vpngateway.yaml -f vpnpeer.yaml

Deleting the IPsecGateway runs its full finalizer unwind (owned Kubernetes children first, then OpenStack cleanup) — critically, this disassociates but does not release a pinned FIP (Cleanup never releases a pinned FIP back to the pool, same contract the VM-migration runbook relies on), so the same address is immediately available for the new VPNGateway's own EnsureFIP to pin.

The client's tunnel is down for the span between the delete and the new gateway pod reaching ESTABLISHED — budget for a brief outage, same as any other maintenance-window cutover. Based on the pinned-FIP mechanism's v0.1.0 live validation (association + strongSwan initiation), expect the tunnel to re-establish within roughly a minute of the new VPNGateway reaching FIPAttached=True, assuming the client's peer retries on its own DPD/retry window without any action on their end.

3. Verify

kubectl get vpngateway <name> -o jsonpath='{.status.conditions}' | jq .
kubectl get vpnpeer <name> -o jsonpath='{.status.conditions}' | jq .
kubectl exec -n <tenant-ns> deploy/<name> -c strongswan -- swanctl --list-sas

Confirm the peer's Authorized condition (see the unified model) as well as the tunnel state — a peer can be ESTABLISHED at the IKE layer while still dark on authorization if the authorization block was mistyped, which is easy to miss if you only check swanctl --list-sas.

Rollback

Re-apply the old IPsecGateway CR:

kubectl delete vpngateway <name> vpnpeer <name>
kubectl apply -f old-ipsecgateway.yaml

The same pinned-FIP contract applies in reverse — deleting the VPNGateway disassociates (never releases) the FIP, and the old IPsecGateway's EnsureFIP picks it back up. The client's firewall never needed to change in either direction, exactly as in the VM-to-Operator Migration runbook.

What this migration does not do

  • It does not retire the IPsecGateway CRD. Both groups are served in parallel indefinitely as of v0.3.0; dropping the served ipsec.bnerd.net kind is a separate, Bernd-gated decision, earliest v0.5.0, and is out of scope for this migration entirely.
  • It is not the same project as the old bnerd-vpn-gateway repo's WireGuard road-warrior migration. That standalone repo is archived only after v0.4.0 ships full road-warrior parity in this operator — also out of scope here.
  • It does not batch-convert every IPsecGateway at once. Each client is its own maintenance window, its own pinned values, and its own verification pass.