Skip to content

Connecting the gateway cluster to service clusters

Gateway pods live on a dedicated transit network (a vpn-transit-style network, not on any service cluster's own fabric). Reaching a new service cluster's Pod/Service network from a tunnel means giving that cluster's router a leg on the transit network and adding one static route on the gateway cluster's own router. This is one-time per service cluster, not per tunnel or per exposure: once a service cluster is plumbed in, every IPsecGateway on the gateway cluster can add exposures reaching into it with no further fabric changes.

Validated end-to-end 2026-08-19

The fabric this page describes — a transit-network leg plus a gateway- router route — has been validated live as part of v0.1.0's end-to-end release testing; see the worked example at the bottom of this page. As of v0.2.0, the ClusterLink CRD below is the reconciler-driven way to set it up.

ClusterLink (ipsec.bnerd.net/v1alpha1, cluster-scoped — it manages estate-level fabric, not tenant state) is the primary way to plumb in a service cluster: one CR does what the manual method below does by hand, and keeps doing it — it's a normal reconciler, so it self-heals if a leg port or route is ever removed out-of-band.

apiVersion: ipsec.bnerd.net/v1alpha1
kind: ClusterLink
metadata:
  name: svc01-link
spec:
  serviceClusterRouter: shoot--services--svc01
  serviceClusterCIDR: 10.251.0.0/16
kubectl apply -f svc01-link.yaml
kubectl get clusterlink svc01-link -o yaml | yq '.status'

Reconciling this:

  1. Allocates a transit IP for the leg port — starting at the transit subnet's .3 (.1 is conventionally the gateway cluster's own router leg, .2 the network's DHCP port; both are skipped by construction, and any address already claimed by another ClusterLink or occupied on the network is skipped too), unless spec.transitIP pins one explicitly.
  2. Creates (or adopts — see below) the leg port on the operator's transit network, tagged for this ClusterLink.
  3. Attaches that port to spec.serviceClusterRouter (a router name or ID — resolved at reconcile time).
  4. Adds the route on the gateway cluster's own router (the operator's configured routerID): spec.serviceClusterCIDR via the allocated transit IP.

No extra route is needed for the return path — once step 3 has the transit-network leg, the service cluster gets a connected route back to the transit network for free.

The target-side security group is still your responsibility, exactly as under the manual method: the service being exposed has to admit ingress from the transit network's CIDR on the exposed ports — see the Networking guide's "Exposure target reachability" section. ClusterLink only plumbs the fabric; it never touches a target's security group.

Adopting a pre-existing, hand-made leg

If a service cluster was already connected the manual way — a leg port that exists but was never tagged by this operator — applying a ClusterLink with the matching serviceClusterRouter and (if it isn't already sitting at .3) the matching spec.transitIP adopts that port instead of creating a duplicate or failing on an address conflict: an untagged port already attached to the target router, at the candidate transit IP, is re-tagged and taken over. Deleting the ClusterLink afterwards deletes that port — an adopted-then-tagged port is this ClusterLink's to delete, the same as one it created outright. See the CRD Reference for the exact adoption rule and every status field/condition.

Deleting the CR unwinds everything it owns, in reverse order: the gateway- router route, then the router attachment, then the leg port itself. Do this only once nothing still depends on reaching that service cluster — every IPsecGateway exposure targeting it will break as soon as the route is gone.

Verification recipe

Add (or reuse) an IPsecGateway exposure targeting a service in the newly connected cluster, then reach its virtual IP through a live tunnel from the client side. A successful check confirms a single gateway can reach exposures on multiple service clusters simultaneously once each is plumbed in, and that adding the exposure itself is a live reconcile with zero pod restarts, consistent with the operator's live-reload behavior for NAT/config changes (see Operations).

Ops note: Gardener admin kubeconfigs are time-boxed

If a service cluster's kubectl starts failing with remote error: tls: expired certificate while you're setting up a verification target (a test pod, a target-side security-group rule) in that cluster, the Gardener admin kubeconfig has simply expired — this is expected, not a fault in the cluster or the operator, and it has nothing to do with ClusterLink itself (reconciling a ClusterLink never touches the service cluster's Kubernetes API, only its OpenStack router). Refresh it via the Gardener virtual-garden shoots/<name>/adminkubeconfig request rather than treating it as an incident.

Manual method (background and troubleshooting)

This is the procedure ClusterLink automates. Useful for understanding exactly what the reconciler is doing, or for plumbing in a service cluster without applying a CR (e.g. a one-off, or before the operator is upgraded to a version that has ClusterLink).

Replace <gateway-cluster> / <service-cluster> with your own cluster names throughout.

  1. Create a port on the transit network for the service cluster's router, with a fixed IP starting at .3 or higher. .1 is conventionally the gateway cluster's own router; .2 is typically the network's DHCP port — pointing anything at .2 collides with DHCP and should be avoided.

    openstack port create --network vpn-transit \
      --fixed-ip subnet=vpn-transit-subnet,ip-address=<transit-leg-ip> \
      vpn-transit-<service-cluster>-leg
    
  2. Attach that port to the service cluster's router:

    openstack router add port shoot--services--<service-cluster> vpn-transit-<service-cluster>-leg
    
  3. Add a static route on the gateway cluster's own router, pointing the service cluster's node CIDR at the leg's IP:

    openstack router set --route destination=<service-cluster-node-CIDR>,gateway=<transit-leg-ip> \
      shoot--services--<gateway-cluster>
    

    --route appends, and it will happily point at the wrong port

    On some OpenStack client versions, --route appends to the router's existing route list rather than replacing it — it will not warn you about a stale or duplicate entry. Always check the result with openstack router show -c routes <router> afterward, and never point a route at the transit network's DHCP port.

  4. Open the target-side security group. The service being exposed — an internal LB VIP, a NodePort, whatever the exposure targets — has to admit ingress from the transit network's CIDR on the exposed ports; this is the same prerequisite the Networking guide's "Exposure target reachability" section already documents for same-cluster exposures, and it applies identically once the target lives in a different cluster.

  5. No extra route is needed for the return path. Once the router in step 2 has the transit-network leg, the service cluster gets a connected route back to the transit network for free — return traffic from the exposed service to the gateway needs nothing further.

Worked example from our reference deployment (validated 2026-08-19)

Concrete values below are from b'nerd's own reference deployment — worked example only, not requirements for your own estate.

  • Gateway cluster vpn01, service cluster svc01.
  • Transit leg: a port on vpn-transit, fixed IP 10.254.0.3, attached to svc01's router.
  • Route: vpn01's router got 10.251.0.0/16 via 10.254.0.3 (svc01's node CIDR, routed at the transit leg's IP).
  • Both are permanent infrastructure — kept after verification, unlike the scaffolding security-group rule and test pod used only to prove the path.
  • Verification: an IPsecGateway exposure on vpn01 targeting 100.64.0.11:8080 was reached through a live tunnel and landed on a pod on an svc01 worker, through the same tunnel already carrying vpn01's own same-cluster exposure — confirming a single gateway can reach exposures on multiple service clusters simultaneously once each is plumbed in.
  • This is exactly the shape a ClusterLink CR (serviceClusterRouter: shoot--services--svc01, serviceClusterCIDR: 10.251.0.0/16, spec.transitIP: 10.254.0.3 to match the existing leg) would take over via its adoption path, described above, without disturbing the tunnel or requiring the leg/route to be recreated.