Back to blog

Tutorial: Redirect, Rewrite and Mirror HTTP with Cilium Gateway API

Nico Vibert
Nico Vibert
Published: Updated: Cilium
Tutorial: Redirect, Rewrite and Mirror HTTP with Cilium Gateway API

HyperText Transfer Protocol (HTTP) might have been created back in 1989 but it has certainly withstood the test of time. It laid the foundation for the World Wide Web and it remains ever popular and the protocol of choice for the vast majority of APIs (despite the more recent alternatives like GraphQL and gRPC). And for the past couple of decades, we’ve used Layer 7-aware load balancers to control, alter and route HTTP traffic as it entered our network.

In this blog post, we are going to explore how you can alter HTTP traffic as it enters Kubernetes clusters, focusing on three specific use cases:

  • HTTP Redirect – to tell the client that the resource they are trying to access has moved
  • HTTP Path Rewrite – to rewrite the URL or entire path used by the client
  • HTTP Mirroring – to copy the traffic sent from the client to another backend

In the world of Kubernetes, it’s the role of the Gateway API to modify and control traffic as it enters our clusters.

We explored in the previous “Getting Started with the Cilium Gateway API tutorial” how you can use Cilium’s Gateway API support to load-balance HTTP traffic and alter HTTP header requests and responses.

We’ve also recorded several short tutorials on the Gateway API, including the one below on “TLS Passthrough”, where traffic can be encrypted all the way through to the server.

In this tutorial, we will explore these use cases. If you’d rather watch me present it, check out this episode of the eBPF and Cilium weekly show eCHO:

If you’d rather do it yourself, follow the steps below. Let’s start!

Demo environment

Before we get starting with the first use case – redirecting HTTP traffic – let’s get our environment ready.

In this tutorial, we will be using a cluster in Azure Kubernetes Services (AKS) in BYOCNI mode. In this mode, a cluster is deployed without a Container Network Interface (CNI) so that we can bring our own – Cilium in this instance.

To deploy the cluster without the CNI, I am following the docs:

export NAME="$(whoami)-$RANDOM"

export AZURE_RESOURCE_GROUP="${NAME}-group"

az group create --name "${AZURE_RESOURCE_GROUP}" -l westus2

az aks create \
  --resource-group "${AZURE_RESOURCE_GROUP}" \
  --name "${NAME}" \
  --network-plugin none

az aks get-credentials --resource-group "${AZURE_RESOURCE_GROUP}" --name "${NAME}"

Expect a lengthy JSON output, beginning with this.

JSON:
{
  "id": "/subscriptions/subscription-id/resourceGroups/nicovibert-4665-group",
  "location": "westus2",
  "managedBy": null,
  "name": "nicovibert-4665-group",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": "Microsoft.Resources/resourceGroups"
}

My cluster is called nicovibert-4665 in this instance. Note that your nodes won’t show as Ready for now – there’s no CNI and therefore connectivity is limited.

kubectl get nodes

Expect an output such as:

NAME                                STATUS     ROLES   AGE     VERSION
aks-nodepool1-35842206-vmss000000   NotReady   agent   8m21s   v1.26.6
aks-nodepool1-35842206-vmss000001   NotReady   agent   8m8s    v1.26.6
aks-nodepool1-35842206-vmss000002   NotReady   agent   8m18s   v1.26.6

When using kubectl, you might even see some error logs such as couldn't get resource list for metrics.k8s.io/v1beta1: the server is currently unable to handle the request which is just because of the lack of connectivity prior to installing Ciliium.

Let’s now use the latest Gateway API Custom Resource Definitions (CRDs). Remember to install them before installing Cilium:

kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_gateways.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml

Support for the features we are exploring today – and for the 1.0 Gateway API version – was recently added to the main branch of Cilium – to use them, you will need Cilium 1.15.

Use this Cilium CLI command (install the CLI with the instructions here if you’ve not done already):

cilium install --version 1.15.0-pre.0 --namespace kube-system  --set kubeProxyReplacement=true --set gatewayAPI.enabled=true --set azure.resourceGroup="${AZURE_RESOURCE_GROUP}"

Expect this output:

Auto-detected Kubernetes kind: AKS
ℹ️  Using Cilium version 1.15.0-pre.0
Auto-detected cluster name: nicovibert-4665
✅ Derived Azure subscription ID subscription-id from subscription cilium-dev
✅ Detected Azure AKS cluster in BYOCNI mode (no CNI plugin pre-installed)
 Auto-detected kube-proxy has been installed

Remember that the kubeProxyReplacement feature (KPR) is required for the Cilium Gateway API. Note that, even when creating the cluster in BYOCNI mode, kubeProxy was deployed in the cluster.

I could have used this new option instead to skip its deployment as it is no longer needed once Cilium is deployed in KPR mode.

