A second look into the Kubernetes Gateway API on OpenShift

- Toni Schmidbauer Toni Schmidbauer ( Lastmod: 2025-08-31 ) - 10 min read

image from A second look into the Kubernetes Gateway API on OpenShift

This is our second look into the Kubernetes Gateway API an it’s integration into OpenShift. This post covers TLS configuration.

The Kubernetes Gateway API is new implementation of the ingress, load balancing and service mesh API’s. See upstream for more information.

Also the OpenShift documentation provides an overview of the Gateway API and it’s integration.

We demonstrate how to add TLS to our Nginx deployment, how to implement a shared Gateway and finally how to implement HTTP to HTTPS redirection with the Gateway API. Furthermore we cover how HTTPRoute objects attach to Gateways and dive into ordering of HTTPRoute objects.

Adding TLS to our Nginx deployment

In our fist post we simply exposed a Nginx web server via the Gateway API. We only enabled HTTP, so let’s try to do the same with HTTPS now.

Remember we use a DNS wildcard domain *.gtw.ocp.lan.stderr.at which points to our Gateway. The gateway is exposed via a Service of type LoadBalancer. We use MetalLB for this.

The first step is setting up a wildcard TLS certificate for our custom domain *.gtw.ocp.lan.stderr.at. We are using EasyRSA here, but use whatever tool you like.

Just for reference this is how we created a wildcard cert with EasyRSA:

$ EASYRSA_CERT_EXPIRE=3650 EASYRSA_EXTRA_EXTS="subjectAltName=DNS:*.gtw.ocp.lan.stderr.at" ./easyrsa gen-req gtw.ocp.lan.stderr.at
$ EASYRSA_CERT_EXPIRE=3650 ./easyrsa sign-req serverClient gtw.ocp.lan.stderr.at

EasyRSA stores the public key under pki/issued and the private key under pki/private. We copied the certificate and the private key to a temporary directory.

Next we need to remove the private key passphrase and create a Kubernetes secret from the private and pubic key:

$ openssl rsa -in gtw.ocp.lan.stderr.at.key -out gtw.ocp.lan.stderr.at-insecure.key
$ oc create secret -n openshift-ingress tls gateway-api --cert=gtw.ocp.lan.stderr.at.crt --key=gtw.ocp.lan.stderr.at-insecure.key

Now it’s time to add a TLS listener to our Gateway resource in the openshift-ingress namespace. Remember for the OpenShift Gateway API implementation, Gateways have to be deployed in the openshift-ingress namespace.

---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: http-gateway
  namespace: openshift-ingress
spec:
  gatewayClassName: openshift-default
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "*.gtw.ocp.lan.stderr.at"
  - name: https
    protocol: HTTPS (1)
    port: 443 (2)
    hostname: "*.gtw.ocp.lan.stderr.at" (3)
    tls:
      mode: Terminate (4)
      certificateRefs:
        - name: gateway-api (5)
    allowedRoutes: (6)
      namespaces:
        from: All
1We want to support HTTPS
2We use the default HTTPS port 443
3The URLs we support with this listener are the same as for HTTP
4We use edge termination for now, this means HTTP traffic will only be encrypted up to the gateway. From the gateway to our pod we speak plain HTTP.
5This is the name of the TLS secret we created above
6We accept routes from all namespaces
Also remember from our first post that we created a ReferenceGrant in the namespace where Nginx is running. Otherwise HTTP routes will not be accepted.

Finally lets try to access our Nginx pod via HTTPS:

$ curl -v https://nginx.gtw.ocp.lan.stderr.at
* Host nginx.gtw.ocp.lan.stderr.at:443 was resolved.
* IPv6: (none)
* IPv4: 10.0.0.150
*   Trying 10.0.0.150:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=gtw.ocp.lan.stderr.at
*  start date: Aug 30 10:01:33 2025 GMT
*  expire date: Aug 28 10:01:33 2035 GMT
*  subjectAltName: host "nginx.gtw.ocp.lan.stderr.at" matched cert's "*.gtw.ocp.lan.stderr.at"
*  issuer: CN=tntinfra CA
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to nginx.gtw.ocp.lan.stderr.at (10.0.0.150) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://nginx.gtw.ocp.lan.stderr.at/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: nginx.gtw.ocp.lan.stderr.at]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: nginx.gtw.ocp.lan.stderr.at
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< server: nginx/1.29.1
< date: Sat, 30 Aug 2025 14:30:20 GMT
< content-type: text/html
< content-length: 615
< last-modified: Wed, 13 Aug 2025 14:33:41 GMT
< etag: "689ca245-267"
< accept-ranges: bytes

