• GitHub
  • Slack
  • Linkerd Forum

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

No service mesh can provide a strong security guarantee about egress traffic by itself; for example, a malicious actor could bypass the Linkerd sidecar - and thus Linkerd’s egress controls - entirely. Fully restricting egress traffic in the presence of arbitrary applications thus typically requires a more comprehensive approach.

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.