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.
Step 5 — Watch it come up¶
Expect, in order: Validated → Allocated → PortReady → PortAttached →
FIPAttached → RoutingReady → ConfigRendered → DeploymentReady, 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 ESTABLISHED — status-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.