Back to blog

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

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

HyperText Transfer Protocol (HTTP) might have been created 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). For the past few 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 (redirect, rewrite, and mirror) as it enters Kubernetes clusters with Cilium’s Gateway API, 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, the Gateway API’s role is to modify and control traffic as it enters our clusters.

In the previous “Getting Started with the Cilium Gateway API tutorial,” we explored 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 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 start with the first use case—redirecting HTTP traffic—let’s prepare our environment. This tutorial will use an Azure Kubernetes Service (AKS) cluster in BYOCNI mode and an Elastic Kubernetes Service (EKS) cluster in ENI 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.

Create an AKS cluster with BYOCNI and fetch the credentials once the cluster is up and running.

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}"

Truncated O/P:
===========

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

Note that your nodes won't show as Ready for now - there's no CNI, so connectivity is limited.

kubectl get nodes

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

Truncated O/P:
===========

When using kubectl, you might even see 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 before installing Ciliium."

Let’s 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 so already) for both the EKS and AKS clusters:

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.

You can use this new option instead of skipping 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 HTTPRoutes 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 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 ensure that each Gateway API provides a consistent user experience, each 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 received an IP address. Remember that the IP address allocation is done automatically for you in public clouds. In contrast, you would need to use MetalLB or Cilium’s own LoadBalancer IP Address Management feature on a private cloud 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 sending HTTP redirects to clients to tell them that the resource they are trying to access in the cluster has moved to a different location.
This is a 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 shown 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 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 with /original-prefix to /replacement-prefix. Note that we support different models that 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 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 redirecting the client to a different URL. Instead, we can 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 packet’s body.

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 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 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. It has plenty of use cases – 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 infra-backend-v1 as standard but copy it across to infra-backend-v2 (purple arrow in the image below). The response infra-backend-v1 will be processed normally by the Gateway, but the responses infra-backend-v2 will be ignored (red arrow).

Let’s request 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 Let’s use the Kubernetes debug functionality instead, with an image (nicolaka/netshoot) 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

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:

Delete the AKS Cluster:

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

Delete the EKS Cluster:

eksctl delete cluster cluster2 --region ap-northeast-1

Recap

Cilium might be famously known for being a high-performance CNI and its network policy engine, but it is equally capable of performing L7 load-balancing. Unlike my days as a network engineer, when 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. This is another powerful addition to all the other Layer 7 Load Balancing features that Cilium Gateway API already supports.

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

Learn More

Nico Vibert
AuthorNico VibertSenior Staff Technical Marketing Engineer
Amit Gupta
AuthorAmit GuptaSenior Technical Marketing Engineer

Related

Blogs

A Deep Dive into Cilium Gateway API: The Future of Ingress Traffic Routing

In this blog post, learn what the Cilium Gateway API is and how the Gateway API project came to be and the issues it solves.

By
Nico VibertSachin Jha
Blogs

Cilium 1.13 – Gateway API, mTLS datapath, Service Mesh, BIG TCP, SBOM, SNI NetworkPolicy, …

Announcing Cilium 1.13 - Gateway API, mTLS datapath, Service Mesh, BIG TCP, SBOM, SNI NetworkPolicy - and many more features!

By
Thomas Graf
By
Thomas Graf

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