Skip to content

IPsec Gateway Operator

Project/module identity: bnerd-vpn-operator (formerly bnerd-ipsec-operator).

v0.3.0 — unified vpn.bnerd.net model

v0.3.0 adds a new, protocol-neutral API group — vpn.bnerd.net/v1alpha1 (VPNGateway + VPNPeer) — covering both IPsec and WireGuard gateways under one model, with per-peer authorization (see The unified model and WireGuard gateways). It is served alongside, not instead of, the frozen ipsec.bnerd.net/v1alpha1 group documented throughout the rest of this site — see Migrating from IPsecGateway if you're moving an existing client over. Every reconciler code path is covered by the unit/envtest suites. See Known Limitations for the complete, current list before onboarding anything that depends on either group's still-open items.

v0.2.0 (ipsec.bnerd.net group)

Every reconciler code path is covered by unit/envtest suites against a fake OpenStack client and a fake VICI session. v0.2.0 adds the ClusterLink CRD — inter-cluster fabric plumbing as a reconciled resource. It ships with a few known, deliberate constraints for this release — single gateway pod per tunnel with no HA/spreading, IKEv2 with pre-shared-key authentication only, and one documented egress-policy deviation. See the Known Limitations page for the full, current list before onboarding a tunnel that depends on any of those.

A Kubernetes operator that runs multi-tenant IPsec site-to-site gateways as one pod per client, declaratively, via a single IPsecGateway CRD — replacing the current fleet of hand-provisioned VMs (one Ansible-managed VM per client tunnel) with reconciled, self-describing Kubernetes objects.

What it does

  • One pod per client. Each IPsecGateway gets its own Deployment (strongswan + init-nat + status-agent + ipsec-exporter containers), its own Neutron port + security group, and its own floating IP — the same isolation model as the VM fleet, running on Kubernetes instead.
  • SNAT by default; a routed mode is planned but not yet implemented. A tunnel's traffic is source-NATed through the gateway's own virtual service network by default (mode: snat, the only mode v0.1.0 supports — mode: routed is accepted by the CRD's enum but rejected by Validate() as not-implemented, see Networking).
  • Virtual service networks carved from 100.64.0.0/10. Every gateway is allocated a /24 out of the operator's configured CGNAT pool (100.64.0.0/16 by default) — its own private addressing plane for exposures, independent of any tenant's real network.
  • A dedicated floating IP per client, either allocated fresh from a pool or pinned to an existing one (the mechanism the migration runbook uses to move a client off its VM without a client-side firewall change).

Who this is for

Platform/network engineers provisioning a client's IPsec tunnel

You're filling out a client's IPsec parameter sheet (Fortigate, pfSense, whatever they run) and need to translate it into an IPsecGateway CR. Start with the Quick Start, then keep the Parameter Sheet Mapping page open while you work.

Engineers migrating a client off a hand-provisioned VM

You're cutting a client over from an existing, hand-provisioned IPsec VM to this operator. Go straight to the VM-to-Operator Migration runbook.

On-call, triaging a tunnel-down alert

Go straight to Operations.

Architecture at a glance

flowchart TB
    CR["IPsecGateway CR\n(one per client)"] --> Ctrl["bnerd-vpn-operator\nreconcile loop"]

    Ctrl -->|"EnsureSecurityGroup\nEnsurePort\nEnsureFIP"| Neutron[(Neutron\nOpenStack)]
    Ctrl -->|renders + owns| NAD["NetworkAttachmentDefinition\n(Multus, 2nd NIC)"]
    Ctrl -->|renders + owns| CM1["ConfigMap\nswanctl.conf"]
    Ctrl -->|renders + owns| CM2["ConfigMap\nnat.rules"]
    Ctrl -->|renders + owns| SA["ServiceAccount +\nRoleBinding (per tenant ns)"]
    Ctrl -->|renders + owns| NP["NetworkPolicy\n(NIC1 ingress only)"]
    Ctrl -->|renders + owns| Deploy["Deployment\n(gateway pod)"]

    Deploy --> Pod["Gateway Pod"]
    Pod --> C1["init-nat\napplies nat.rules"]
    Pod --> C2["strongswan\nIKE/IPsec daemon"]
    Pod --> C3["status-agent\nvici load-shared +\nconfig reload +\nstatus.tunnels patch"]
    Pod --> C4["ipsec-exporter\nprometheus /metrics"]

    C3 -.->|"get/patch\nstatus subresource"| CR
    NAD -.->|"NIC2, Neutron port"| Neutron
    Pod -->|"IKE/ESP over NAT-T\n(FIP = local_id)"| Peer(["Client peer gateway"])

    style Ctrl fill:#6366f1,stroke:#4f46e5,color:#fff
    style Pod fill:#16a34a,stroke:#15803d,color:#fff

The operator itself never touches the tunnel dataplane — it only ever provisions OpenStack resources, renders Kubernetes children, and reads status back over a shared VICI socket via status-agent. The gateway pod's strongswan container does the actual IKE/ESP work.

CRD at a glance

CRD Scope Group/Version Purpose
IPsecGateway Namespaced ipsec.bnerd.net/v1alpha1 One CR = one client's IPsec site-to-site gateway pod. Feature-frozen, bugfix-only as of v0.3.0.
ClusterLink Cluster-scoped ipsec.bnerd.net/v1alpha1 One CR = one service cluster plumbed into the transit fabric.
VPNGateway Namespaced vpn.bnerd.net/v1alpha1 One CR = one client's protocol-neutral (ipsec or wireguard) gateway pod. New in v0.3.0, pre-release.
VPNPeer Namespaced vpn.bnerd.net/v1alpha1 One CR = one remote party of a VPNGateway, with per-peer authorization. New in v0.3.0, pre-release.