graph TD
sharedGWC[Shared GatewayClass]-->|linked to| shared_EG_controller
shared_EG_controller-->|provisions| shared_EP
tenantAGWC[Tenant A GatewayClass]-->|linked to| tenant_A_EG_controller
tenant_A_EG_controller-->|provisions| tenant_A_EP
shared_EP-->|Tenant B HTTPRoute &\n Shared Gateway Listener with \nallowedRoute from Tenant B ns| Backend_B
shared_EP-->|Tenant C HTTPRoute &\n Shared Gateway Listener with \nallowedRoute from Tenant C ns| Backend_C
tenant_A_EP-->|HTTPRoute| Backend_A
tenant_A_EP-->|Tenant A HTTPRoute & \nTenant D ReferenceGrant| Backend_D
subgraph tenant-a-ns                        
tenant_A_EG_controller[Tenant A EG Controller]
tenant_A_EP[Tenant A Envoy Proxy]
Backend_A[Tenant A Backend]
end
subgraph tenant-b-ns                        
Backend_B[Tenant B Backend]
end
subgraph tenant-c-ns                        
Backend_C[Tenant C Backend]
end
subgraph tenant-d-ns
Backend_D[Tenant D Backend]
end
subgraph shared-ns                        
shared_EG_controller[Shared EG Controller]
shared_EP[Shared Envoy Proxy]
end
Spin up a kind cluster:
make cluster-up
The Envoy Gateway project recommends a multi-tenancy model whereby each tenant deploys their own Envoy Gateway controller in a namespace which they own. We will also explore the implications and risks associated with multiple tenants using a shared controller. As such, we will create two Envoy Gateway controllers - one dedicated controller for Tenant A, and one shared controller for Tenants B and C. To create these controllers, run the following (using Envoy Gateway v0.5.0):
make tenant-a-controller-install
make shared-controller-install
Each Envoy Gateway controller can accept a single GatewayClass resource, so let's create a Tenant A and a shared GatewayClass:
make gwc-tenant-a
make gwc-shared
Create the shared Gateway in the shared
namespace:
make shared-gw-create
Note that the shared Gateway defines a listener which allows routes from the tenant-b
and tenant-c
namespaces to attach to it, as per our example architecture:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: eg
namespace: shared
spec:
gatewayClassName: eg-shared
listeners:
- name: http
protocol: HTTP
port: 8080
allowedRoutes:
namespaces:
from: Selector
selector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- tenant-b
- tenant-c
kubernetes.io/metadata.name
label (which is automatically set to the name of the namespace), we used a custom label. In this case, a malicious internal actor with the ability to label namespaces would be able to change the set of namespaces supported by the Gateway. With some social engineering, users of a legitimate service which uses the shared gateway could possibly be tricked into sending traffic to a malicious backend. We can look at an example of this once we've set up our backend services for all the tenants.
Create backend services and deployments for Tenants A, B, C and D, noting that we will also create a dedicated Gateway for Tenant A:
make run-backend-services
Note the creation of HTTPRoute
s in each of Tenant A, B and C's namespaces. For Tenant A, the parentRef
is its own Gateway:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: backend
namespace: tenant-a
spec:
parentRefs:
- name: eg
hostnames:
- "www.tenant-a.example.com"
rules:
- backendRefs:
- group: ""
kind: Service
name: backend
port: 3000
weight: 1
matches:
- path:
type: PathPrefix
value: /
For Tenants B and C, the parentRef
refers to the shared Gateway:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: backend
namespace: tenant-c
spec:
parentRefs:
- name: eg
namespace: shared
hostnames:
- "www.tenant-c.example.com"
rules:
- backendRefs:
- group: ""
kind: Service
name: backend
port: 3000
weight: 1
matches:
- path:
type: PathPrefix
value: /
For now, we have not set up a route to Tenant D's backend.
Set up port forwarding to the Tenant A Gateway:
make port-forward-tenant-a
Curl the service:
make curl-tenant-a
Set up port forwarding to the Shared Gateway:
make port-forward-shared-tenants
Curl the Tenant B service:
make curl-tenant-b
Curl the Tenant C service:
make curl-tenant-c
tenant-c
namespace:
make malicious-httproute
Now let's imagine that this threat actor can trick a user of Tenant B's service to visit www.tenant-b.example.com/totally-legit
:
make curl-tenant-b-malicious
Note that the response is served by the backend pod in "namespace": "tenant-c"
So far, we have not set up a route to Tenant D's backend Service. Note that until now, all HTTPRoute
s have been created in the same namespaces as the backend which they route to.
Imagine someone with the permission to create HTTPRoute
s in the tenant-a
namespace (as in cross-ns-route.yaml) tries to create a route to Tenant D's backend service:
make cross-ns-route-create
If we try to access Tenant D's Service via Tenant A's Gateway, we will see an error:
make curl-tenant-d
In accordance with Gateway API security principles, for such a cross-namespace connection to succeed, a handshake has to occur between namespaces. In this case, as the HTTPRoute
was created in the tenant-a
namespace, a ReferenceGrant would need to exist in the tenant-d
namespace. Let's demonstrate this:
make ref-grant-create
Try accessing the Tenant D Service again, and observe that it now works:
make curl-tenant-d
tenant-d
namespace wanted to set up an HTTPRoute to a malicious backend, they would need to compromise a service account or set of user credentials which would allow them to create an HTTPRoute in the tenant-a
namespace.
First, we will build an image and load it into our kind cluster. For this demo, we are simply going to install tcpdump on top of an Envoy Proxy base image, and copy in the tcpdump.sh
script, which we can run from within the container to demonstrate the threat:
make build-malicious-envoy
Envoy Gateway allows you to customise the Envoy Proxy image as per the documentation here. This can be used to ensure that your image complies with any mandated security standards, but in this case, we are going to use our toy maliciously crafted image.
Let's deploy the Tenant E infrastructure, including the new GatewayClass and EnvoyProxy:
make tenant-e-infra-install
Run the tcpdump.sh
script from within the Envoy Proxy container:
make exec-into-proxy-e
Open a new terminal window, set up port forwarding and curl the Tenant E service:
make port-forward-tenant-e
make curl-tenant-e
Switch back to the original terminal window and observe the request that we just sent being captured. An attacker with access to (or malicious code running inside) a proxy may want to send captured traffic to an attacker controlled service, or tamper with traffic in transit.
⚠️ carry out a supply chain attack resulting in a malicious image being run⚠️ exploit known vulnerabilities in an unpatched Envoy Proxy⚠️ exploit weaknesses in the Kubernetes cluster to move laterally from a compromised pod to an Envoy Pod
Note that in our example, we have not used Envoy Gateway's namespaced mode which will be available in v0.6.0, so each Envoy Gateway has Kubernetes permissions defined by a ClusterRole, and watches for resources in all namespaces as detailed here. Let's imagine that Tenant A has been compromised, and the attacker wants to modify the Shared Envoy Proxy. Firstly we can look at the Envoy Gateway ClusterRole used by Tenant A's Gateway:
kubectl describe clusterrole eg-tenant-a-gateway-helm-envoy-gateway-role | grep envoyproxies
Note that we cannot patch existing envoy proxies, so by using Tenant A's Envoy Gateway Service Account, our attacker will not be able to modify the EnvoyProxy referred to by the Shared GatewayClass. However, note that we can patch GatewayClasses:
kubectl describe clusterrole eg-tenant-a-gateway-helm-envoy-gateway-role | grep gatewayclasses
Let's use the ./scripts/perform-action-as-gateway.sh
script to see if we can patch the Shared GatewayClass to use a malicious EnvoyProxy in the tenant-a
namespace using the Envoy Gateway's Service Account. Firstly we will create the EnvoyProxy (note that this resource cannot be created using the EG Service Account):
make create-malicious-proxy
Now we will patch the Shared GatewayClass using the Tenant A Gateway Service Account:
make patch-gatewayclass
Let's force the Envoy Gateway and Proxy pods to restart:
make restart-shared-pods
Observe that the Envoy image has not changed!
make grep-shared-envoy-image
This is by design in Gateway API! As per the API specification: "a Gateway is based on the state of the GatewayClass at the time it was created and changes to the GatewayClass or associated parameters are not propagated down to existing Gateways. This recommendation is intended to limit the blast radius of changes to GatewayClass or associated parameters."
./scripts/perform-action-as-gateway.sh kubectl describe secrets -A
The minimal base infra required to run the demos for the talk can be set up using:
make demo-base-infra
The demo-magic
scripts demo-1.sh, demo-2.sh, and demo-3.sh can then be run in order.
If you are still port warding connections, you can stop by running:
make stop-port-forwarding
To delete the kind cluster, run:
make cluster-down