Managing egress traffic
In this guide, we’ll walk you through an example of egress traffic management: visualizing, applying policies and implementing advanced routing configuration for traffic that is targeted to destinations that reside outside of the cluster.
Warning
Visualizing egress traffic
In order to be able to capture egress traffic and apply policies to it we will
make use of the EgressNetwork
CRD. This CRD is namespace scoped - it applies
to clients in the local namespace unless it is created in the globally
configured egress namespace. For now, let’s create an egress-test
namespace
and add a single EgressNetwork
to it.
kubectl create ns egress-test
kubectl apply -f - <<EOF
apiVersion: policy.linkerd.io/v1alpha1
kind: EgressNetwork
metadata:
namespace: egress-test
name: all-egress-traffic
spec:
trafficPolicy: Allow
EOF
This is enough to visualize egress traffic going through the system. In order to do so, you can deploy a simple curl container and start hitting an external to the cluster service:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: egress-test
annotations:
linkerd.io/inject: enabled
spec:
containers:
- name: client
image: curlimages/curl
command:
- "sh"
- "-c"
- "sleep infinity"
EOF
Now SSH into the client container and start generating some external traffic:
kubectl -n egress-test exec -it client-xxx -c client -- sh
$ while sleep 1; do curl -s http://httpbin.org/get ; done
In a separate shell, you can use the Linkerd diagnostics command to visualize the traffic.
linkerd dg proxy-metrics -n egress-test po/client | grep outbound_http_route_request_statuses_total
outbound_http_route_request_statuses_total{
parent_group="policy.linkerd.io",
parent_kind="EgressNetwork",
parent_namespace="egress-test",
parent_name="all-egress-traffic",
parent_port="80",
parent_section_name="",
route_group="",
route_kind="default",
route_namespace="",
route_name="http-egress-allow",
hostname="httpbin.org",
http_status="200",
error=""
} 697
Notice that these raw metrics allow you to quickly identify egress traffic targeted
towards different destinations simply by querying for parent_kind
of type
EgressNetwork
. For now all traffic is allowed and we are simply observing it.
We can also observe that because our EgressNetwork
default traffic policy is
set to Allow
, the default http route is named as http-egress-allow
.
This is a placeholder route that is being populated automatically by the
Linkerd controller.
Restricting egress traffic
After you have used metrics in order to compose a picture of your egress
traffic, you can start applying policies that allow only some of it to go
through. Let’s update our EgressNetwork
and change its trafficPolicy
to Deny
:
kubectl patch egressnetwork -n egress-test all-egress-traffic \
-p '{"spec":{"trafficPolicy": "Deny"}}' --type=merge
Now, you should start observing failed requests from your client container. Furthermore, looking at metrics we can observe the following result:
outbound_http_route_request_statuses_total{
parent_group="policy.linkerd.io",
parent_kind="EgressNetwork",
parent_namespace="egress-test",
parent_name="all-egress-traffic",
parent_port="80",
parent_section_name="",
route_group="",
route_kind="default",
route_namespace="",
route_name="http-egress-deny",
hostname="httpbin.org",
http_status="403",
error=""
} 45
We can clearly observe now that the traffic targets the same parent but the
name of the route is now http-egress-deny
. Furthermore, the http_status
is
403
or Forbidden
. By changing the traffic policy to Deny
, we have forbidden
all egress traffic originating from the local namespace. In order to allow some
of it, we can make use of the Gateway API types. Assume that you want to
allow traffic to httpbin.org
but only for requests that target the /get
endpoint. For that purpose we need to create the following HTTPRoute
:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: httpbin-get
namespace: egress-test
spec:
parentRefs:
- name: all-egress-traffic
kind: EgressNetwork
group: policy.linkerd.io
namespace: egress-test
port: 80
rules:
- matches:
- path:
value: "/get"
EOF
We can see that traffic is now flowing again and if we look at metrics we will
be able to see that this happens through the httpbin-get
route.
outbound_http_route_request_statuses_total{
parent_group="policy.linkerd.io",
parent_kind="EgressNetwork",
parent_namespace="egress-test",
parent_name="all-egress-traffic",
parent_port="80",
parent_section_name="",
route_group="gateway.networking.k8s.io",
route_kind="HTTPRoute",
route_namespace="egress-test",
route_name="httpbin-get",
hostname="httpbin.org",
http_status="200",
error=""
} 63
Interestingly enough though, if we go back to our client shell and we try to initiate HTTPS traffic to the same service, it will not be allowed:
~ $ curl -v https://httpbin.org/get
curl: (35) TLS connect error: error:00000000:lib(0)::reason(0)
This is the case because our current configuration only allows plaintext HTTP
traffic to go through the system. We can additionally allow HTTPS traffic, by
using the Gateway API TLSRoute
:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: tls-egress
namespace: egress-test
spec:
hostnames:
- httpbin.org
parentRefs:
- name: all-egress-traffic
kind: EgressNetwork
group: policy.linkerd.io
namespace: egress-test
port: 443
rules:
- backendRefs:
- kind: EgressNetwork
group: policy.linkerd.io
name: all-egress-traffic
EOF
This fixes the problem and we can see HTTPS requests to the external service succeeding reflected in the metrics:
linkerd dg proxy-metrics -n egress-test po/client | grep outbound_tls_route_open_total
outbound_tls_route_open_total{
parent_group="policy.linkerd.io",
parent_kind="EgressNetwork",
parent_namespace="egress-test",
parent_name="all-egress-traffic",
parent_port="443",
parent_section_name="",
route_group="gateway.networking.k8s.io",
route_kind="TLSRoute",
route_namespace="egress-test",
route_name="tls-egress",
hostname="httpbin.org"
} 2
This configuration allows traffic to httpbin.org
only. In order to apply
policy decisions for TLS connections, the proxy parses the SNI extension
header from the ClientHello
of the TLS session and uses that as the target
hostname identifier. This means that if we try to initiate a request to
github.com
from our client, we will see the proxy eagerly closing the
connection because it is not forbidden by our current policy configuration:
linkerd dg proxy-metrics -n egress-test po/client | grep outbound_tls_route_close_total
outbound_tls_route_close_total{
parent_group="policy.linkerd.io",
parent_kind="EgressNetwork",
parent_namespace="egress-test",
parent_name="all-egress-traffic",
parent_port="443",
parent_section_name="",
route_group="",
route_kind="default",
route_namespace="",
route_name="tls-egress-deny",
hostname="github.com",
error="forbidden"
} 1
In a similar fashion we can use the other Gateway API route types such as GRPCRoute
and TCPRoute to shape traffic that is captured by an EgressNetwork
primitive.
All these traffic types come with their corresponding set of route-based
metrics that describe how traffic flows through the system and what policy
decisions have been made.
Redirecting egress traffic back to the cluster
Using the Gateway API route types to model egress traffic allows us to implement some more advanced routing configurations. Assume that we want to apply the following rules:
- unencrypted HTTP traffic can only target
httpbin.org/get
an no other endpoints - encrypted HTTPs traffic is allowed to all destinations
- all other unencrypted HTTP traffic need to be redirected to an internal service
To begin with, let’s create our internal service to which traffic should be redirected:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: internal-egress
namespace: egress-test
spec:
type: ClusterIP
selector:
app: internal-egress
ports:
- port: 80
protocol: TCP
name: one
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: egress-test
name: internal-egress
spec:
replicas: 1
selector:
matchLabels:
app: internal-egress
template:
metadata:
labels:
app: internal-egress
annotations:
linkerd.io/inject: enabled
spec:
containers:
- name: legacy-app
image: buoyantio/bb:v0.0.5
command: [ "sh", "-c"]
args:
- "/out/bb terminus --h1-server-port 80 --response-text 'You cannot go there right now' --fire-and-forget"
ports:
- name: http-port
containerPort: 80
EOF
In order to allow the first rule, we need to create an HTTPRoute
that looks
like this:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: httpbin-get
namespace: egress-test
spec:
parentRefs:
- name: all-egress-traffic
kind: EgressNetwork
group: policy.linkerd.io
namespace: egress-test
port: 80
rules:
- matches:
- path:
value: "/get"
EOF
To allow all tls traffic, we need the following TLSRoute
:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: tls-egress
namespace: egress-test
spec:
parentRefs:
- name: all-egress-traffic
kind: EgressNetwork
group: policy.linkerd.io
namespace: egress-test
port: 443
rules:
- backendRefs:
- kind: EgressNetwork
group: policy.linkerd.io
name: all-egress-traffic
EOF
Finally to redirect the rest of the plaintext HTTP traffic to the internal
service, we create an HTTPRoute
with a custom backend being the internal service:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: unencrypted-http
namespace: egress-test
spec:
parentRefs:
- name: all-egress-traffic
kind: EgressNetwork
group: policy.linkerd.io
namespace: egress-test
port: 80
rules:
- backendRefs:
- kind: Service
name: internal-egress
port: 80
EOF
Now let’s verify all works as expected:
# plaintext traffic goes as expected to the /get path
$ curl http://httpbin.org/get
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/8.11.0",
"X-Amzn-Trace-Id": "Root=1-674599d4-77a473943844e9e31844b48e"
},
"origin": "51.116.126.217",
"url": "http://httpbin.org/get"
}
# encrypted traffic can target all paths and hosts
$ curl https://httpbin.org/ip
{
"origin": "51.116.126.217"
}
# arbitrary unencrypted traffic goes to the internal service
$ curl http://google.com
{
"requestUID": "in:http-sid:terminus-grpc:-1-h1:80-190120723",
"payload": "You cannot go there right now"}
}
Cleanup
In order to clean everything up, simply delete the namespace:
kubectl delete ns egress-test
.