(output omitted)

Yes, we can reach our Nginx via HTTPS, and the gateway presents the TLS certificate we created.

Be aware that we are still using the same HTTPRoute for Nginx from our previous blog post.

Just for completeness here is the HTTPRoute:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: nginx-route
spec:
  parentRefs:
  - name: http-gateway
    namespace: openshift-ingress
  hostnames: ["nginx.gtw.ocp.lan.stderr.at"]
  rules:
  - backendRefs:
    - name: nginx
      namespace: gateway-api-test
      port: 8080
Also Remember that we are using a dedicated Gateway and all HTTPRoutes must be in the namespace openshift-ingress

Moving to a shared gateway

Up until now we had to create all HTTPRoute objects in the openshift-ingress namespace. The Gateway API support two modes of operations:

  • Dedicated gateway: all HTTPRoute object need to be in the same namespace as the gateway

  • Shared gateway: The gateway runs in the openshift-ingress namespace and we allow HTTPRoute objects from all or specific namespaces.

The first step in creating a shared gateway is to modify the gateway resource:

---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: http-gateway
  namespace: openshift-ingress
spec:
  gatewayClassName: openshift-default
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "*.gtw.ocp.lan.stderr.at"
    allowedRoutes: (1)
      namespaces:
        from: All
1We now allow HTTPRoute objects from all namespaces in the cluster

Next we delete the existing HTTPRoute for Nginx in the openshift-ingress namespaces, and verify that we can’t reach Nginx:

$  oc delete httproutes.gateway.networking.k8s.io -n openshift-ingress nginx-route
httproute.gateway.networking.k8s.io "nginx-route" deleted
$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 404 Not Found (1)
date: Sat, 30 Aug 2025 15:02:23 GMT
transfer-encoding: chunked
1Our Nginx route stopped working

Next we apply our modified Gateway resource in the openshift-ingress namespace and the HTTPRoute object in the gateway-api-test namespace.

$ oc apply -n openshift-ingress -f gateway--selector.yaml
gateway.gateway.networking.k8s.io/http-gateway configured
$ oc apply -n gateway-api-test -f httproute.yaml (1)
httproute.gateway.networking.k8s.io/nginx-route created
$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 200 OK (2)
server: nginx/1.29.1
date: Sat, 30 Aug 2025 15:04:34 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 13 Aug 2025 14:33:41 GMT
etag: "689ca245-267"
accept-ranges: bytes
1We create the HTTPRoute in the gateway-api-test namespace
2We can reach our Nginx pod again

So our shared gateway seems to be working. But what if we want to restrict which namespaces are allowed to create route objects?

The Gateway API allows the following settings under spec.listeners[].allowedRoutes.namespaces.from field

  • All: Allow from all namespaces

  • Selector: Specify a selector

  • Same: Only allow HTTPRoutes in the same namespaces

  • None: Do not allow any routes to attach

See the API specification FromNamespaces for details.

Let’s try to use a more specific selector for our gateway:

---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: http-gateway
  namespace: openshift-ingress
spec:
  gatewayClassName: openshift-default
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "*.gtw.ocp.lan.stderr.at"
    allowedRoutes:
      namespaces:
        from: Selector (1)
        selector:
          matchLabels:
            kubernetes.io/metadata.name: gateway-api-test (2)
1Now we are using the Selector option
2Because we do not have a specific label on the namespace we would like to use, let’s use the metadata.name label Kubernetes created for us

We create a new yaml file gateway-selector.yaml and appy the new configuration:

$ oc apply -n openshift-ingress -f gateway-selector.yaml
gateway.gateway.networking.k8s.io/http-gateway configured
$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 200 OK
server: nginx/1.29.1
date: Sat, 30 Aug 2025 15:17:17 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 13 Aug 2025 14:33:41 GMT
etag: "689ca245-267"
accept-ranges: bytes

All good, still working.

Just for testing we modified the namespace name in the Gateway definition to NOT match the namespace of our Nginx deployment and confirmed that we receive a 404 not found response.

Implementing HTTP to HTTPS redirect

