CKS — Egress Policies
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.50on 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:
webpods infrontendnamespace, on port 8080 - Can send egress to:
postgrespods indatabasenamespace, 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.