After installation, check the Cilium status with:

cilium status --wait

Expect the status to be:

    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    disabled (using embedded mode)
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet              cilium             Desired: 3, Ready: 3/3, Available: 3/3
Containers:            cilium             Running: 3
                       cilium-operator    Running: 1
Cluster Pods:          5/5 managed by Cilium
Helm chart version:    1.15.0-pre.0
Image versions         cilium             quay.io/cilium/cilium:v1.15.0-pre.0: 3
                       cilium-operator    quay.io/cilium/operator-generic:v1.15.0-pre.0: 1

The status for both Cilium and Operator should be OK. Now that our environment is ready, let’s go ahead and deploy our demo app and our Gateway before deploying the HTTRoutes to illustrate the use cases we will cover in this blog post.

We will use this YAML manifest I have put on a GitHub repo.

This configuration is actually a subset of configs used in the Gateway API conformance tests. Remember that one of the benefits of the Gateway API project is consistency. To make sure that each Gateway API provides a consistent user experience, each Gateway API implementation is tested against a set of conformance tests that creates a series of Gateways and Routes and tests that the implementation matches the API specification.

Deploy this demo app:

kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-samples/main/Rewrite%2C%20Redirect%2C%20Mirror/demo-app.yaml

Expect this output:

service/infra-backend-v1 created
deployment.apps/infra-backend-v1 created
service/infra-backend-v2 created
deployment.apps/infra-backend-v2 created

We are re-using the same Gateway configuration we used in the previous blog post. It’s a simple Gateway configuration (it’s simply listening for HTTP traffic):

cat << 'EOF' > gateway.yaml
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: cilium-gw
spec:
  gatewayClassName: cilium
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Same
EOF

kubectl apply -f gateway.yaml

Expect this output:

gateway.gateway.networking.k8s.io/cilium-gw created

Let’s double check that the Gateway has been installed and it has received an IP address. Remember, that in public clouds, the IP address allocation is done automatically for you whereas on private cloud, you would need to use MetalLB or Cilium’s own LoadBalancer IP Address Management feature to assign this IP.

kubectl get gateway 

Expect an outcome, such as:

NAME        CLASS    ADDRESS          PROGRAMMED   AGE
cilium-gw   cilium   20.115.194.177   True         85s

Let’s save this IP as the $GATEWAY variable.

GATEWAY=$(kubectl get gateway cilium-gw -o jsonpath='{.status.addresses[0].value}')
echo $GATEWAY

HTTP Redirect

One common use case for ingress gateways is to send HTTP redirects to clients in order to tell them that the resource they are trying to access in the cluster has moved to a different location.
This is a really common requirement for migration, content optimization, SSL/TLS enforcement – the list goes on.

Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. With the Gateway API, we can use redirect filters to substitute various URL components independently, as you will see below.

Let’s use this HTTPRoute YAML for the four examples in this section. We will review each rule in detail below.

kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-samples/main/Rewrite%2C%20Redirect%2C%20Mirror/http-redirect-route.yaml

Path Redirect

In this first example, we will do a simple redirect: we only replace a portion of the URL and redirect them there:

You should see the IP address allocated to the Gateway (such as 20.115.194.177).

The following rule will match traffic to /original-prefix and redirect the client to a different URL:

- matches:
    - path:
        type: PathPrefix
        value: /original-prefix
    filters:
    - type: RequestRedirect
      requestRedirect:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /replacement-prefix

Let’s try, using curl.

curl -l -v http://$GATEWAY/original-prefix

Notice we use -l in the curl request to follow the redirects (by default, curl will not follow redirects) and that we use the verbose option of curl to see the response headers.

