Module 4: Networking Ingress And Networkpolicy
7. Hands-on: real `NetworkPolicy` on `andes-cargo`
Description
This is the lesson where lesson 6's deny-by-default model gets tested against andes-cargo-cluster for real — with a finding that wasn't in the script, and that's worth documenting with the same honesty holding up the rest of this guide. There's a widely repeated warning in Kubernetes tutorials and forums: "kind, with its default CNI (kindnet), doesn't enforce NetworkPolicy — you can apply the policy, you're not going to get any error, and traffic is going to keep flowing exactly the same, with nothing warning you." That warning was true for years. This lesson verifies it, live, against your own cluster — and the real result contradicts conventional wisdom. Everything that follows really ran, including the investigation into why.
Connection to the module
This lesson builds, against the real cluster, exactly the two-policy pattern lesson 6 explained in theory: default-deny-ingress first, allow-from-ingress-nginx after. The difference from lesson 6 is that here every claim gets verified with a real curl, not a diagram.
Step 1 — A test Pod, to simulate "any other Pod in the cluster"
Lesson 6 described the problem with a hypothetical Pod, "with no relation to Andes Cargo." This lesson makes it real: a Pod named test-client, in the same andes-cargo namespace (to simulate the most generous possible case — you don't even need to be in another namespace for the problem to exist), with the sole task of trying to curl against status-api-service.
kubectl run test-client --image=curlimages/curl:8.11.1 -n andes-cargo --restart=Never --command -- sleep 3600
What to expect:
pod/test-client created
kubectl wait --for=condition=Ready pod/test-client -n andes-cargo --timeout=60s
What to expect:
pod/test-client condition met
Step 2 — Confirm the baseline: with no NetworkPolicy, direct access works
Before restricting anything, confirm the exact problem lesson 6 described: test-client talks directly to status-api-service, bypassing Ingress, with no resistance whatsoever.
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 5 http://status-api-service.andes-cargo.svc.cluster.local/health
What to expect (literal, executed):
HTTP_STATUS:200
200, direct — the same result any Pod, in any namespace, would get, never having gone through ingress-nginx. This is your baseline: keep it in mind, because the rest of this lesson measures against it.
Step 3 — The first policy: default-deny-ingress
# networkpolicy-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: andes-cargo
spec:
podSelector: {}
policyTypes:
- Ingress
podSelector: {} — an empty selector — is the exact syntax for "every Pod in this namespace," with no exception. With no ingress: section declared, and with policyTypes: [Ingress] present, the effect is maximum possible isolation: no Pod in the andes-cargo namespace is going to accept incoming traffic from any source, until another NetworkPolicy adds an explicit exception.
kubectl apply -f networkpolicy-default-deny.yaml
What to expect:
networkpolicy.networking.k8s.io/default-deny-ingress created
Step 4 — Re-verify: was it really blocked?
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://status-api-service.andes-cargo.svc.cluster.local/health
What to expect (literal, executed — a 000 code means curl never received any HTTP response, and the command ends with a nonzero exit code):
HTTP_STATUS:000
command terminated with exit code 28
Traffic got blocked — for real. curl's exit code 28 specifically means "timeout" (CURLE_OPERATION_TIMEDOUT). To rule out any doubt that this is a DNS problem instead of a real network block, repeat the test against the Pod's IP directly, bypassing the Service's DNS name:
POD_IP=$(kubectl get pods -n andes-cargo -l app=andes-cargo-status-api -o jsonpath='{.items[0].status.podIP}')
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://$POD_IP:8080/health
What to expect (literal, executed — same result, against the Pod's real IP, with no DNS resolution in the mix):
HTTP_STATUS:000
command terminated with exit code 28
Blocked against the direct IP too. This confirms the block happens at the network layer, not in some name-resolution mechanism — exactly the level a real NetworkPolicy has to operate at to be trustworthy.
Step 5 — The investigation: isn't kindnet supposed to ignore this?
Here's where this lesson stops, because Step 4's result contradicts a widely repeated warning about kind. For years, the community's standard advice was clear: kind's default CNI (kindnet) does not implement NetworkPolicy's enforcement engine — you can apply the policy, kubectl apply isn't going to return any error (the object is syntactically valid), but nothing in the cluster enforces it. Step 4's blocked traffic shouldn't have happened, per that warning.
Investigated against the primary source (not assumed from general knowledge), the full story is this: the kind project merged NetworkPolicy enforcement support directly into kindnetd itself on July 23, 2024, in pull request kubernetes-sigs/kind#3612 — built on the kube-network-policies project, instead of creating a separate component. The change's own description sums it up: network policy enforcement becomes, literally, "part of kindnetd, just a different DaemonSet" — not an external CNI replacing kindnet, but a new capability added to the same project.
kubectl get daemonset kindnet -n kube-system -o jsonpath='{.spec.template.spec.containers[0].image}'
What to expect (literal — your exact image tag depends on the kind version you installed in Module 1; what matters is that it corresponds to a version published well after July 2024):
docker.io/kindest/kindnetd:v20260528-9350166c
This investigation's honest conclusion: the "kind doesn't enforce NetworkPolicy" warning was true for years, and keeps circulating in tutorials and forum answers that weren't updated — but it stopped being universally true as of mid-2024. The kindnetd version this guide installed (Module 1) does really enforce NetworkPolicy, with real evidence in Step 4. This is exactly why this guide never repeats a technical claim without verifying it against the real cluster: a recommendation that was correct at the time can become outdated with no one updating the text that repeats it. Don't trust this lesson either without verifying it — if you pick this guide back up with a kind version much older than v0.32.0 (before July 2024), repeat Step 4 yourself before assuming the result is going to be the same.
THE TIMELINE THIS LESSON VERIFIED
Years before Jul-2024 kindnetd does NOT enforce NetworkPolicy
(the warning still circulating)
│
Jul-23-2024 PR #3612 merged: NetworkPolicy
enforcement added TO kindnetd
(based on kube-network-policies)
│
This lesson (Aug-2026) Verified live, against
andes-cargo-cluster: kindnetd
DOES block real traffic
Step 6 — About Calico, and why this lesson doesn't install it
Before Step 5's finding, this guide expected to have to install Calico — the third-party CNI most often cited as a replacement to get real NetworkPolicy enforcement on kind — to be able to demonstrate a genuine block. The live investigation changed that plan, and it's worth explaining why, instead of installing it anyway with no real need.
Calico remains, today, an extremely common choice on real production clusters — including EKS, where the default CNI (Amazon VPC CNI, Module 7) historically had more limited NetworkPolicy support than a dedicated CNI like Calico — and offers capabilities beyond what Kubernetes' standard NetworkPolicy object covers: GlobalNetworkPolicy (cluster-wide rules, not per namespace), policies with explicit priority and true "deny" rules (unlike the purely additive model lesson 6 explained), and an eBPF-based data plane for high-performance clusters. Installing Calico would also require recreating the cluster a second time in this same lesson (disableDefaultCNI: true is, same as extraPortMappings, configuration only declared at cluster-creation time — the same restriction you already saw in lesson 4) — a real cost, with no pedagogical gain, because this lesson's goal (a real, verified block) is already met with direct evidence.
This guide prefers the honesty of an unexpected, well-investigated, well-cited result, over installing an additional piece just to confirm something the cluster already demonstrated on its own.
Step 7 — The second policy: allow only ingress-nginx's traffic
With the block confirmed as real, add the explicit exception status-api-service needs to keep working through the door lessons 4-5 built:
# networkpolicy-allow-ingress-nginx.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-nginx
namespace: andes-cargo
spec:
podSelector:
matchLabels:
app: andes-cargo-status-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
Three decisions, each deliberate:
podSelector: app: andes-cargo-status-api, not{}— this policy only affects Andes Cargo's Pods, not the whole namespace (useful the moment the namespace has more than one service, something this guide doesn't build but any real cluster ends up having).namespaceSelector: kubernetes.io/metadata.name: ingress-nginx—kubernetes.io/metadata.nameis a label Kubernetes automatically adds to every namespace since version 1.21, with the exact namespace name as its value. It's the recommended way to select a namespace by name in aNetworkPolicy, with no dependence on someone having labeled that namespace by hand.ports: 8080/TCP— the exception is limited to the exact port the container listens on (targetPortinservice.yaml, Module 2), not "any port from that namespace." Least privilege, applied down to the last available detail.
kubectl apply -f networkpolicy-allow-ingress-nginx.yaml
What to expect:
networkpolicy.networking.k8s.io/allow-from-ingress-nginx created
Step 8 — Verify both paths, one by one
The allowed path — through Ingress:
curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health
What to expect (literal, executed):
HTTP_STATUS:200
The path that stays blocked — test-client direct, bypassing Ingress:
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://status-api-service.andes-cargo.svc.cluster.local/health
What to expect (literal, executed — unchanged from Step 4, because test-client runs in the andes-cargo namespace, not in ingress-nginx):
HTTP_STATUS:000
command terminated with exit code 28
This pair of results is the complete proof of the two-policy pattern lesson 6 explained: traffic coming from ingress-nginx — this module's design's only legitimate path — gets through; any other Pod, including one in the same namespace as Andes Cargo, remains completely blocked. Confirm both policies' final state:
kubectl get networkpolicy -n andes-cargo
What to expect (literal, executed — AGE is your variable value):
NAME POD-SELECTOR AGE
allow-from-ingress-nginx app=andes-cargo-status-api 22s
default-deny-ingress <none> 2m53s
kubectl describe networkpolicy allow-from-ingress-nginx -n andes-cargo
What to expect (literal, executed):
Name: allow-from-ingress-nginx
Namespace: andes-cargo
Created on: 2026-08-14 14:42:01 -0600 CST
Labels: <none>
Annotations: <none>
Spec:
PodSelector: app=andes-cargo-status-api
Allowing ingress traffic:
To Port: 8080/TCP
From:
NamespaceSelector: kubernetes.io/metadata.name=ingress-nginx
Not affecting egress traffic
Policy Types: Ingress
Not affecting egress traffic — the last line — confirms exactly what lesson 6 anticipated: this policy, by declaring only policyTypes: [Ingress], doesn't touch at all andes-cargo-status-api's ability to initiate outbound connections (for example, toward an external database like LocalStack, if someone wired it up). /shipments/<id>'s 500 keeps existing for the same cause as always — no module in this guide installs LocalStack inside the cluster — not because of any new restriction from this lesson.
Analogy: the locks installed, tested with the wrong key first
Picking back up the building analogy: this lesson really installed the locks on every interior door of Andes Cargo's office (default-deny-ingress), and tested them the only trustworthy way — trying to walk in with someone who does not have the correct card (test-client), and confirming the door really doesn't open. Only after that test did the correct card get handed exactly to whoever should have it: the building's receptionist (ingress-nginx), and only them. Any other resident of the building — including one who lives on the same floor as Andes Cargo — still can't get in, even standing physically nearby.
Common mistakes
Repeating "kind doesn't enforce NetworkPolicy" without having verified it against your own version (the mistake this lesson corrects at the root, and the most important one to prevent going forward). What happens: someone reads that warning in a tutorial or a forum answer — very likely written before July 2024 — and repeats it as a permanent fact, without considering the kind project itself changed that behavior. How to spot it: if you're about to install Calico "because kind doesn't support this," without having first run this lesson's Step 4 against your own cluster. How to fix it: always verify, with this lesson's exact pattern (baseline, apply the policy, re-verify), before assuming any infrastructure behavior depending on a specific tool version that changes over time.
Forgetting default-deny-ingress also blocks ingress-nginx's legitimate traffic, until the second policy is added (sequencing, already anticipated in Step 8 but easy to overlook if you skip steps). What happens: someone applies only default-deny-ingress and, testing curl through Ingress, sees it also fails, and thinks something went wrong with ingress-nginx itself. How to spot it: if curl via --resolve andes-cargo.local fails right after Step 3, before having reached Step 7. How to fix it: it's the expected behavior — default-deny-ingress makes no exception by default, not even for traffic coming from your own Ingress. The second policy (Step 7) is what restores that specific path.
Confusing the 000 code/exit code 28 with an application error (diagnostics, distinguishes this lesson from lesson 5). What happens: someone sees curl returning no HTTP code and looks for the problem in andes-cargo-status-api's logs, expecting to find a traceback like Module 3's. How to spot it: andes-cargo-status-api's Pods' kubectl logs shows no new entry corresponding to the blocked attempt — the request never reached the application. How to fix it: a 000 code with curl means the connection never completed at the network level — the packet got dropped before reaching the Flask process. That total absence of application logs is, in itself, proof the block happens at an earlier layer (the NetworkPolicy, enforced by kindnetd), not inside the container.
Exercises
Exercise 1 — Reconstruct the whole investigation from memory. Without going back to the lesson, describe, in order: (a) which warning the community repeats about kind and NetworkPolicy; (b) what you found when verifying it against your cluster; (c) the exact source (PR and date) explaining the discrepancy.
See solution
(a) That kindnet, kind's default CNI, doesn't enforce NetworkPolicy — the policy would be accepted with no error, but wouldn't block anything. (b) When applying default-deny-ingress against the real andes-cargo-cluster, traffic did get blocked (curl with code 000, exit code 28, verified against the Service and directly against the Pod's IP). (c) Pull request kubernetes-sigs/kind#3612, merged July 23, 2024, which added NetworkPolicy enforcement directly to kindnetd, based on the kube-network-policies project.
Exercise 2 — Explain why testing with the Pod's IP, not just the DNS name, mattered. A colleague asks why this lesson repeated Step 4's test against the Pod's direct IP, given it had already failed against the Service's DNS name. Explain, in two or three sentences, what doubt that second test rules out.
See solution
A reasonable explanation: "A curl that fails against a DNS name could fail for two very different reasons: CoreDNS not resolving the name (a DNS problem, not a network block — exactly what happened in Module 3 with LocalStack), or the network connection itself being blocked. Repeating the test directly against the Pod's IP removes name resolution from the equation entirely — if it still fails against a real IP, the only possible explanation is a network-level block, exactly what a NetworkPolicy should produce."
Exercise 3 — Predict what would happen if test-client ran in the ingress-nginx namespace. If, instead of creating test-client in the andes-cargo namespace, you had created it inside the ingress-nginx namespace, would you expect curl against status-api-service to work, after applying both this lesson's policies? Justify your answer with allow-from-ingress-nginx's exact rule.
See solution
Yes, it would work — the allow-from-ingress-nginx rule allows traffic from any Pod whose namespace has the kubernetes.io/metadata.name: ingress-nginx label, with no additional restriction on which specific Pod within that namespace. This reveals a real limitation of this lesson's design, useful for understanding the model in depth: the policy trusts the source namespace, not the specific Pod's identity (ingress-nginx-controller, and no other). Any Pod, unrelated to the real controller, deliberately placed inside the ingress-nginx namespace (something that would require permission to create objects there, a different security layer — RBAC — out of this guide's scope) would also pass the policy. On a real production cluster, that RBAC layer is exactly what prevents anyone from simply "moving into" the correct namespace to bypass a namespaceSelector-based NetworkPolicy.
Summary and next step
This lesson built, against the real andes-cargo-cluster, lesson 6's complete two-policy pattern — default-deny-ingress followed by allow-from-ingress-nginx — with every claim verified with real curl: direct access open before any policy (200), completely blocked after default-deny-ingress (000, verified against the Service and against the Pod's IP), and restored only for the legitimate path after the second policy (Ingress → 200; any other Pod → still blocked). Along the way, this lesson corrected a widely repeated warning about kind: since July 2024, kindnetd does really enforce NetworkPolicy, a finding verified live and cited against its primary source, not assumed from conventional wisdom.
Before moving on you should be able to: reconstruct the complete two-policy pattern against a real cluster; explain why the warning about kind and NetworkPolicy stopped being universally true, with the exact date and PR; and diagnose a real NetworkPolicy block (000 code, no application logs) versus an error in the application itself.
Next lesson: this module's project, status-api-service exposed and protected. There Ingress (lessons 4-5) and NetworkPolicy (lessons 6-7) come together in the same final verification: the allowed path, and the blocked path, both confirmed, side by side.
Resources
- Kubernetes — Network Policies — the same reference from lesson 6, now confirmed with real block-and-allow evidence.
- GitHub — kubernetes-sigs/kind, PR #3612: Kindnet Network Policies — this lesson's exact primary source: merged July 23, 2024.
- Kubernetes — Namespaces: Automatic Labelling — official reference for the
kubernetes.io/metadata.namelabel, used inallow-from-ingress-nginx. - Project Calico — the CNI named by contrast in Step 6, with extended support (
GlobalNetworkPolicy) beyond Kubernetes' standardNetworkPolicy. - Calico — Network Policy — official documentation for Calico's extended policy model, for anyone wanting to go deeper than this guide's scope.