In short

Start each namespace with a default-deny NetworkPolicy for ingress and egress, then allow only the flows the application needs, including DNS over UDP and TCP port 53. Select namespaces with the immutable kubernetes.io/metadata.name label, keep namespace and pod selectors in one peer entry when both must match, and remember that policies only add allows. Enforce deployment rules with an admission policy, such as a ValidatingAdmissionPolicy, and prove both with live allowed and denied probes.

  • NetworkPolicy objects do nothing without a network plugin that enforces them; prove each rule with real connections.
  • Policies form a union of allows: a broad second policy reopens what a narrow one restricts, and an empty policy subtracts nothing.
  • namespaceSelector and podSelector in one peer entry mean AND; split into two entries they mean OR.
  • Select namespaces by kubernetes.io/metadata.name: the API server sets it, so a tenant cannot relabel its way in.
  • Default-deny egress also blocks DNS until you allow UDP and TCP port 53 to the cluster DNS pods.
  • A ValidatingAdmissionPolicy runs CEL inside the API server, so the rule needs no webhook to stay available.

Verify it

  1. Run the probe script: prod/api must answer from db:5432, while prod/debug and test/api must fail to connect.
  2. Resolve a service name from an isolated pod to confirm the DNS allowance, then connect to an external address and expect a timeout.
  3. Run kubectl run t -n prod --image=busybox:1.37 --dry-run=server and expect the ValidatingAdmissionPolicy denial.
  4. List every policy that selects the database with kubectl get networkpolicy -n prod -o yaml and confirm that none allows all namespaces on 5432.
  5. Repeat the probes after network plugin, policy or cluster upgrades.
Applies to
  • Kubernetes 1.37.0 tested with kind 0.33.0, whose default kindnet plugin enforces NetworkPolicy; ValidatingAdmissionPolicy is stable since 1.30

Kubernetes network and admission policy. Enforcement depends on the installed controller, CNI and exception paths.

Part of
Learning path: Runtime and cloud defense (step 5 of 13). Workflow: Fix a finding
Learning goals and prerequisitesAbout 26 minutes: 7 to read and 19 for the exercise

What you will learn

  • Evaluate the union of NetworkPolicy allow rules
  • Distinguish peer selector AND from separate-rule OR
  • Explain what admission and runtime network tests independently prove

Before you start

Platform engineers and service owners who write Kubernetes NetworkPolicies and admission rules

  • Understand Pod selectors, namespaces, ingress/egress directions and admission requests.

Refresh first: Run Kubernetes workloads with only the privileges they need

Go to the guided practice

Policy meaning depends on the enforcing component

A Kubernetes NetworkPolicy object describes desired traffic rules, but enforcement depends on a supporting network implementation. Admission controls decide whether an API operation may create or change a resource; they do not replace runtime network controls. Review both policies alongside their controllers, versions, permissions and failure behavior.

Start with a connection inventory: frontend to API, API to database, DNS resolution and required outbound providers. Include management and health paths. Namespaces are useful organizational scopes but do not, by themselves, block network traffic. Policies compose, so an additional broad allow can reopen a path that a reviewer assumed was closed.

Worked policy: why a second allow rule reopens the database

A database pod in namespace prod has label app=db. It should receive TCP/5432 only from pods labeled app=api in the prod namespace. NetworkPolicy rules are additive. Once a direction is isolated, any applicable allow rule can permit traffic in that direction. A broad second policy is not overridden by a more specific first one; a deny-looking policy with an empty rule list does not subtract permissions granted elsewhere.

The YAML selects the source namespace by kubernetes.io/metadata.name, a label the API server sets to the namespace name and keeps at that value even when someone tries to overwrite it; a label such as environment: prod can be added to any namespace by whoever may edit namespace labels. It places namespaceSelector and podSelector in the same peer entry, so they jointly constrain the source: the pod must have app=api and live in prod. Writing two peer entries, one containing each selector, changes the policy to two allowed sets. That admits every pod in prod, plus the pods selected by the separate pod-only peer in the policy namespace. The indentation changes the security meaning, and the cluster test in the network section shows it.

For a connection to succeed when both endpoints are isolated, applicable source egress and destination ingress must permit it. The database policy below covers destination ingress only. Add source egress and DNS allowances according to the cluster’s supported networking behavior; do not assume successful name resolution establishes application connectivity. Standard NetworkPolicy governs supported network-layer flows, not a user’s right to read a database row.

Admission asks a different question: may this resource enter the cluster? A policy requiring a verified artifact identity and prohibiting privileged execution should reject an untrusted signer even if the signature is cryptographically valid. Test controller outage behavior and protected exceptions. A document that passed admission can still communicate over a network path you accidentally allowed; a perfectly isolated network does not prevent an administrator from deploying a hostile image.

Fixeddb-from-prod-api.yamlYAML
# Ingress-only example; requires a network plugin that enforces NetworkPolicy.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: db-from-prod-api, namespace: prod}
spec:
  podSelector: {matchLabels: {app: db}}
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: prod}}
          podSelector: {matchLabels: {app: api}}
      ports: [{protocol: TCP, port: 5432}]

Permit the actual dependency graph

Use default-deny isolation as a starting point where supported, then add narrow ingress and egress rules for required peers, ports and DNS. Understand whether selectors refer to pods, namespaces or both in the chosen rule structure. Verify labels are governed so an untrusted workload cannot simply assign itself the labels used to confer access.

Standard NetworkPolicy is not a general HTTP authorization engine or portable hostname-based egress filter. If a service fetches caller-provided URLs, combine network restrictions with connection-time SSRF protections, as the SSRF guide at /domains/appsec/secure-features/ssrf-egress-boundaries/ describes, and suitable egress infrastructure. Test behavior for service addresses, direct pod addresses and any host-network or external paths relevant to the deployment.

The policy set below applies that design to the whole namespace: deny everything, allow DNS, let the database accept api pods only, and let api pods reach only the database. On a kind 0.33 cluster running Kubernetes 1.37, prod/api reached the database while prod/debug and test/api could not, DNS still resolved, and an external connection from the api pod timed out. With egress left open, splitting the two selectors into separate peers let prod/debug connect, and adding a second policy that allowed every namespace on 5432 let all three connect: the two regressions the exercise asks you to predict.

Configurationprod-network-policies.yamlYAML
# 1. Isolate every pod in prod for ingress and egress until a policy allows a flow
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: prod}
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 2. Let every pod in prod resolve names through cluster DNS, over UDP and TCP
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-dns, namespace: prod}
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}
          podSelector: {matchLabels: {k8s-app: kube-dns}}
      ports:
        - {protocol: UDP, port: 53}
        - {protocol: TCP, port: 53}
---
# 3. The database accepts TCP 5432 only from api pods in the prod namespace.
#    kubernetes.io/metadata.name is set by the API server and cannot be relabeled.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: db-from-prod-api, namespace: prod}
spec:
  podSelector: {matchLabels: {app: db}}
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: prod}}
          podSelector: {matchLabels: {app: api}}
      ports: [{protocol: TCP, port: 5432}]
---
# 4. api pods may connect to the database and nowhere else
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: api-to-db, namespace: prod}
spec:
  podSelector: {matchLabels: {app: api}}
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector: {matchLabels: {app: db}}
      ports: [{protocol: TCP, port: 5432}]

Reject unsafe changes with a recoverable operating model

Define a small admission contract: permitted registries or artifact identities, prohibited privilege settings, required resource bounds and approved exceptions. The privileged container lab at /labs/kubernetes-policy/ has you reject a privileged workload before deployment. Artifact verification must validate the expected signer and provenance policy, not merely detect that some signature exists. Signing and build-provenance mechanics belong to the supply-chain workflow; admission enforces the resulting deployment decision.

Protect the admission controller's identity, configuration and communication. Choose timeout and failure behavior deliberately, testing both controller outage and normal updates. A fail-closed policy can block incident recovery if operators have never rehearsed a controlled exception; an unrestricted bypass defeats the policy. Audit exceptions and keep the escape route narrower than routine administration.

A ValidatingAdmissionPolicy expresses such a rule in CEL and runs inside the API server, so it has no webhook to fail. The policy below, bound to the prod namespace, admits a pod only when every container, init container and ephemeral container image comes from registry.example/ and is pinned by digest. In the test cluster it denied busybox:1.37, a tag from the approved registry and an approved application image paired with an unapproved init container, and it admitted a digest-pinned image; the unbound test namespace was unaffected. CEL can check strings, not signatures: verify signer identity with Sigstore policy-controller or Kyverno verifyImages, as the artifact signature guide at /domains/supply-chain/provenance/artifact-signature-policy/ shows. A rule on pods also leaves a Deployment that creates rejected pods admitted but failing, so match the workload controllers too if you want earlier feedback.

Admission webhooks are network services too, and they need the same treatment as any other workload. In 2025 the ingress-nginx admission webhook accepted requests from anything on the Pod network without authentication, which turned configuration-injection bugs into a 9.8 cluster-takeover risk (CVE-2025-1974); a policy that lets only the control plane reach webhook ports would have closed that path: /domains/runtime/containers/case-study-ingress-nginx/.

Configurationapproved-images-policy.yamlYAML
# Admit pods in prod only when every container image comes from the approved
# registry and is pinned by digest. Built into the API server since Kubernetes 1.30.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: approved-images-by-digest
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods", "pods/ephemeralcontainers"]
  variables:
    - name: images
      expression: >-
        object.spec.containers.map(c, c.image)
        + (has(object.spec.initContainers) ? object.spec.initContainers.map(c, c.image) : [])
        + (has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers.map(c, c.image) : [])
  validations:
    - expression: "variables.images.all(i, i.startsWith('registry.example/') && i.contains('@sha256:'))"
      message: "every image must come from registry.example/ and be pinned by digest"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: approved-images-by-digest-prod
spec:
  policyName: approved-images-by-digest
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels: {kubernetes.io/metadata.name: prod}

Use positive and negative probes

Run an allowed frontend request and a forbidden peer request against the same destination, observing both source and target. Confirm the network plugin enforces the object. Submit a permitted manifest, one violating each admission rule, and an update that introduces a violation. Test after upgrades and policy-controller outages. Retain cluster version, plugin configuration and expected outcomes so an environment change cannot inherit stale assurance.

The script below opens a fresh TCP connection to the database from each source pod. Run it after every policy change; an answer from db means the connection was allowed.

Testprobe-db.shShell
# Fresh TCP connections to db:5432 from three sources (answer "db-ok" means allowed)
for src in prod/api prod/debug test/api; do
  ns=${src%/*}; pod=${src#*/}
  out=$(kubectl -n "$ns" exec "$pod" -- sh -c 'echo | nc -w 3 db.prod.svc.cluster.local 5432 2>&1' 2>&1)
  echo "$src -> db:5432: ${out:-no answer}"
done

Diagnose incomplete fixes

A network plugin reporting that it supports NetworkPolicy is a prerequisite, not the result. Test actual connections from separately identified source pods, and inspect all policies selecting either endpoint. Cached connections, host networking, address translation and plugin-specific behavior can affect observations. State whether the probe creates a fresh connection and which plugin/version enforced it.

Labels used as security selectors become part of the authorization boundary. If a tenant may relabel its namespace environment=prod or launch a pod with a privileged selector label, the apparent policy can be bypassed through authorized object changes. Protect label administration and admission rights rather than treating a naming convention as trustworthy identity.

Check the result against concrete cases

These outcomes belong to the worked example; the cluster test above observed each network case on Kubernetes 1.37 with kindnet, and a different plugin needs its own run.

Check the result against concrete cases
CaseExpected resultWhat it establishes
prod/API → db:5432 with required source egressAllowIntended peer remains usable
prod/debug → db:5432Deny absent another allowPod selector matters
test/API → db:5432Deny absent another allowNamespace selector matters
prod/API → db:3306Deny absent another allowPort restriction matters
Add another policy allowing all pods on 5432Previously denied peers can become allowedPolicies form an additive union
Valid signature from unapproved identityAdmission denial under stated trust policyAuthenticity alone is not publisher authorization

Guided practice

Evaluate the supplied policy and connection matrix by hand. All source egress is permitted for this exercise; only the database ingress policies decide the network result.

Supplied scenario material
Plain text
Database: namespace prod(environment=prod), pod app=db.
Sources: A=prod/app=api; B=prod/app=debug; C=test(environment=test)/app=api.
P1 is the YAML above. P2 selects the same database and allows every pod in every namespace on TCP/5432.
A,B,C each create a new connection to db:5432.
Proposed fix F1 keeps P2 and adds an empty-ingress policy.
Proposed fix F2 removes P2 and retains P1.
Admission: permitted signer build-prod; image X has a valid signature by build-test.
  1. 1. Compute the original effective policy

    Decide A, B and C with P1 and P2 both installed. Explain the role of each policy.

    Expected result and reasoning

    All three are allowed by P2. P1 permits A but cannot subtract B or C from the additional broad allow. Treating the narrower rule as higher priority would produce the wrong result.

  2. 2. Compare the two fixes

    Evaluate F1 and F2 and identify the change that restores the intended boundary.

    Expected result and reasoning

    F1 does not help: an empty ingress policy contributes no allows but does not cancel P2. F2 allows A through P1 and denies B and C, given the stated assumptions and supporting network enforcement.

  3. 3. Change one YAML indentation decision

    Predict the effect of splitting the two selectors into separate peers in P1.

    Expected result and reasoning

    The peers become alternatives. In particular B now matches the prod-namespace peer despite app=debug. This concrete regression should be retained because the change can look like harmless YAML formatting.

  4. 4. Separate network and admission conclusions

    Decide image X and identify whether F2 provides any evidence about its publisher.

    Expected result and reasoning

    X must fail the build-prod identity requirement even though its signature is valid. F2 constrains network access and establishes nothing about X’s provenance. Retain separate deployment-admission and network-connection evidence.

Your deliverable: Allow or deny decisions for A, B and C under each policy set, the corrected policy, the split-peer regression and a separate admission decision for image X.

The exercise is a desk evaluation with source egress assumed open; the probes and policies on this page show how to confirm each decision on a disposable cluster.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Repair the code

Privileged container lab: Reject a privileged workload before deployment

NetworkPolicy lab: Lock down a shop namespace

Run a local project

Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.

Node.js 22+ offline · kind/Docker/kubectl for live cluster · 45–60 min

Verify Kubernetes admission and network boundaries

Repair warning-only admission and an overbroad network rule, then apply them to a disposable cluster and probe allowed and denied paths.

Needs: The live-cluster steps (2 to 5 in README.txt) need Docker with Linux containers, kind v0.31.0, kubectl v1.35, internet access and another 75–120 minutes. The offline tests need only Node.js.

In the extracted folder, run:

Shell
node --test exercise.test.mjs
node render.mjs
# Follow README.txt for pinned cluster setup and live probes

12 regression tests · The starter intentionally fails some cases.

Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.

The offline manifest checks run in the site test suite. The live kind and Calico path, with pinned setup, probes and cleanup, has been run end to end; it is a single-node teaching fixture, not a production baseline.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. Kubernetes Network Policies. Kubernetes v1.37.
  2. Kubernetes admission controllers. Kubernetes v1.37.
  3. Kubernetes security checklist. Kubernetes v1.37.
  4. Kubernetes ValidatingAdmissionPolicy. Kubernetes v1.37.
  5. Kubernetes: declare network policy. Kubernetes v1.37.
  6. Kubernetes: well-known labels (kubernetes.io/metadata.name). Kubernetes v1.37.
  7. kind: release v0.33.0

Reference information

Download this guide as Markdown