*   Trying 20.115.194.177:80...
* Connected to 20.115.194.177 (20.115.194.177) port 80 (#0)
> GET /original-prefix HTTP/1.1
> Host: 20.115.194.177
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 302 Found
< location: http://20.115.194.177:80/replacement-prefix
< date: Tue, 12 Sep 2023 08:51:52 GMT
< server: envoy
< content-length: 0
< 
* Connection #0 to host 20.115.194.177 left intact

The location is used in Redirect messages to tell the client where to go. As you can see, the client is redirected to http://20.115.194.177:80/replacement-prefix. The prefix was replaced from /original-prefix to /replacement-prefix. Note we support different models to replace the URL – we can replace just a portion of the path or the entire one.

Host & Path redirects

You can also redirect the client to a different host.

Let’s try, with this specification, to redirect the client to example.org:

  - matches:
    - path:
        type: PathPrefix
        value: /path-and-host
    filters:
    - type: RequestRedirect
      requestRedirect:
        hostname: example.org
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /replacement-prefix

Let’s make HTTP requests to that external address and path:

curl -l -v http://$GATEWAY/path-and-host

Expect an output such as:

*   Trying 20.115.194.177:80...
* Connected to 20.115.194.177 (20.115.194.177) port 80 (#0)
> GET /path-and-host HTTP/1.1
> Host: 20.115.194.177
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 302 Found
< location: http://example.org:80/replacement-prefix
< date: Tue, 12 Sep 2023 08:52:26 GMT
< server: envoy
< content-length: 0
< 
* Connection #0 to host 20.115.194.177 left intact

As you can see, the client is redirected to http://example.org:80/replacement-prefix.

Both the hostname and the path prefix were modified.

Redirect to new prefix and custom status code

Next, you can also modify the status code. By default, as you saw above, the redirect status code is 302. It means that the resources have been moved temporarily.

To indicate that the resources the client is trying to access have moved permanently, you can use the status code 301. You can also combine it with the prefix replacement.

Let’s use this example:

  - matches:
    - path:
        type: PathPrefix
        value: /path-and-status
    filters:
    - type: RequestRedirect
      requestRedirect:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /replacement-prefix
        statusCode: 301

Try to access this URL:

curl -l -v http://$GATEWAY/path-and-status

Expect an output such as:

*   Trying 20.115.194.177:80...
* Connected to 20.115.194.177 (20.115.194.177) port 80 (#0)
> GET /path-and-status HTTP/1.1
> Host: 20.115.194.177
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< location: http://20.115.194.177:80/replacement-prefix
< date: Tue, 12 Sep 2023 08:52:33 GMT
< server: envoy
< content-length: 0
< 
* Connection #0 to host 20.115.194.177 left intact

Note the status code returned is 301 Moved Permanently and the client is redirected to http://20.115.194.177:80/replacement-prefix.

Redirect to new prefix and custom status code

Finally, we can also use the Gateway API to impose tighter security controls. You can redirect the client to use HTTPS instead of HTTP; changing the scheme used by the client.

Look at the last line in this specification:

  - matches:
    - path:
        type: PathPrefix
        value: /scheme-and-host
    filters:
    - type: RequestRedirect
      requestRedirect:
        hostname: example.org
        scheme: "https"

Let’s try it.

curl -l -v http://$GATEWAY/scheme-and-host

Expect an output such as:

*   Trying 20.115.194.177:80...
* Connected to 20.115.194.177 (20.115.194.177) port 80 (#0)
> GET /scheme-and-host HTTP/1.1
> Host: 20.115.194.177
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 302 Found
< location: https://example.org:443/scheme-and-host
< date: Tue, 12 Sep 2023 09:09:20 GMT
< server: envoy
< content-length: 0
< 
* Connection #0 to host 20.115.194.177 left intact

As you can see, the client initially tried to connect via HTTP and is redirected to https://example.org:443/scheme-and-host:

HTTP Rewrite

Sometimes, we don’t need to redirect the client to a different URL. Instead, we can simply rewrite components of a client request.

Let’s explore this with a couple of examples. Create the HTTPRoute by applying the manifest below:

kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-samples/main/Rewrite%2C%20Redirect%2C%20Mirror/http-rewrite-route.yaml

The Gateway will replace the /prefix/one in the URL request to /one.

Let’s now check that traffic based on the URL path is proxied and altered by the Gateway API:

curl http://$GATEWAY/prefix/one

The request is received by an echo server that copies the original request and sends the reply back in the body of the packet.

JSON:
{
 "path": "/one",
 "host": "20.115.194.177",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.1.2"
  ],
  "X-Envoy-External-Address": [
   "A.B.C.D"
  ],
  "X-Envoy-Original-Path": [
   "/prefix/one"
  ],
  "X-Forwarded-For": [
   "A.B.C.D"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "7b09721a-2162-45e0-a0a8-c55829f1603a"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "infra-backend-v1-57dc9df649-lwxc6"
}                      

As you can see, the Gateway changed the original request from /prefix/one to /one.

As we use the Envoy proxy for L7 traffic processing, note that Envoy also adds the information about the original path in the packet (see "X-Envoy-Original-Path").

We can also combine this, with previous Gateway API features we explored in the previous tutorial. You might want to rewrite traffic and add some headers to it to add some metadata (so that the receiving server can interpret it accordingly).

If you look at the HTTPRoute we’ve just created, you will see that traffic to /rewrite-path-and-modify-headers will not only be partially rewritten, but that some headers can be added, removed or modified.

curl http://$GATEWAY/prefix/rewrite-path-and-modify-headers

Expect an output such as:

JSON:
{
  "path": "/prefix",
  "host": "20.115.194.177",
  "method": "GET",
  "proto": "HTTP/1.1",
  "headers": {
    "Accept": [
      "*/*"
    ],
    "User-Agent": [
      "curl/8.1.2"
    ],
    "X-Envoy-External-Address": [
      "A.B.C.D"
    ],
    "X-Envoy-Original-Path": [
      "/prefix/rewrite-path-and-modify-headers"
    ],
    "X-Forwarded-For": [
      "A.B.C.D"
    ],
    "X-Forwarded-Proto": [
      "http"
    ],
    "X-Header-Add": [
      "header-val-1"
    ],
    "X-Header-Add-Append": [
      "header-val-2"
    ],
    "X-Header-Set": [
      "set-overwrites-values"
    ],
    "X-Request-Id": [
      "de0055e7-5d91-49fd-98cc-181ed132a08d"
    ]
  },
  "namespace": "default",
  "ingress": "",
  "service": "",
  "pod": "infra-backend-v1-57dc9df649-lwxc6"
}

HTTP Mirroring

In this final example, we will review how to mirror traffic. There are plenty of use cases for it – monitoring incoming traffic for forensics, analysis, logging, troubleshooting, etc…

With the following rule, we can mirror traffic meant for infra-backend-v1 to infra-backend-v2.

---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: request-mirror
spec:
  parentRefs:
  - name: cilium-gw
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /mirror
    filters:
    - type: RequestMirror
      requestMirror:
        backendRef:
          name: infra-backend-v2
          port: 8080
    backendRefs:
    - name: infra-backend-v1
      port: 8080

Apply it:

kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-samples/main/Rewrite%2C%20Redirect%2C%20Mirror/http-mirror-route.yaml

The Gateway will forward the traffic to infra-backend-v1 as standard, but will also copy it across to infra-backend-v2 (purple arrow in the image below). The response from infra-backend-v1 will be processed normally by the Gateway but the responses from infra-backend-v2 will be ignored (red arrow).

Let’s make a request to the /mirror path.

curl http://$GATEWAY/mirror

Look at the output below. Traffic was sent to the infra-backend-v1 Service. Was it also mirrored to infra-backend-v2 ? How can we prove it?

JSON:
{
 "path": "/mirror",
 "host": "20.115.194.177",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.1.2"
  ],
  "X-Envoy-External-Address": [
   "A.B.C.D"
  ],
  "X-Forwarded-For": [
   "A.B.C.D"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "3caacef1-bbcb-4e01-a088-d3d411bd6dc1"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "infra-backend-v1-57dc9df649-xgvkr"
}

The image used by the echo Pods serving this Service is minimal. Instead of trying to install tcpdump on it, let’s use the Kubernetes debug functionality instead, with an image (nicolaka/netshoot) that is designed to troubleshoot networking issues.

The command below will deploy a container in the same Pod as the infra-backend-v2 Pod and will see the traffic coming in:

BACKEND_POD_NAME=$(kubectl get pods --no-headers=true -o custom-columns=":metadata.name" -l app=infra-backend-v2 | head -n 1)

kubectl debug $BACKEND_POD_NAME -it --image=nicolaka/netshoot  -- tcpdump -i eth0

Once you run the curl command again from a different terminal, you should see traffic appearing (you may have to run it on a few occasions as traffic will be load-balanced between multiple infra-backend-v2 Pods):

11:30:15.778113 IP 10.244.2.149.59950 > infra-backend-v2-664bffc4-8hbll.3000: Flags [F.], seq 235, ack 627, win 503, options [nop,nop,TS val 4109474445 ecr 389226307], length 0
11:30:15.778233 IP infra-backend-v2-664bffc4-8hbll.3000 > 10.244.2.149.59950: Flags [F.], seq 627, ack 236, win 501, options [nop,nop,TS val 389286309 ecr 4109474445], length 0

And this shows that the Gateway mirrored traffic to the infra-backend-v2 Service.

Post Demo Clean Up

After testing, you can clean up your demo environment and remove both the cluster and the resource group with the following commands:

az aks delete --resource-group "${AZURE_RESOURCE_GROUP}" --name "${NAME}"
az group delete --name "${AZURE_RESOURCE_GROUP}"

Recap

Cilium might be famously known for being a high-performance CNI and its network policy engine but it’s equally capable of doing L7 load-balancing. Unlike my days as a network engineer where I would have to drive to a data center, lug a load balancer onto the rack, install it and configure it, with Cilium, I don’t have to install anything else – it’s just another feature to enable and a YAML manifest to apply.

In this blog post, you will have learned – and hopefully tried alongside me – how Cilium Gateway API can send HTTP redirects, rewrite URL paths and mirror HTTP traffic. It’s another powerful addition to all the other Layer 7 Load Balancing features that Cilium Gateway API already supported.

Think this might be useful for your applications? Got any use case in mind? Let me know, on the Cilium Slack or you can find me on LinkedIn.

Learn More

Nico Vibert
AuthorNico VibertSenior Staff Technical Marketing Engineer

Industry insights you won’t delete. Delivered to your inbox weekly.