Control Kubernetes Traffic with Network Policies

KubernetesBeginner
Practice Now

Introduction

Namespaces organize resources but do not automatically block network traffic. NetworkPolicies define which selected Pods may send or receive connections, provided the cluster network plugin enforces them. The prepared Calico network does.

You will prove traffic is initially open, apply default deny, then allow only a labeled client from an approved namespace.

The lab follows a safe policy workflow: establish a working baseline, deny traffic, add the smallest intended exception, then test both an allowed and a denied case. Testing only the allowed client would miss an overly broad policy.

Deploy the Server and Clients

In this step, create separate application and client namespaces, an NGINX server, and two long-running clients.

The commands intentionally use familiar imperative forms so the networking experiment is not buried in unrelated YAML. One client receives label role=approved; the other receives role=untrusted. The client namespace receives access=approved. Later policy selectors will use both label levels.

cd /home/labex/project/network-policy
kubectl create namespace practice-apps
kubectl create namespace practice-clients
kubectl label namespace practice-clients access=approved

Create and expose the server. kubectl expose makes a ClusterIP Service whose selector matches the Deployment's generated app=protected-web Pod label:

kubectl create deployment protected-web -n practice-apps --image=nginx:1.27-alpine --replicas=1
kubectl expose deployment protected-web -n practice-apps --port=80 --target-port=80

Create two diagnostic client Pods. Their sleep loops keep them available for repeated kubectl exec tests:

kubectl run creates each standalone Pod directly. --labels attaches the identity that policy will select, --restart=Never prevents kubectl from creating a workload controller, and -- separates kubectl options from the sh -c command that runs inside the container. On the wait command, --all means every Pod in practice-clients must become ready.

kubectl run approved-client -n practice-clients --image=busybox:1.36 --image-pull-policy=IfNotPresent --labels=role=approved --restart=Never -- sh -c 'while true; do sleep 30; done'
kubectl run untrusted-client -n practice-clients --image=busybox:1.36 --image-pull-policy=IfNotPresent --labels=role=untrusted --restart=Never -- sh -c 'while true; do sleep 30; done'
kubectl wait --for=condition=Ready pod -n practice-clients --all --timeout=120s
kubectl rollout status deployment/protected-web -n practice-apps --timeout=60s

Namespace separation alone still permits both clients to reach the Service.

The readiness commands should finish with evidence for both client Pods and the server Deployment:

pod/approved-client condition met
pod/untrusted-client condition met
deployment "protected-web" successfully rolled out

At this point, labels describe the clients but do not restrict them. A label becomes a traffic rule only after a NetworkPolicy selector uses it.

Prove Traffic Is Initially Open

In this step, request the cross-namespace Service from both clients. The fully qualified name includes Service and namespace.

Inside the same namespace, protected-web would be enough. From practice-clients, protected-web.practice-apps tells cluster DNS both the Service name and its namespace. Test both clients before applying policy so a later failure can be attributed to the policy rather than DNS or Service configuration.

As in the health-probe lab, wget -qO- writes the response body without progress output. --timeout=3 prevents a denied request from hanging indefinitely, and the pipe to grep displays only the recognizable NGINX text.

cd /home/labex/project/network-policy
kubectl exec approved-client -n practice-clients -- wget -qO- --timeout=3 http://protected-web.practice-apps | grep 'Welcome to nginx'
kubectl exec untrusted-client -n practice-clients -- wget -qO- --timeout=3 http://protected-web.practice-apps | grep 'Welcome to nginx'
kubectl get networkpolicies -n practice-apps

With no selecting NetworkPolicy, ingress is allowed. This corrects the common misconception that namespaces are network security boundaries by themselves.

Both commands should match Welcome to nginx, and kubectl get networkpolicies should report no resources. That is the known-good baseline you will intentionally change.

<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>
<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>
No resources found in practice-apps namespace.

The repeated page text proves both client identities can reach the same Service before isolation begins.

Apply Default-Deny Ingress

In this step, select every Pod in practice-apps and allow no ingress sources.

A NetworkPolicy becomes restrictive only for Pods selected by that policy and for the traffic direction listed in policyTypes. Here podSelector: {} selects every Pod in practice-apps, and the absence of any ingress allow rule creates default-deny ingress.

cd /home/labex/project/network-policy
cat <<'EOF' > default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny-ingress, namespace: practice-apps}
spec:
  podSelector: {}
  policyTypes: [Ingress]
EOF
kubectl apply -f default-deny.yaml
kubectl exec approved-client -n practice-clients -- wget -qO- --timeout=3 http://protected-web.practice-apps || echo 'blocked as expected'

An empty podSelector selects every Pod in the policy namespace. The expected timeout proves ingress is now denied, not that the Service disappeared.

|| echo ... handles the expected nonzero exit from wget so the terminal continues. The failure is evidence in this controlled experiment. The server Pod remains ready; only network admission changed.

networkpolicy.networking.k8s.io/default-deny-ingress created
wget: download timed out
command terminated with exit code 1
blocked as expected

The timeout is expected because a denied packet receives no application response. The final line confirms that the shell deliberately converted this expected failure into a readable result.

Allow One Approved Client

In this step, add an allow policy requiring both an approved namespace and approved Pod label.

NetworkPolicies are additive. The default-deny policy remains in place, while this second policy contributes one allowed source. The selected server Pod may accept TCP port 80 only when the source namespace and source Pod both match.

cd /home/labex/project/network-policy
cat <<'EOF' > allow-approved.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-approved-client, namespace: practice-apps}
spec:
  podSelector: {matchLabels: {app: protected-web}}
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector: {matchLabels: {access: approved}}
          podSelector: {matchLabels: {role: approved}}
      ports:
        - {protocol: TCP, port: 80}
EOF
kubectl apply -f allow-approved.yaml
kubectl exec approved-client -n practice-clients -- wget -qO- --timeout=3 http://protected-web.practice-apps | grep 'Welcome to nginx'
kubectl exec untrusted-client -n practice-clients -- wget -qO- --timeout=3 http://protected-web.practice-apps || echo 'untrusted client blocked'

Because the two selectors are in the same from entry, both must match. Separate entries would mean logical OR.

The approved request should succeed again, while the untrusted request should still time out. That paired result proves the policy is selective rather than simply reopening all ingress.

networkpolicy.networking.k8s.io/allow-approved-client created
<title>Welcome to nginx!</title>
<h1>Welcome to nginx!</h1>
wget: download timed out
command terminated with exit code 1
untrusted client blocked

Read the result as a pair: page content for approved-client, then a timeout for untrusted-client. Seeing only the successful request would not prove the rule is narrow enough.

Summary

You proved namespaces do not isolate traffic by default, applied default-deny ingress, and allowed one client through combined namespace, Pod, protocol, and port criteria.