As a last test for this post let’s try to implement HTTP to HTTPS redirects.

We deployed the following Gateway configuration:

---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: http-gateway
  namespace: openshift-ingress (1)
spec:
  gatewayClassName: openshift-default
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "*.gtw.ocp.lan.stderr.at"
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            kubernetes.io/metadata.name: gateway-api-test2
  - name: https (2)
    protocol: HTTPS
    port: 443
    hostname: "*.gtw.ocp.lan.stderr.at"
    tls:
      mode: Terminate
      certificateRefs:
        - name: gateway-api
    allowedRoutes:
      namespaces:
        from: All
1Always deploy the gateway to the openshift-ingress namespace for the OpenShift Gateway API implementation
2We added the HTTPS configuration back

The upstream documentation contains an example on how to implements HTTP to HTTPS redirects. We created the following additional HTTPRoute object in the gateway-api-test namespace:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-https-redirect
spec:
  parentRefs:
  - name: http-gateway (1)
    namespace: openshift-ingress
    sectionName: http (2)
  hostnames:
  - nginx.gtw.ocp.lan.stderr.at
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        statusCode: 301
1Match our Gateway http-gateway
2Match the http section in our gateway

Just for reference this is the HTTPRoute object to expose Nginx:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: nginx-route
spec:
  parentRefs:
  - name: http-gateway
    namespace: openshift-ingress
  hostnames: ["nginx.gtw.ocp.lan.stderr.at"]
  rules:
  - backendRefs:
    - name: nginx
      namespace: gateway-api-test
      port: 8080

First we re-applied our Gateway configuration

$ oc apply -f gateway-https-selector.yaml
gateway.gateway.networking.k8s.io/http-gateway configured

Let’s try and verify if our redirect is working, we need to apply both routes:

$ oc apply -f httproute.yaml
httproute.gateway.networking.k8s.io/nginx-route created
$ oc apply -f http-https-redirect-route.yaml
httproute.gateway.networking.k8s.io/http-https-redirect created

And test with curl:

$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 200 OK (1)
server: nginx/1.29.1
date: Sat, 30 Aug 2025 15:37:20 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 13 Aug 2025 14:33:41 GMT
etag: "689ca245-267"
accept-ranges: bytes
1Hm, strange we still get 200 OK and NOT a redirect to HTTPS

Understanding HTTPRoute ordering

After a longer search through the documentation we found some hints on why this is happening.

Let’s take a more detailed look at our http-to-https route again, as a HTTPRoute attaches to a Gateway, we focus on the parentRefs in the HTTPRoute object. In our current understanding parentRefs select a Gateway:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-https-redirect
spec:
  parentRefs: (1)
  - name: http-gateway (2)
    namespace: openshift-ingress (3)
    sectionName: http (4)
  hostnames:
  - nginx.gtw.ocp.lan.stderr.at
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        statusCode: 301
1Ok, this is the parentRefs section we are looking for
2name selects the name of the Gateway we want to attach to
3namespace specifies the namespace where we can find the Gateway
4sectionName selects the section in the Gateway where we want to attach to.

So this HTTPRoute explicitly attaches to a Gateway in a Namespace that has a Section http defined.

If you look at the Gateway configuration above you will see that we have a section for HTTP traffic and one for HTTPS traffic.

Let’s compare this with our Nginx HTTPRoute definition:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: nginx-route
spec:
  parentRefs: (1)
  - name: http-gateway (2)
    namespace: openshift-ingress (3)
  hostnames: ["nginx.gtw.ocp.lan.stderr.at"]
  rules:
  - backendRefs:
    - name: nginx
      namespace: gateway-api-test
      port: 8080
1The parentRefs section
2The Gateway we would like to attach to
3The namespace where the Gateway is deploy

Note that Section is missing in this configuration.

So this HTTPRoute actually attaches to both sections in our Gateway definition, HTTP and HTTPS. Which is not what we want.

  • When a client hits the HTTP endpoint we want to redirect the traffic to HTTPS

  • When a client hits the HTTPS endpoint we want the traffic to be forward to our Nginx deployment

We found the following statement statement how ordering works in the Gateway API:

If ties still exist across multiple Routes, matching precedence MUST be
determined in order of the following criteria, continuing on ties:
The oldest Route based on creation timestamp.

When we look at the timestamps of our HTTPRoutes:

oc get httproute -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.creationTimestamp}{"\n"}{end}'
http-https-redirect     2025-08-31T09:17:46Z (1)
nginx-route     2025-08-31T09:17:40Z (2)
1Creation timestamp of the redirect route
2Creation timestamp of the nginx route

The Nginx HTTPRoute is older than the HTTP-to-HTTP HTTPRoute. So this matches first and a 200 OK is returned.

So let’s try to revers how we applied our HTTPRoutes:

$ oc delete httproutes.gateway.networking.k8s.io --all
httproute.gateway.networking.k8s.io "http-https-redirect" deleted
httproute.gateway.networking.k8s.io "nginx-route" deleted

$ oc apply -f http-to-https-httproute.yaml
httproute.gateway.networking.k8s.io/http-https-redirect created
$ oc apply -f nginx-httproute.yaml
httproute.gateway.networking.k8s.io/nginx-route created

$ oc get httproute -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.creationTimestamp}{"\n"}{end}'
http-https-redirect     2025-08-31T10:34:55Z (1)
nginx-route     2025-08-31T10:35:11Z (2)
1Creation timestamp of the HTTP-to-HTTPS route
2Creation timestamp of the nginx route

Now the HTTP-to-HTTPS route is the oldest route. Let’s try again calling Nginx with curl:

$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 301 Moved Permanently (1)
location: https://nginx.gtw.ocp.lan.stderr.at/
date: Sun, 31 Aug 2025 10:37:13 GMT
transfer-encoding: chunked

$ curl -I https://nginx.gtw.ocp.lan.stderr.at
HTTP/2 200 (2)
server: nginx/1.29.1
date: Sun, 31 Aug 2025 10:37:17 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 13 Aug 2025 14:33:41 GMT
etag: "689ca245-267"
accept-ranges: bytes
1The HTTP endpoint returns a redirect
2the HTTPS endpoint returns 200 OK from Nginx

So now we have the expected behavior: HTTP is redirect to HTTPS!

As depending on the time when an object is created is definitely NOT a good idea, let’s be more specific in our Nginx HTTPRoute:

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: nginx-route
spec:
  parentRefs:
  - name: http-gateway
    namespace: openshift-ingress
    sectionName: https (1)
  hostnames: ["nginx.gtw.ocp.lan.stderr.at"]
  rules:
  - backendRefs:
    - name: nginx
      namespace: gateway-api-test
      port: 8080
1We explicitly select the HTTPS section in our Gateway configuration

Next we delete our HTTPRoutes again, and re-apply them in the order that didn’t work the first time (Nginx is the oldest route):

$ oc delete httproutes.gateway.networking.k8s.io --all
httproute.gateway.networking.k8s.io "http-https-redirect" deleted
httproute.gateway.networking.k8s.io "nginx-route" deleted

$ oc apply -f http-to-https-httproute.yaml
httproute.gateway.networking.k8s.io/http-https-redirect created

$ oc get httproute -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.creationTimestamp}{"\n"}{end}'
http-https-redirect     2025-08-31T10:45:01Z
nginx-route     2025-08-31T10:44:57Z (1)

$ curl -I http://nginx.gtw.ocp.lan.stderr.at
HTTP/1.1 301 Moved Permanently (2)
location: https://nginx.gtw.ocp.lan.stderr.at/
date: Sun, 31 Aug 2025 10:46:22 GMT
transfer-encoding: chunked

$ curl -I https://nginx.gtw.ocp.lan.stderr.at
HTTP/2 200 (3)
server: nginx/1.29.1
date: Sun, 31 Aug 2025 10:46:30 GMT
content-type: text/html
content-length: 615
last-modified: Wed, 13 Aug 2025 14:33:41 GMT
etag: "689ca245-267"
accept-ranges: bytes
1The Nginx route is the oldest route
2The HTTP endpoint returns a redirect to HTTPS
3The response from our Nginx deployment

Finally everything works as expected!

A HTTPRoute attaches to a Gateway. Always be as specific as possible which Gateway to match and which section in the Gateway.

Conclusion

In this blog post we demonstrated to implement TLS with the Gateway API. We also implemented a shared Gateway with HTTPRoute objects in different namespaces.

Furthermore we configured HTTP to HTTPS redirects and dove into HTTPRoute ordering if a route matches multiple listeners in a Gateway definition.