Gateway API
For our exercises we now start with Gateway API and Traefik , an Open Source Ingress Controller that is a safe and popular choice when you need a simple solution. It is maintained and well-integrated by the Kubernetes project itself. It has already been set up in our test cluster beforehand (via helm, by the way, see Setup instructions above) so we can jump right at making use of it.
As the name implies, this solution utilizes Traefik, an open-source Application Proxy for publishing services, with the means to easily configure it to our purposes. Under the hood things are not so simple, of course, and you can view the various components via
kubectl get all --all-namespaces -l app.kubernetes.io/name=traefik
(note the --all-namespaces), e.g.:
NAMESPACE NAME READY STATUS RESTARTS AGE
traefik pod/traefik-754d89dc55-6pcx2 1/1 Running 0 7h16m
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik service/traefik LoadBalancer 10.0.190.105 <ingress_ip_traefik> 80:30732/TCP,443:30445/TCP 7h16m
traefik service/traefik-metrics ClusterIP 10.0.153.12 <none> 9100/TCP 7h16m
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
traefik deployment.apps/traefik 1/1 1 1 7h16m
NAMESPACE NAME DESIRED CURRENT READY AGE
traefik replicaset.apps/traefik-754d89dc55 1 1 1 7h16mBut for now we will just use what Traefik provides without diving into too much detail on how this will be achieved.
Note
As we will make use of the Traefik Ingress IP quite a lot, best export it to your environment: export INGRESS_IP_TRAEFIK=<your_ingress_ip>
export INGRESS_IP_TRAEFIK=$(kubectl get service --namespace traefik traefik -o jsonpath='{.status.loadBalancer.ingress[].ip}')
should do the trick, which you can then verify via
echo $INGRESS_IP_TRAEFIK
This variable will not persist over a logout nor will it spread to other separate sessions, so remember to set it again whenever you (re)connect to your user VM.
Namespace
With Traefik still being a pre-defined cluster-wide central solution, we again need to take care to not step on each others’ toes in the following exercises. For that we will prefix some parts of the following resources with a user-specific namespace.
Note
As we will use this personal namespace several times as a resource prefix, make it available now for easy consumption via a variable:
export NAMESPACE=$(kubectl config view --minify --output 'jsonpath={..namespace}'); echo $NAMESPACE
This variable will not persist over a logout nor will it spread to other separate sessions, so remember to set it again whenever you (re)connect to your user VM.
Exercise - Recreate our sample application
Remember how to use the command line to create a Deployment called sampleapp with the image novatec/technologyconsulting-hello-container:v0.1?
You still have that running, i.e. Kubernetes told you that Error from server (AlreadyExists): deployments.apps “sampleapp” already exists ? Doesn’t matter, let’s just keep using it then.
And now we will expose that Deployment using a LoadBalancer Service, in effect similarly to what we have covered in Services but with a different command:
kubectl expose deployment sampleapp --type LoadBalancer --port 8080
Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP
For good measure, let’s confirm that we can access it just fine. Do you know how?
So far so good:
graph LR;
I{Internet} -->|LoadBalancer:8080| S
subgraph Kubernetes
subgraph your Namespace
S(sampleapp)
end
end
Exercise - Expose our sample application using Gateway API
Now create sampleapp-gateway.yaml by executing the following (yes, execute it all at once), utilizing your personal
namespace as a host component, and our Traefik Ingress IP by way of nip.io
wildcard DNS as a suffix:
cat <<.EOF > sampleapp-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: sampleapp
spec:
gatewayClassName: traefik
listeners:
- name: sampleapp
hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
port: 8000
protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sampleapp
spec:
parentRefs:
- name: sampleapp
hostnames:
- hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
rules:
- backendRefs:
- name: sampleapp
port: 8080
matches:
- path:
type: PathPrefix
value: /
.EOFCreate the Gateway API resources by running the following command:
kubectl apply -f sampleapp-gateway.yaml
Take note that this yaml includes two separate resources divided by a YAML document delimiter, the Gateway and a HTTPRoute that attaches to the Gateway via its parentRefs.
Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-GATEWAY
Verify the IP address is set:
kubectl get gateway,httproute sampleapp
NAME CLASS ADDRESS PROGRAMMED AGE
gateway.gateway.networking.k8s.io/sampleapp traefik <ingress_ip_traefik> True 2m26s
NAME HOSTNAMES AGE
httproute.gateway.networking.k8s.io/sampleapp ["hello.<your_namespace>.<ingress_ip_traefik>.nip.io"] 2m26sNote
This can take a couple of minutes.
Conceptually this now really similar to what did in Services , we could visualize the access path like this:
graph LR;
A{Internet} -->|LoadBalancer:80| G
subgraph Kubernetes
subgraph your Namespace
G(Gateway)
H(HTTPRoute)
S(sampleapp)
G -->|Listener:HTTP| H
H -->|ClusterIP:8080| S
end
end
Then verify we can access our sampleapp through Gateway API:
curl --verbose http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
* Host hello.<your_namespace>.<ingress_ip_traefik>.nip.io:80 was resolved.
* IPv6: (none)
* IPv4: <ingress_ip_traefik>
* Trying <ingress_ip_traefik>:80...
* Connected to hello.<your_namespace>.<ingress_ip_traefik>.nip.io (<ingress_ip_traefik>) port 80
> GET /hello HTTP/1.1
> Host: hello.<your_namespace>.<ingress_ip_traefik>.nip.io
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 57
< Content-Type: text/plain;charset=UTF-8
< Date: Fri, 13 Feb 2026 14:48:28 GMT
<
* Connection #0 to host hello.<your_namespace>.<ingress_ip_traefik>.nip.io left intact
Hello World (from sampleapp-846cd66cfb-fgfq2) to somebodyOK, so this works. Now let’s not expose our sampleapp via a LoadBalancer Service anymore, but restrict access to ClusterIP, i.e. disallow direct access from outside of our cluster:
kubectl delete services sampleapp; kubectl expose deployment sampleapp --type ClusterIP --port 8080
Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SERVICE
And confirm we can still access it through Gateway API:
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
Hello World (from sampleapp-846cd66cfb-fgfq2) to somebodyWell, it still works: via Gateway API our request gets redirected cluster-internally to the ClusterIP of our Service, and then to the sampleapp pod. But how do we benefit from this? Not really so far with such a simple application, and with only a single application we didn’t even save on external IP addresses required. But let’s dive into more detail.
Exercise - Extend our sample application
Create sampleapp-subpath.yaml by executing the following (i.e. do everything we have done for our sampleapp again, but
now a bit differently, and from YAML and not directly via command line):
cat <<.EOF > sampleapp-subpath.yaml
apiVersion: v1
kind: Service
metadata:
name: sampleapp-subpath
spec:
type: ClusterIP
ports:
- port: 8080
selector:
app: sampleapp-subpath
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sampleapp-subpath
spec:
replicas: 1
selector:
matchLabels:
app: sampleapp-subpath
template:
metadata:
name: sampleapp-subpath
labels:
app: sampleapp-subpath
spec:
containers:
- name: sampleapp-subpath
env:
- name: PROPERTY
value: everyone via Gateway API from a subpath
image: novatec/technologyconsulting-hello-container:v0.1
restartPolicy: Always
.EOFCreate these resources by running the following command:
kubectl apply -f sampleapp-subpath.yaml
Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SUBPATH
What would this application serve, and how could you access it right now?
Now extend our sampleapp-gateway.yaml as follows:
- backendRefs:
- name: sampleapp-subpath
port: 8080
matches:
- path:
type: PathPrefix
value: /subpath
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /Apply the change:
kubectl apply -f sampleapp-gateway.yaml
Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SUBPATH-GATEWAY
Verify that the HTTPRoute is configured as intended:
kubectl describe httproute sampleapp
Name: sampleapp
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
API Version: gateway.networking.k8s.io/v1
Kind: HTTPRoute
[...]
Spec:
Hostnames:
hello.<your_namespace>.<ingress_ip_traefik>.nip.io
Parent Refs:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: sampleapp
Rules:
Backend Refs:
Group:
Kind: Service
Name: sampleapp
Port: 8080
Weight: 1
Matches:
Path:
Type: PathPrefix
Value: /
Backend Refs:
Group:
Kind: Service
Name: sampleapp-subpath
Port: 8080
Weight: 1
Filters:
Type: URLRewrite
URL Rewrite:
Path:
Replace Prefix Match: /
Type: ReplacePrefixMatch
Matches:
Path:
Type: PathPrefix
Value: /subpath
[...]And then access our applications like this:
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echowhich should yield
Hello World (from sampleapp-846cd66cfb-fgfq2) to somebody
Hello World (from sampleapp-subpath-577ddd78df-b66fs) to everyone via Gateway API from a subpathIn other words, the most specific match succeeds, and we have integrated two slightly different applications, each served by a separate microservice, into a single serving domain like this:
hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> / sampleapp:8080
/subpath sampleapp-subpath:8080graph LR;
A{Internet} -->|LoadBalancer:80| G
subgraph Kubernetes
subgraph your Namespace
G(Gateway)
H(HTTPRoute)
S(sampleapp)
U(sampleapp-subpath)
G -->|Listener:HTTP| H
H -->|/<br />ClusterIP:8080| S
H -->|/subpath<br />ClusterIP:8080| U
end
end
Of course, with our sampleapp this doesn’t yet make much sense. But think of different microservices each serving a specific version of an API, and having them all accessible at /api/v1/, /api/v2/, /api/v3/ … Or generally think of any multi-microservice-driven application that needs to comply with Same-origin policy , and you notice you cannot implement this via simple LoadBalancer Service definitions.
So, let’s continue with this and roll out a new version of our sampleapp.
Exercise - Gradual rollout a new version of our sampleapp
In a production scenario, we do not want to roll out a new version of an application straight away in case any issues that were not detected during development and testing arise. Therefore we want to gradually deploy our new version in a controlled manner.
- Manually target the new version via a request header (canary rollout).
- Weighted targeting of the new version via traffic splitting.
- Fully target the new version (weight 100%).
Let’s continue with these steps.
Exercise - Canary rollout
Create sampleapp-v2.yaml by executing the following:
cat <<.EOF > sampleapp-v2.yaml
apiVersion: v1
kind: Service
metadata:
name: sampleapp-v2
spec:
type: ClusterIP
ports:
- port: 8080
selector:
app: sampleapp-v2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sampleapp-v2
spec:
replicas: 1
selector:
matchLabels:
app: sampleapp-v2
template:
metadata:
name: sampleapp-v2
labels:
app: sampleapp-v2
spec:
containers:
- name: sampleapp-v2
env:
- name: PROPERTY
value: everyone via Gateway API from v2
image: novatec/technologyconsulting-hello-container:v0.1
restartPolicy: Always
.EOFCreate these resources by running the following command:
kubectl apply -f sampleapp-v2.yaml
To be able to access the new version extend our sampleapp-gateway.yaml as follows:
- backendRefs:
- name: sampleapp-v2
port: 8080
matches:
- path:
type: PathPrefix
value: /
- headers:
- name: version
value: v2Apply the change:
kubectl apply -f sampleapp-gateway.yaml
The sampleapp and sampleapp-subpath are still accessible as before. To access the sampleapp-v2, a header version: v2 is required. Access the three applications like this:
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl -H "version: v2" http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echowhich should yield
Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpathWe have integrated a new sampleapp-v2 like this:
hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> / sampleapp:8080
/ header=version:v2 sampleapp-v2:8080
/subpath sampleapp-subpath:8080graph LR;
A{Internet} -->|LoadBalancer:80| G
subgraph Kubernetes
subgraph your Namespace
G(Gateway)
H(HTTPRoute)
S(sampleapp)
V(sampleapp-v2)
U(sampleapp-subpath)
G -->|Listener:HTTP| H
H -->|/<br />ClusterIP:8080| S
H -->|/<br />header=version:v2<br />ClusterIP:8080| V
H -->|/subpath<br />ClusterIP:8080| U
end
end
Exercise - Blue-green traffic rollout
Adapt our sampleapp-gateway.yaml as follows:
- backendRefs:
- name: sampleapp
port: 8080
weight: 50
- name: sampleapp-v2
port: 8080
weight: 50
matches:
- path:
type: PathPrefix
value: /Apply the change:
kubectl apply -f sampleapp-gateway.yaml
The sampleapp and sampleapp-subpath are still accessible, just as before. To access the sampleapp-v2 a header version: v2 is required. Access the three applications like this:
for i in {1..10}; do curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo; done
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echowhich should yield
Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
...
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpathUsers no longer need to include a specific header (for example, when using curl) to access the new sampleapp version. Traffic is now distributed automatically based on the configured weight settings.
hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> / weight=50 sampleapp:8080
/ weight=50 sampleapp-v2:8080
/subpath sampleapp-subpath:8080graph LR;
A{Internet} -->|LoadBalancer:80| G
subgraph Kubernetes
subgraph your Namespace
G(Gateway)
H(HTTPRoute)
S(sampleapp)
V(sampleapp-v2)
U(sampleapp-subpath)
G -->|Listener:HTTP| H
H -->|/<br />weight=50<br />ClusterIP:8080| S
H -->|/<br />weight=50<br />ClusterIP:8080| V
H -->|/subpath<br />ClusterIP:8080| U
end
end
Feel free to adjust the weights and see what happens.
To finish the gradual rollout we only have to fully route the traffic to the sampleapp-v2 by adjusting the weights.
Adapt our sample-gateway.yaml just like this:
- backendRefs:
- name: sampleapp
port: 8080
weight: 0
- name: sampleapp-v2
port: 8080
weight: 1
matches:
- path:
type: PathPrefix
value: /Apply the change:
kubectl apply -f sampleapp-gateway.yaml
The traffic should only list responses from sampleapp-v2 as shown below:
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
...
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpathNow we have fully migrated to the new version v2 and we could delete the old resources, the sampleapp deployment, service and reference to the sampleapp from the httproute including the weight attributes.
Exercise - Extend our ToDo application
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:80 -> todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443 -> <ingress_ip_traefik>:443 -> / todoui:8090
/backend/v2 todobackend:8080 w/ basic auth (for debugging)graph LR;
A{Internet} -->|LoadBalancer:80<br />LoadBalancer:443| G
subgraph Kubernetes
subgraph your Namespace
G(Gateway)
H(HTTPRoute)
T(HTTPRoute)
U(todoui)
B(todobackend)
P(postgresdb)
end
G -->|Listener:HTTP| H
H -->|redirect to HTTPS| G
G -->|Listener:HTTPS| T
T -->|/<br />ClusterIP:8090| U
T -->|/backend/v2<br />basic auth<br />ClusterIP:8080| B
U -->|ClusterIP:8080| B
B -->|ClusterIP:5432| P
end
So we will need a Gateway, one HTTPRoute to frontend and one to backend, a redirect to https, basic auth settings, and a certificate for TLS termination. (Of course, normally you’d have versioned all backend Deployments, but let’s just take the easy path here and work with what we already have.)
TLS certificate
Self-signed will suffice for now:
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
-keyout todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.key \
-out todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt \
-subj "/CN=todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io"which will output some line(s) with symbols indicating its generating data.
Verify the certificate’s CN:
openssl x509 -in todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt -noout -subject
subject=CN=todo.<your_namespace>.<your_ingress_ip>.nip.ioLoad this as a Kubernetes secret:
kubectl create secret tls todo-gateway-tls-secret --key todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.key --cert todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt
secret/todo-gateway-tls-secret createdAnd verify it is present:
kubectl describe secrets todo-gateway-tls-secret
Name: todo-gateway-tls-secret
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
Type: kubernetes.io/tls
Data
====
tls.crt: 1180 bytes
tls.key: 1704 bytesRemember from Exercise - create a Secret how to verify the contents?
Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-CERT
Basic auth
Well, we do not have htpasswd installed locally. No problem, Docker to the rescue:
Info
This could take some time because your local docker daemon has to download the httpd:latest image!
docker run -it --rm -v $(pwd):/tempdir -w /tempdir httpd:latest htpasswd -c auth backenddebugger
New password: password
Re-type new password: password
Adding password for user backenddebuggerYes, it is important that the file generated is named auth (actually - that the secret which we are about to create
has a key data.auth), and yes, let’s use the literal password as password, just for the sake of simplicity.
Verify the contents which have conveniently been placed into our current work directory:
cat auth
backenddebugger:$apr1$K3X6TKeX$RQaGr8FqnXYxgDU4Z4lBa0Load this as a Kubernetes secret, with a generic name as we are going to reuse this later with Traefik:
kubectl create secret generic todo-backend-basic-auth --from-file auth
secret/todo-backend-basic-auth createdAnd verify it is present and it contains the correct data:
kubectl describe secrets todo-backend-basic-auth
Name: todo-backend-basic-auth
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
auth: 54 byteskubectl get secrets todo-backend-basic-auth -o jsonpath='{.data.auth}' | base64 --decode
backenddebugger:$apr1$K3X6TKeX$RQaGr8FqnXYxgDU4Z4lBa0Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-AUTH
Gateway API
Thus, now it is time to plug it all together. Create a file todoapp-gateway.yaml by executing the following (yes,
execute it all at once), again utilizing your personal namespace as a host prefix:
cat <<.EOF > todoapp-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: todo-gateway
spec:
gatewayClassName: traefik
listeners:
- name: todo-http
hostname: todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
port: 8000
protocol: HTTP
- name: todo-https
hostname: todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
port: 8443
protocol: HTTPS
tls:
certificateRefs:
- name: todo-gateway-tls-secret
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: todo-redirect
spec:
parentRefs:
- name: todo-gateway
sectionName: todo-http
hostnames:
- todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: todo-backend-auth
spec:
basicAuth:
secret: todo-backend-basic-auth
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: todo
spec:
parentRefs:
- name: todo-gateway
sectionName: todo-https
hostnames:
- todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
rules:
- backendRefs:
- name: todoui
port: 8090
matches:
- path:
type: PathPrefix
value: /
- backendRefs:
- name: todobackend
port: 8080
matches:
- path:
type: PathPrefix
value: /backend/v2
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: todo-backend-auth
.EOFApply it with kubectl apply -f todoapp-gateway.yaml and verify what has been created:
kubectl describe gateway todo-gateway
Name: todo-gateway
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
API Version: gateway.networking.k8s.io/v1
Kind: Gateway
[...]
Spec:
Gateway Class Name: traefik
Listeners:
Allowed Routes:
Namespaces:
From: Same
Hostname: todo.<your_namespace>.<ingress_ip_traefik>.nip.io
Name: todo-http
Port: 8000
Protocol: HTTP
Allowed Routes:
Namespaces:
From: Same
Hostname: todo.<your_namespace>.<ingress_ip_traefik>.nip.io
Name: todo-https
Port: 8443
Protocol: HTTPS
Tls:
Certificate Refs:
Group:
Kind: Secret
Name: todo-gateway-tls-secret
Mode: Terminate
[...]and
kubectl describe httproutes todo-redirect todo
Name: todo-redirect
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
API Version: gateway.networking.k8s.io/v1
Kind: HTTPRoute
[...]
Spec:
Hostnames:
todo.<your_namespace>.<ingress_ip_traefik>.nip.io
Parent Refs:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: todo-gateway
Section Name: todo-http
Rules:
Filters:
Request Redirect:
Scheme: https
Status Code: 301
Type: RequestRedirect
Matches:
Path:
Type: PathPrefix
Value: /
[...]
Name: todo
Namespace: <your_namespace>
Labels: <none>
Annotations: <none>
API Version: gateway.networking.k8s.io/v1
Kind: HTTPRoute
[...]
Spec:
Hostnames:
todo.<your_namespace>.<ingress_ip_traefik>.nip.io
Parent Refs:
Group: gateway.networking.k8s.io
Kind: Gateway
Name: todo-gateway
Section Name: todo-https
Rules:
Backend Refs:
Group:
Kind: Service
Name: todoui
Port: 8090
Weight: 1
Matches:
Path:
Type: PathPrefix
Value: /
Backend Refs:
Group:
Kind: Service
Name: todobackend
Port: 8080
Weight: 1
Filters:
Type: URLRewrite
URL Rewrite:
Path:
Replace Prefix Match: /
Type: ReplacePrefixMatch
Extension Ref:
Group: traefik.io
Kind: Middleware
Name: todo-backend-auth
Type: ExtensionRef
Matches:
Path:
Type: PathPrefix
Value: /backend/v2
[...]Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-GATEWAY
Verification
Well, but does it actually work as intended? Let’s find out by first verifying TLS termination:
curl --verbose --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/ | head -n 20
[...]
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
* subject: CN=todo.<your_namespace>.<your_ingress_ip>.nip.io
* start date: Feb 16 08:46:33 2026 GMT
* expire date: Feb 16 08:46:33 2027 GMT
* issuer: CN=todo.<your_namespace>.<your_ingress_ip>.nip.io
* SSL certificate verify result: self-signed certificate (18), continuing anyway.
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
[...]
<!DOCTYPE HTML>
<html>
<head>
<title>Schönste aller Todo Listen</title>
[...]Then let’s verify the redirect HTTP->HTTPS is present:
curl --verbose http://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/
[...]
< HTTP/1.1 301 Moved Permanently
< Location: https://todo.<your_namespace>.<your_ingress_ip>.nip.io/
[...]
Moved PermanentlyAnd confirm the contents on redirect:
curl --silent --location --insecure http://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/ | head -n 4
<!DOCTYPE HTML>
<html>
<head>
<title>Schönste aller Todo Listen</title>So, TLS termination, including a redirect HTTP->HTTPS, seems to work just fine.
Tip
More details / checks wanted? Run
docker run --rm -it drwetter/testssl.sh:latest https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/
and enjoy.
Now let’s check basic auth on backend:
curl --verbose --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/; echo
[...]
< HTTP/2 401
< content-type: text/plain
< content-length: 17
< www-authenticate: Basic realm="traefik"
< date: Mon, 16 Feb 2026 14:29:33 GMT
<
401 Unauthorized
[...]OK, we indeed need to authenticate, so let’s try this while inserting sample data (if none already present):
curl --request POST --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/testabc --user backenddebugger:password; echo
added testabcAnd query data from backend:
curl --silent --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/ --user backenddebugger:password; echo
["testabc"]All in all, now looking back at what we attempted to achieve we can see that we have indeed reached out goal and are finished:
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:80 -> todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443 -> <ingress_ip_traefik>:443 -> / todoui:8090
/backend/v2/ todobackend:8080 w/ basic auth (for debugging)The redirect HTTP->HTTPS is in place, TLS termination works and we can access our ToDo frontend just fine, and the backend is secured by basic auth.