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.
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:
- The Gateway’s HTTPS listener (shown above) references a TLS Secret and sets
mode: Terminate. - Every
HTTPRoutethat should accept HTTPS traffic needs itsparentRefsentry 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
-
How Cilium Uses eBPF: Identity, Policy, and Sockets What actually happens in the kernel when a Pod starts, talks to another Pod, or gets a packet policy decision — and why it's fast. -
Set Up a Cluster for Cilium: Disable the Default CNI First Cilium replaces your cluster's networking entirely, which means the order of operations matters more than with most add-ons. -
Write Network Policies: From L3/L4 to L7 Rules Kubernetes starts with every Pod able to reach every other Pod — CiliumNetworkPolicy is how you narrow that down, down to the HTTP method if you need to.