Cilium Cilium

Expose Services with Cilium's Gateway API Support

GatewayClass, Gateway, and HTTPRoute — host-based routing, path matching, URL rewrites, and TLS, with no separate gateway Pods.

14 min Intermediate

Kubernetes Gateway API is the standardized successor to Ingress, and Cilium implements it directly using its own eBPF data path plus an embedded Envoy for Layer 7 processing. This guide covers the core resources, GatewayClass, Gateway, HTTPRoute, and the routing patterns built from them, assuming Cilium is already installed with Gateway API enabled (see the previous guide).

GatewayClass: declaring Cilium as an available controller

A GatewayClass tells the cluster “this controller is available for Gateways to use”:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: cilium
spec:
  controllerName: io.cilium/gateway-controller

If you set gatewayAPI.gatewayClass.create: false during installation (as the previous guide recommends), this is the resource you create yourself to fill that gap.

Customize the class with CiliumGatewayClassConfig

Cilium provides its own CRD, CiliumGatewayClassConfig, for settings that should apply to every Gateway using a given class, most commonly the Kubernetes Service type the Gateway gets exposed through:

apiVersion: cilium.io/v2alpha1
kind: CiliumGatewayClassConfig
metadata:
  name: gateway-api-config
  namespace: default
spec:
  service:
    type: NodePort
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: cilium
spec:
  controllerName: io.cilium/gateway-controller
  parametersRef:
    group: cilium.io
    kind: CiliumGatewayClassConfig
    name: gateway-api-config
    namespace: default

NodePort here is specifically a kind-cluster accommodation, covered below; on a cluster with a real cloud load balancer available, LoadBalancer is the more typical choice, and later guides in this series on BGP route advertisement cover getting a routable address without a cloud provider’s load balancer at all.

Create a Gateway

A Gateway is the actual listener configuration, referencing the class above:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: demo-gateway
  namespace: default
spec:
  gatewayClassName: cilium
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
    - name: https
      protocol: HTTPS
      port: 443
      allowedRoutes:
        namespaces:
          from: All
      tls:
        mode: Terminate
        certificateRefs:
          - name: demo-tls-secret

No gateway Pods: this is where Cilium differs

With most Gateway API implementations, creating a Gateway produces a dedicated Deployment and Pods for that gateway instance. With Cilium, it doesn’t: traffic is intercepted via eBPF at the node level and routed according to the Gateway’s configuration, with only a Kubernetes Service appearing, no Pods dedicated to the Gateway itself.

kubectl get gateway demo-gateway
kubectl get service -l gateway.networking.k8s.io/gateway-name=demo-gateway

On a kind cluster specifically, a NodePort Service gets a randomized port by default, not the ports you actually want (30080/30443 from the earlier kind cluster config). Patch it explicitly:

kubectl patch service <gateway-service-name> --type='json' -p='[
  {"op": "replace", "path": "/spec/ports/0/nodePort", "value": 30080},
  {"op": "replace", "path": "/spec/ports/1/nodePort", "value": 30443}
]'

This patching step is purely a kind-cluster workaround for its lack of a real load balancer, not a general Cilium limitation. On a cluster with an actual cloud LoadBalancer implementation, or with Cilium’s own L2 announcements or BGP route advertisement (a later guide in this series) in place, the Gateway’s Service gets a routable address without manual patching.

Route by hostname

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: python-route
  namespace: default
spec:
  parentRefs:
    - name: demo-gateway
  hostnames:
    - "python.example.com"
  rules:
    - backendRefs:
        - name: python-service
          port: 5000

parentRefs attaches this route to the Gateway (they must be in the same namespace, or the route needs an explicit cross-namespace reference policy). Any request arriving with the Host header python.example.com is sent to python-service.

Route by path: exact vs. prefix matching

  rules:
    - matches:
        - path:
            type: Exact
            value: /
      backendRefs:
        - name: python-service
          port: 5000

Exact matches only that literal path; a request for /status wouldn’t match a rule scoped to /. For anything beyond a single fixed path, PathPrefix matches the given prefix and forwards the rest of the path along unchanged:

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /

The distinction matters in practice: an API with several endpoints behind one route needs PathPrefix, not Exact, or every endpoint beyond the literal matched path returns a route-not-found rather than reaching the backend at all.

URL rewrites: a clean external path, a different internal one

A common pattern is exposing multiple backend services under one consolidated hostname with a tidy path scheme, while each backend keeps its own internal routes unaware of that external prefix:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: python-route
  namespace: default
spec:
  parentRefs:
    - name: demo-gateway
  hostnames:
    - "example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api/python
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      backendRefs:
        - name: python-service
          port: 5000

A request to example.com/api/python/status is matched by the /api/python prefix, rewritten to /status before it reaches python-service, so the backend itself never needs to know about the /api/python prefix the outside world sees. Define a parallel route for each backend service (/api/go to a Go service, and so on) to build a single consolidated API gateway out of several independently-developed backends, each kept unaware of the path structure the gateway presents externally.

TLS: two things need to agree

Serving HTTPS traffic needs two pieces configured consistently:

  1. The Gateway’s HTTPS listener (shown above) references a TLS Secret and sets mode: Terminate.
  2. Every HTTPRoute that should accept HTTPS traffic needs its parentRefs entry to actually target that HTTPS listener by name, not just the Gateway as a whole:
  parentRefs:
    - name: demo-gateway
      sectionName: https

Without sectionName pointing at the HTTPS listener specifically, a route attaches to whichever listener(s) its match criteria fit by default, which may not include the one you intended. Create the TLS Secret before applying a Gateway that references it:

kubectl create secret tls demo-tls-secret \
  --cert=path/to/cert.pem \
  --key=path/to/key.pem \
  -n default

kubectl describe gateway demo-gateway is the first place to check if a listener isn’t accepting traffic as expected; a missing or malformed TLS Secret is a common reason a listener shows as not accepted.

Popular Guides