CKS — Egress Policies

7 minute read

Controlling Outbound Traffic

Egress policies control what destinations a pod can send traffic to. Without egress policies, a compromised pod can:

  • Exfiltrate data to any external IP
  • Connect to cryptocurrency mining pools
  • Reach cloud metadata APIs and steal credentials
  • Communicate with command-and-control servers

Egress control is a core CKS requirement. The exam expects you to write egress policies that restrict pods to only the destinations they need.


Allowing Only DNS Egress

After applying default deny egress, the first policy you write is always the DNS allow. This is the foundation for all egress control.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Why Both UDP and TCP?

Protocol When Used
UDP 53 Standard DNS queries (most traffic)
TCP 53 Large responses (>512 bytes), DNSSEC, zone transfers

Always allow both. If you only allow UDP and a DNS response is truncated, the pod retries over TCP — and that retry fails silently.


Restricting Egress to Specific External IPs

Allow pods to reach only specific external services:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-external-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment-service
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.50/32
      ports:
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

What This Does

  • Selects: only pods labeled app: payment-service
  • Allows egress: to 203.0.113.50 on HTTPS (443) — a specific payment gateway
  • Allows egress: to CoreDNS for name resolution
  • All other egress: denied (by the default deny policy)

The /32 CIDR means a single IP address.


Egress to Specific Internal Services

Control which internal services a pod can reach using podSelector and namespaceSelector:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-backend
  namespace: frontend
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: backend
          podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 8080
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This allows the web pods in frontend to reach only the api pods in the backend namespace on port 8080. They cannot reach the database, other backend services, or any external IP.


Egress to a Database

A common pattern — backend services need to reach a database:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-postgres
  namespace: backend
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: database
          podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

The api pods can reach PostgreSQL on port 5432, and DNS. Nothing else.


Combining Ingress and Egress in One Policy

You can define both ingress and egress rules in a single NetworkPolicy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
  namespace: backend
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: frontend
          podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: database
          podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

What This Does

The api pods in backend:

  • Accept ingress from: web pods in frontend namespace, on port 8080
  • Can send egress to: postgres pods in database namespace, on port 5432
  • Can send egress to: CoreDNS on port 53
  • All other traffic: denied in both directions

This is a complete least-privilege network policy for the API service.


Egress to Specific CIDRs

Allow egress to an entire CIDR range — useful for corporate networks:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-corporate
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 10.0.0.0/8
      ports:
        - protocol: TCP
          port: 443
        - protocol: TCP
          port: 80
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This allows all pods in production to reach the 10.0.0.0/8 network on HTTP and HTTPS, plus DNS. All other egress is denied.


The Complete Egress Lockdown Pattern

Here is the recommended approach for a fully locked-down namespace:

Step 1: Default Deny All

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Step 2: Allow DNS

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Step 3: Per-Service Egress Policies

For each service, define exactly what it can reach:

# Web service can reach the API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 8080
---
# API service can reach the database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

Egress Rules — How They Combine

NetworkPolicies are additive. If multiple policies select the same pod, the union of all their egress rules applies:

Policy Allows Egress To
allow-dns CoreDNS on port 53
web-egress api pods on port 8080
Combined effect CoreDNS on 53 OR api pods on 8080

Within a single policy, multiple egress rules are also OR’d — traffic is allowed if it matches any rule.

Within a single rule, to entries and ports entries are AND’d:

egress:
  - to:          # traffic must match a 'to' entry
      - ipBlock:
          cidr: 10.0.0.0/8
    ports:        # AND match a 'ports' entry
      - protocol: TCP
        port: 443

This allows TCP 443 to 10.0.0.0/8. Not TCP 443 everywhere, and not all ports to 10.0.0.0/8.


Security Impact of Egress Policies

Without Egress Control With Egress Control
Pods can reach any IP on the internet Pods can only reach approved destinations
Data exfiltration to any server Exfiltration requires reaching approved IPs
Cloud metadata accessible Metadata endpoint blocked
C2 communication trivial C2 servers unreachable
Crypto miners can phone home Mining pool connections blocked

Practice Questions

Question 1: Write an egress policy that allows pods labeled `app: worker` in the `jobs` namespace to reach only `10.0.5.0/24` on port 3306 (MySQL) and DNS.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: worker-egress
  namespace: jobs
spec:
  podSelector:
    matchLabels:
      app: worker
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 10.0.5.0/24
      ports:
        - protocol: TCP
          port: 3306
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

The first egress rule allows MySQL traffic to the 10.0.5.0/24 subnet. The second allows DNS. All other egress is denied by the default deny policy.

Question 2: A pod has both a default-deny-egress policy and an allow-dns policy applied. Can the pod reach a service in the same namespace on port 8080?

No. The default deny blocks all egress. The allow-DNS policy only permits egress to port 53 on CoreDNS pods. There is no rule allowing egress to port 8080.

To reach the service, you need an additional egress rule:

egress:
  - to:
      - podSelector:
          matchLabels:
            app: target-service
    ports:
      - protocol: TCP
        port: 8080
Question 3: What is the difference between omitting the `ports` field in an egress rule vs specifying it?
  • Omitting ports: allows egress to the matching destinations on all ports and protocols
  • Specifying ports: allows egress only on the specified ports and protocols

Example without ports:

egress:
  - to:
      - podSelector:
          matchLabels:
            app: api

This allows traffic to api pods on any port — 80, 443, 8080, 22, etc. For least-privilege, always specify the port.

Question 4: Write a single NetworkPolicy for the `api` pod that allows ingress from `web` pods on port 8080 and egress to `postgres` pods on port 5432, plus DNS.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Both Ingress and Egress are listed in policyTypes, so this policy governs both directions for the api pod.

Question 5: You have default deny egress in a namespace. You create an egress policy allowing traffic to `10.0.0.0/8`. Can pods reach `10.0.5.10` on port 22?

Yes. The egress rule allows traffic to 10.0.0.0/8 which includes 10.0.5.10. Since no ports restriction is specified in the question’s rule, all ports are allowed — including port 22.

If you want to restrict to specific ports, add a ports section:

egress:
  - to:
      - ipBlock:
          cidr: 10.0.0.0/8
    ports:
      - protocol: TCP
        port: 443

This would allow only HTTPS, not SSH.

Updated: