Skip to content

Quick Start

v0.1.0 — validated end-to-end

The install steps below are accurate against the chart and reconciler as of v0.1.0, and have been proven end-to-end on a live reference deployment: the attachment mechanism, the reconciler driving it, and the IPsec tunnel itself all ran against a real OpenStack project and a real strongSwan peer. See Known Limitations for this release's remaining constraints.

Goal: install the operator, then walk through a first IPsecGateway CR to ESTABLISHED.

Prerequisites

Requirement Why
Kubernetes 1.25+ with the Multus CNI (k8s.cni.cncf.io/v1 NetworkAttachmentDefinition CRD) installed The gateway pod's second NIC (tunnel-facing) is a Multus attachment; the operator applies (but never parses) a NetworkAttachmentDefinition per gateway.
An OpenStack project with a Neutron network, router, and floating-IP pool the operator is allowed to provision into Every gateway gets its own Neutron port, security group, and floating IP.
That Neutron network (networkID) is a dedicated transit network (e.g. vpn-transit) — never the cluster's own worker network Reusing the worker network makes the OpenStack CCM sync every gateway port's IPs and floating IP into node.status.addresses on the node the port happens to attach to, breaking kubectl exec/logs against the entire node, not just the gateway pod (confirmed live). See Connecting Service Clusters for how a dedicated transit network reaches other clusters.
An application-credential (not username/password) for that project internal/openstack.NewRealClientFromEnv only supports the OS_APPLICATION_CREDENTIAL_ID/OS_APPLICATION_CREDENTIAL_SECRET auth path.
Security groups on every spec.exposures[] target already allow ingress from the gateway network's subnet The operator does not manage target-side SG rules — traffic arrives at the target from the gateway's own Neutron port, which isn't automatically a member of whatever SG the target normally trusts. See Networking.

Step 1 — Create the OpenStack credentials Secret

kubectl create secret generic ipsec-operator-openstack \
  --namespace bnerd-ipsec-system \
  --from-literal=OS_AUTH_URL=https://keystone.example.com:5000/v3 \
  --from-literal=OS_APPLICATION_CREDENTIAL_ID=<id> \
  --from-literal=OS_APPLICATION_CREDENTIAL_SECRET=<secret> \
  --from-literal=OS_REGION_NAME=<region>

The chart's Deployment template envFrom-wires exactly those keys (see charts/bnerd-ipsec-operator/templates/deployment.yaml); OS_REGION_NAME is optional, the other three are required. The chart never generates or reads this Secret's content — you own it.

Step 2 — Install the chart with the values it requires

Three values are hard-required — helm install/template fails its render without them (see the chart's required guards, templates/deployment.yaml):

Value Required Meaning
openstack.credentialSecretRef yes Name of the Secret from Step 1, in the release namespace.
networkID yes Neutron network ID the gateway ports are created on.
routerID yes Neutron router ID routed-mode/reverse-exposure tunnels get a route added to — required even if no gateway on this operator uses routed mode today (internal/config.Config.Validate() treats it as unconditionally required).

Two more values have a usable-but-not-production default, so they aren't guarded by required but are still worth setting explicitly:

Value Default Meaning
nadTemplate The host-device-by-hwaddr config, verified end-to-end against a real OpenStack/Neutron network Opaque Multus CNI config JSON template with {{portID}}/{{fixedIP}}/{{mac}} substitutions — see Networking. It requires Multus and containernetworking/plugins (host-device + static IPAM) installed on the target shoot, and Cilium's cni-exclusive set to false — see that same section for both. Override only if your own SDN needs a different mechanism.
fipPool "" Default floating IP source for gateways that don't set spec.endpoint.floatingIPPool themselves — must be the Neutron external network's UUID, not a human-readable pool name (passed straight through as gophercloud's FloatingNetworkID, see CRD Reference). Leave empty only if every IPsecGateway on this operator sets its own pool.
internalSourceCIDRs [] Fabric-side source CIDRs admitted, at the security-group level, to reach spec.reverseExposures[] virtual IPs — required for reverse-exposure traffic to actually reach the port, not just get routed to it (e2e finding 6; see Networking). Leave empty only if no gateway on this operator uses reverseExposures.
helm install ipsec-operator ./charts/bnerd-ipsec-operator \
  --namespace bnerd-ipsec-system --create-namespace \
  --set openstack.credentialSecretRef=ipsec-operator-openstack \
  --set networkID=<neutron-network-uuid> \
  --set routerID=<neutron-router-uuid> \
  --set fipPool=<neutron-external-network-uuid> \
  --set-file nadTemplate=my-nad-template.json

monitoring.enabled (ServiceMonitor/PodMonitor/PrometheusRule) is false by default — the monitoring.coreos.com/v1 CRDs it renders may not exist in every target cluster. Set --set monitoring.enabled=true on a cluster that has the Prometheus Operator installed; see Operations.

Verify the operator is running:

kubectl get pods -n bnerd-ipsec-system
kubectl logs -n bnerd-ipsec-system deploy/ipsec-operator-bnerd-ipsec-operator-controller-manager

internal/config.Config.Validate() fails startup fatally (not just a failed reconcile) if networkID, routerID, nadTemplate, the gateway image, or the status-agent image are unset — a crash-looping operator pod almost always means one of those.

Step 3 — Create the PSK Secret

kubectl create secret generic acme-psk \
  --namespace default \
  --from-literal=psk='<the pre-shared key>'

The PSK is never rendered into any ConfigMap — status-agent reads this Secret directly and pushes it into charon over VICI's load-shared command (see Operations).

Step 4 — Apply your first IPsecGateway

This walkthrough uses a representative configuration (IKEv2, aes256-sha512-ecp384, one forward exposure and one reverse exposure) — substitute your own peer/proposal/subnet values. peer.address below uses an RFC 5737 documentation address as a stand-in for a real client's peer IP.

apiVersion: ipsec.bnerd.net/v1alpha1
kind: IPsecGateway
metadata:
  name: acme
  namespace: default
spec:
  endpoint:
    # The Neutron external network's UUID, not a human-readable pool
    # name — see the CRD Reference. Or set floatingIP instead, to pin
    # an existing address.
    floatingIPPool: 3c1a5c8e-9e3f-4b1a-8f2e-1a2b3c4d5e6f
  ike:
    version: 2
    proposals:
      - aes256-sha512-ecp384
    lifetime: 86400s
    dpd:
      delay: 30s
      timeout: 150s
  auth:
    pskSecretRef:
      name: acme-psk
      key: psk
  tunnels:
    - name: acme-hq
      peer:
        address: 203.0.113.10
        id: 203.0.113.10
      remoteSubnets:
        - 10.20.0.0/16
      esp:
        proposals:
          - aes256-sha512-ecp384
        lifetime: 28800s
      mode: snat
  exposures:
    - name: keycloak
      target:
        address: 10.30.0.10
        ports: [443]
  reverseExposures:
    - name: ldaps
      target:
        address: 10.20.2.20
        ports: [636]

spec.serviceCIDR is left unset here — the operator allocates the next free /24 out of serviceCIDRPool automatically and records it in status.serviceCIDR.

kubectl apply -f acme-gateway.yaml

Step 5 — Watch it come up

kubectl get ipsecgateway acme -o yaml | yq '.status'

Expect, in order: ValidatedAllocatedPortReadyPortAttachedFIPAttachedRoutingReadyConfigRenderedDeploymentReady, all True (see the CRD Reference for the full condition/reason vocabulary). DeploymentReady may sit briefly False/DeploymentUnavailable even after every earlier condition is True — it only flips True once the gateway pod itself reports Available (kubectl get deployment acme showing 1/1 under AVAILABLE), not merely once the Deployment object was applied. Once the pod is running and the peer answers, status.tunnels[0].state moves from DOWN to ESTABLISHEDstatus-agent polls this from strongSwan's VICI socket and patches it in, independently of the conditions above.

kubectl get pods -n default -l ipsec.bnerd.net/gateway=acme
kubectl exec -n default deploy/acme -c strongswan -- swanctl --list-sas

If the tunnel doesn't come up, start with Tunnel-down triage.

Step 6 — Reach a service cluster, not just the gateway cluster (optional)

An exposure's target above lives on the gateway cluster's own network by default. To reach a target on a different Kubernetes cluster through the tunnel, that service cluster needs a one-time fabric link first — apply a ClusterLink CR (cluster-scoped, one per service cluster) before adding the exposure. See Connecting Service Clusters for the CR and what it automates.

Upgrading

Apply CRDs before helm upgrade, every time

Helm does not upgrade CRDs on helm upgrade — it only installs them once, on the first helm install (this is Helm's own documented behavior, not something this chart works around). A stale CRD is silently pruned of any newer status/spec field the new controller version expects, which has produced a real failure mode: a mis-scheduled gateway pod after a field the reconciler relied on was pruned. Always run:

kubectl apply -f charts/bnerd-ipsec-operator/crds/
helm upgrade ipsec-operator ./charts/bnerd-ipsec-operator \
  --namespace bnerd-ipsec-system \
  --reuse-values

in that order — CRDs first, helm upgrade second — on every upgrade, not just ones you know changed the CRD.