CKS — Default Deny Policies

7 minute read

Why Default Deny Is Essential for CKS

By default, Kubernetes allows all pod-to-pod communication within a cluster. Any pod can reach any other pod on any port. This is the opposite of zero-trust networking.

The CKS exam expects you to lock down namespaces with default deny policies as the first step in securing cluster networking. Without default deny:

  • A compromised pod can scan the entire cluster network
  • A pod in frontend can directly access the database in backend
  • Lateral movement after initial compromise is trivial

Default deny is the foundation. Everything else builds on top of it.


Default Deny Ingress

This policy selects all pods in the namespace (empty podSelector) and declares Ingress as a policy type — but specifies no ingress rules. Result: all inbound traffic to every pod in the namespace is denied.

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

What This Does

  • Selects: every pod in the production namespace (podSelector: {} matches all pods)
  • Policy type: Ingress — this policy governs inbound traffic
  • Ingress rules: none — no inbound traffic is allowed
  • Egress: not affected — outbound traffic is still unrestricted

Apply It

kubectl apply -f default-deny-ingress.yaml

# Verify
kubectl get networkpolicy -n production

Default Deny Egress

Same pattern, but for outbound traffic:

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

What This Does

  • Selects: every pod in the production namespace
  • Policy type: Egress — this policy governs outbound traffic
  • Egress rules: none — no outbound traffic is allowed
  • Ingress: not affected

Warning: Default deny egress breaks DNS resolution. Pods will not be able to resolve service names or any hostnames. You almost always need to pair this with an allow-DNS-egress policy. See below.


Default Deny Both Directions

The most restrictive starting point — deny all traffic in both directions:

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

What This Does

  • No inbound traffic to any pod in the namespace
  • No outbound traffic from any pod in the namespace
  • DNS is broken — pods cannot resolve anything
  • This is the most secure starting point — only explicitly allowed traffic can pass

The DNS Problem

After applying default deny egress, every pod in the namespace loses DNS resolution:

# From a pod in the namespace with default deny egress
nslookup kubernetes.default
;; connection timed out; no servers could be reached

This breaks virtually everything — services cannot find each other, health checks fail, and applications crash.

The Fix: Allow DNS Egress

This is the most common CKS pattern — default deny all + allow DNS:

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

What This Does

  • Selects: all pods in production
  • Allows egress: to pods labeled k8s-app: kube-dns in any namespace, on port 53 (UDP and TCP)
  • Everything else egress: still denied (by the default deny policy)

Why TCP and UDP? DNS typically uses UDP, but falls back to TCP for large responses (over 512 bytes) and zone transfers. Always allow both.

Alternative: Allow DNS by Namespace Only

If you don’t want to rely on pod labels, allow egress to the entire kube-system namespace on port 53:

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

This uses the built-in kubernetes.io/metadata.name label that Kubernetes automatically sets on every namespace.


Applying Default Deny Per Namespace

Every sensitive namespace should have its own default deny policy. You cannot create a single cluster-wide default deny NetworkPolicy — NetworkPolicies are namespaced resources.

# Apply default deny to multiple namespaces
for ns in production staging backend database; do
  kubectl apply -f default-deny-all.yaml -n $ns
  kubectl apply -f allow-dns-egress.yaml -n $ns
done

What Happens After Default Deny

Once default deny is in place, the only way to allow traffic is with additional NetworkPolicies that explicitly permit it:

  1. Default deny all → nothing can talk to anything
  2. Allow DNS egress → pods can resolve names, but cannot connect
  3. Allow specific ingress → frontend can receive traffic from the ingress controller
  4. Allow specific egress → backend can reach the database

This is the zero-trust networking model: deny everything, then allow only what is needed.


Common Mistakes

Mistake 1: Forgetting DNS After Default Deny Egress

# This BREAKS everything — no DNS
spec:
  podSelector: {}
  policyTypes:
    - Egress

Always pair default deny egress with an allow-DNS policy.

Mistake 2: Missing policyTypes

# This does NOTHING — no policyTypes means no enforcement
spec:
  podSelector: {}

Without policyTypes, the policy has no effect. You must specify Ingress, Egress, or both.

Mistake 3: Thinking Default Deny Is Cluster-Wide

NetworkPolicies are namespaced. A default deny in production does not affect staging. You need to apply default deny to each namespace individually.

Mistake 4: Applying to the Wrong Namespace

# Wrong — applies to default namespace
kubectl apply -f default-deny-all.yaml

# Right — specify the namespace
kubectl apply -f default-deny-all.yaml -n production

If the metadata.namespace is set in the YAML, it overrides the -n flag. Be careful.


The Complete Default Deny Pattern

Here is the complete two-policy pattern you should memorize for the CKS exam:

Policy 1: Default Deny All

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

Policy 2: Allow DNS Egress

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

Apply both, then add specific allow policies for each workload.


Security Impact

Without Default Deny With Default Deny
Any pod can reach any other pod Only explicitly allowed traffic passes
Compromised pod can scan the cluster Lateral movement is blocked
Data exfiltration is easy Egress must be explicitly allowed
No audit trail of intended traffic Policies document intended traffic flows

Practice Questions

Question 1: Write a NetworkPolicy that denies all ingress traffic to pods in the `secure` namespace.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Key points:

  • podSelector: {} selects all pods in the namespace
  • policyTypes: ["Ingress"] means this policy governs inbound traffic
  • No ingress rules means all inbound traffic is denied
Question 2: After applying default deny egress, DNS stops working. Write the policy to fix DNS while keeping all other egress denied.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This allows egress only to CoreDNS pods on port 53 (UDP and TCP). All other egress remains denied by the default deny policy.

Question 3: What is the effect of a NetworkPolicy with `podSelector: {}` and no `policyTypes` field?

It has no effect. Without policyTypes, the NetworkPolicy does not enforce any rules. You must explicitly specify Ingress, Egress, or both in the policyTypes field.

Actually, there is a nuance: if you specify ingress or egress rules but omit policyTypes, Kubernetes infers the policy types from the rules present. But if neither rules nor policyTypes are specified, the policy is effectively a no-op.

Question 4: Can a single NetworkPolicy apply default deny across all namespaces in a cluster?

No. NetworkPolicies are namespaced resources. A NetworkPolicy in namespace A only affects pods in namespace A. To apply default deny across the cluster, you must create a default deny NetworkPolicy in each namespace individually.

Some tools like Kyverno or OPA Gatekeeper can automate this by creating default deny policies whenever a new namespace is created.

Question 5: You applied default deny all (ingress + egress) and allow-DNS-egress to the `app` namespace. A pod in `app` can resolve DNS names but cannot connect to a service in the same namespace. Why?

The default deny policy blocks all ingress. Even though the client pod can resolve the service name (DNS egress is allowed), the target pod’s ingress is denied. You need an additional NetworkPolicy that allows ingress to the target pod from the client pod.

Additionally, the client pod needs an egress rule allowing traffic to the target pod. The allow-DNS-egress policy only permits egress to port 53 — it does not allow egress to other pods.

Both an ingress allow on the target and an egress allow on the source are required.

Updated: