Block outbound internet access with Kubernetes egress filtering
For developers running workloads in Kubernetes who need to stop pods from calling arbitrary internet destinations. This shows the exact NetworkPolicy manifests, namespace labels, test pods, and verification commands to default-deny egress and allow only DNS plus specific destinations.
TL;DR — To stop workloads from calling anywhere on the internet in Kubernetes, apply a default-deny egress
NetworkPolicy, then add explicit allow rules for DNS and only the CIDRs or pods your app must reach. The most common reason it "doesn't work" is that your CNI does not enforceNetworkPolicy, so test that first before debugging YAML. Reading time: ~5 min
Goal
When you are done, pods in one namespace cannot open outbound connections to arbitrary internet IPs; they can reach only the destinations you explicitly allow, and you can prove it with curl, nc, and kubectl output.
Prerequisites
- Kubernetes cluster access with permission to create namespaces, pods, and network policies — check with:
kubectl auth can-i create networkpolicies --all-namespaces
kubectlinstalled, ideally v1.29+ — check with:
kubectl version --client --short
- A CNI plugin that enforces
NetworkPolicy(for example Calico, Cilium, Antrea, or a cloud CNI with policy support enabled) - A test namespace name; this article uses
egress-demo - The DNS service IP in your cluster — get it with:
kubectl -n kube-system get svc -l k8s-app=kube-dns -o wide || kubectl -n kube-system get svc -l k8s-app=coredns -o wide
- If you need to allow access to an internal service by IP, the destination CIDR(s) you want to permit
Steps
Step 1: Create a test namespace and a curl pod
kubectl create namespace egress-demo
kubectl run curl --namespace egress-demo --image=curlimages/curl:8.7.1 --restart=Never -- sleep 3600
kubectl wait --namespace egress-demo --for=condition=Ready pod/curl --timeout=90s
What you should see when this succeeds: pod/curl condition met.
Step 2: Confirm your cluster currently allows outbound internet
kubectl exec -n egress-demo curl -- curl -I --max-time 10 https://example.com
What you should see when this succeeds: an HTTP response header block like:
HTTP/2 200
content-type: text/html
cache-control: max-age=86000
If this already times out, you already have egress controls upstream; keep going, but verify against a destination you know should be reachable.
Step 3: Apply a default-deny egress policy to the namespace
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: egress-demo
spec:
podSelector: {}
policyTypes:
- Egress
egress: []
YAML
What you should see when this succeeds: networkpolicy.networking.k8s.io/default-deny-egress created.
Step 4: Test that arbitrary outbound traffic is now blocked
kubectl exec -n egress-demo curl -- curl -I --max-time 10 https://example.com; echo "exit_code=$?"
What you should see when this succeeds: a timeout and a non-zero exit code, typically:
curl: (28) Connection timed out after 10001 milliseconds
command terminated with exit code 28
exit_code=28
If you still get HTTP/2 200, your CNI is not enforcing NetworkPolicy or the pod is using host networking.
Step 5: Allow DNS so name resolution still works for approved destinations
DNS_IP=$(kubectl -n kube-system get svc -l k8s-app=kube-dns -o jsonpath='{.items[0].spec.clusterIP}' 2>/dev/null)
if [ -z "$DNS_IP" ]; then DNS_IP=$(kubectl -n kube-system get svc -l k8s-app=coredns -o jsonpath='{.items[0].spec.clusterIP}'); fi
echo "$DNS_IP"
kubectl apply -f - <<YAML
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: egress-demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: ${DNS_IP}/32
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
YAML
What you should see when this succeeds: the printed DNS service IP and networkpolicy.networking.k8s.io/allow-dns created.
Step 6: Verify DNS works but internet access is still blocked
kubectl exec -n egress-demo curl -- nslookup example.com
kubectl exec -n egress-demo curl -- curl -I --max-time 10 https://example.com; echo "exit_code=$?"
What you should see when this succeeds: nslookup returns one or more IPs, but curl still times out with exit code 28.
Step 7: Allow only the specific destination your workload needs
For an internal service reachable at 10.20.30.40:443, apply this exact policy:
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-specific-egress
namespace: egress-demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.20.30.40/32
ports:
- protocol: TCP
port: 443
YAML
What you should see when this succeeds: networkpolicy.networking.k8s.io/allow-specific-egress created.
If the destination is another pod or service in the cluster, prefer namespace/pod selectors over IPs:
kubectl label namespace backend name=backend --overwrite
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-namespace-443
namespace: egress-demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: backend
ports:
- protocol: TCP
port: 443
YAML
What you should see when this succeeds: the namespace label is applied and the policy is created.
Step 8: Scope the policy to only selected workloads if needed
If you only want to restrict pods with label app=api, replace podSelector: {} with this:
kubectl apply -f - <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-default-deny-egress
namespace: egress-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress: []
YAML
What you should see when this succeeds: only pods labeled app=api are affected; unlabeled pods still have their previous behavior.
Verify it works
Run all three checks:
kubectl exec -n egress-demo curl -- nslookup example.com
kubectl exec -n egress-demo curl -- curl -I --max-time 5 https://example.com; echo "exit_code=$?"
kubectl exec -n egress-demo curl -- sh -c 'nc -vz -w 5 10.20.30.40 443'; echo "exit_code=$?"
Expected results:
nslookup example.comreturns addresses, proving DNS is allowed.curl -I https://example.comfails with timeout or connect error, proving arbitrary internet egress is blocked.nc -vz -w 5 10.20.30.40 443prints a successful connect for your approved destination, for example:
10.20.30.40 (10.20.30.40:443) open
exit_code=0
This is the intended traffic shape:
View as text
[Curl Pod] ---- 1. DNS 53 allowed ----> [CoreDNS] [Curl Pod] ---- 2. TCP 443 denied ----> [Internet] [Curl Pod] ---- 3. TCP 443 allowed ---> [10.20.30.40]
Common pitfalls
Your CNI does not enforce NetworkPolicy
Mistake: applying correct YAML on a cluster where the network plugin ignores NetworkPolicy.
Symptom: after default-deny-egress, curl -I https://example.com still returns HTTP/2 200.
Fix: check your cluster networking documentation and enable a policy-capable CNI or policy enforcement mode before testing again.
You forgot DNS, so approved hostnames fail too
Mistake: adding default-deny-egress and an allow rule for 443, but no rule for port 53 to CoreDNS.
Symptom: curl https://api.internal.example fails immediately with curl: (6) Could not resolve host.
Fix: apply the allow-dns policy with the actual CoreDNS service IP and both UDP/TCP 53.
You allowed a Service IP that changes or does not represent the real destination
Mistake: using ipBlock for a ClusterIP or an external endpoint behind changing IPs.
Symptom: it works briefly, then breaks after service recreation or upstream IP rotation.
Fix: for in-cluster traffic, use namespaceSelector/podSelector; for external dependencies, enforce egress through a fixed proxy, NAT, or firewall-managed destination list instead of chasing IPs in NetworkPolicy.
The pod uses hostNetwork: true
Mistake: testing with a pod that bypasses normal pod networking.
Symptom: policy appears ignored for that pod only.
Fix: run the test pod without hostNetwork: true; check with:
kubectl get pod curl -n egress-demo -o jsonpath='{.spec.hostNetwork}{"\n"}'
You matched the wrong pods
Mistake: podSelector labels do not exist on the pods you intended to restrict.
Symptom: some workloads are blocked, others still reach the internet.
Fix: inspect labels and align the selector exactly:
kubectl get pods -n egress-demo --show-labels
Existing connections survive briefly after policy changes
Mistake: expecting a long-lived TCP session to drop the instant you apply a deny policy. Symptom: a running app keeps talking to an already-open connection for a short time, while new connections fail. Fix: restart the pod or re-open the connection to test the new policy path:
kubectl delete pod -n egress-demo curl
kubectl run curl --namespace egress-demo --image=curlimages/curl:8.7.1 --restart=Never -- sleep 3600
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI