A second look into the Kubernetes Gateway API on OpenShift
- - 10 min read
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
1 | We want to support HTTPS |
2 | We use the default HTTPS port 443 |
3 | The URLs we support with this listener are the same as for HTTP |
4 | We 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. |
5 | This is the name of the TLS secret we created above |
6 | We 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
1 | We 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
1 | Our 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
1 | We create the HTTPRoute in the gateway-api-test namespace |
2 | We 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)
1 | Now we are using the Selector option |
2 | Because 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
1 | Always deploy the gateway to the openshift-ingress namespace for the OpenShift Gateway API implementation |
2 | We 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
1 | Match our Gateway http-gateway |
2 | Match 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
1 | Hm, 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
1 | Ok, this is the parentRefs section we are looking for |
2 | name selects the name of the Gateway we want to attach to |
3 | namespace specifies the namespace where we can find the Gateway |
4 | sectionName 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
1 | The parentRefs section |
2 | The Gateway we would like to attach to |
3 | The 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)
1 | Creation timestamp of the redirect route |
2 | Creation 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)
1 | Creation timestamp of the HTTP-to-HTTPS route |
2 | Creation 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
1 | The HTTP endpoint returns a redirect |
2 | the 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
1 | We 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
1 | The Nginx route is the oldest route |
2 | The HTTP endpoint returns a redirect to HTTPS |
3 | The 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.
Copyright © 2020 - 2025 Toni Schmidbauer & Thomas Jungbauer