Securing Kubernetes with Network Policies



In Kubernetes, network communication between Pods is open by default. That means any Pod can talk to any other Pod without restrictions. While this is fine for early development, it becomes a huge security risk in production environments. We need a way to control "who can talk to whom." This is where Kubernetes Network Policies come in.

In this chapter, we'll learn what Network Policies are, why they're important, and how to use them. We'll also build a real-world example securing a frontend app and a MySQL database.

What are Kubernetes Network Policies?

Network Policies are Kubernetes resources that control traffic between Pods and other network endpoints based on rules.

With Network Policies, we can:

  • Allow only specific Pods to communicate with each other.
  • Restrict traffic based on Pod labels, namespaces, and ports.
  • Secure sensitive services like databases.
  • Build "Zero Trust" networking inside the cluster.

Think of them as firewalls for Kubernetes.

Important: Network Policies only take effect if your Kubernetes cluster supports them. Many CNI plugins like Calico, Cilium, Weave Net, and Kube-router support Network Policies.

Why Use Network Policies?

Without Network Policies:

  • Any compromised Pod could move laterally and attack other services.
  • Secrets, databases, and internal APIs are exposed.

With Network Policies:

  • Services only talk to authorized clients.
  • We can limit blast radius in case of a breach.
  • Kubernetes apps become more secure and compliant.

How Network Policies Work

Network Policies define two main types of traffic:

  • Ingress Traffic: Incoming traffic to the Pod.
  • Egress Traffic: Outgoing traffic from the Pod.

Each Policy can:

  • Select Pods by their labels.
  • Allow/deny traffic from specific Pods, namespaces, or IP blocks.
  • Specify allowed ports and protocols (TCP/UDP).

By default:

  • If no Network Policy selects a Pod, all traffic is allowed.
  • If one or more Network Policies select a Pod, the Pod only allows what the policies permit. All other traffic is denied by default.

Prerequisites

Before we proceed, ensure:

  • You have a running Kubernetes cluster (Minikube, KIND, or real cluster).
  • Your cluster has a CNI plugin that supports Network Policies (e.g., Calico, Cilium, or Weave Net).

Note: If you use Minikube, start with Calico like this:

$ minikube start --network-plugin=cni --cni=calico

Setting Up the Environment

Let's first deploy two basic apps to simulate communication.

Create a namespace for isolation:

$ kubectl create namespace secure-app

Output

namespace/secure-app created

Deploy a backend pod:

$ kubectl run backend --image=nginx --namespace=secure-app --labels="app=backend" --port=80

Output

pod/backend created

Expose it:

$ kubectl expose pod backend --port=80 --namespace=secure-app

Output

service/backend exposed

Deploy a frontend pod:

$ kubectl run frontend --image=nginx --namespace=secure-app --labels="app=frontend" --port=80

Output

pod/frontend created

Confirm if the pods are running:

$ kubectl get pods -n secure-app

Output

NAME       READY   STATUS    RESTARTS   AGE
backend    1/1     Running   0          4m51s
frontend   1/1     Running   0          3m59s

Test Pod-to-Pod Communication

By default, frontend can reach backend easily.

Open a shell inside the frontend pod:

$ kubectl exec -n secure-app -it frontend -- /bin/sh

Inside the pod, try to curl the backend service. We should get an HTML response from the nginx server:

# curl backend

Output

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Create a Basic Network Policy (Deny All)

Now let's lock everything down first.

Create a file called deny-all.yaml:

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

What this does:

  • Targets all pods (empty podSelector: {}).
  • Denies all incoming traffic (Ingress) unless explicitly allowed.

Apply it:

$ kubectl apply -f deny-all.yaml

Output

networkpolicy.networking.k8s.io/deny-all created

Test Again

Try to curl backend from frontend again:

# curl backend

Output

curl: (7) Failed to connect to backend port 80: Connection refused

Allow Specific Communication

Now let's allow traffic only from certain Pods.

Create a file called allow-frontend-to-backend.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: secure-app
spec:
  podSelector:
    matchLabels:
      app: backend
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 80

Explanation,

  • Selects pods with label app=backend.
  • Allows ingress from pods labeled app=frontend.
  • Only on TCP port 80.

Apply it:

$ kubectl apply -f allow-frontend-to-backend.yaml

Output

networkpolicy.networking.k8s.io/allow-frontend-to-backend created

Test Again

Inside frontend pod, run:

# curl backend

Output

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Real-World Applications: Securing a Frontend and MySQL Backend

Let's go deeper. Suppose we have:

  • A frontend app (app: frontend) running on port 3000.
  • A MySQL database (app: mysql) running on port 3306.

We want:

  • Only the frontend app to talk to MySQL.
  • No other Pods should access MySQL.

Let's implement it step-by-step.

Deploy MySQL

Create a file called mysql-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "password"
        ports:
        - containerPort: 3306

Apply it:

$ kubectl apply -f mysql-deployment.yaml

Output

deployment.apps/mysql created

Check that the MySQL deployment is running correctly:

$ kubectl get deployments

Output

NAME    READY   UP-TO-DATE   AVAILABLE   AGE
mysql   1/1     1            1           5m42s

Check the pods to ensure MySQL is running:

$ kubectl get pods

Output:

NAME                     READY   STATUS    RESTARTS   AGE
mysql-6ddd846c5d-f6z5q   1/1     Running   0          6m5s

Deploy Frontend App

Create a file called frontend-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  selector:
    matchLabels:
      app: frontend
  replicas: 1
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: node:14
        command: ["node", "-e", "require('http').createServer((req,res)=>res.end('Hello')).listen(3000)"]
        ports:
        - containerPort: 3000

Apply it:

$ kubectl apply -f frontend-deployment.yaml

Output

deployment.apps/frontend created

Confirm if the frontend deployment and pod is running correctly:

$ kubectl get deployments

Output

NAME       READY   UP-TO-DATE   AVAILABLE   AGE
frontend   0/1     1            0           9s
mysql      1/1     1            1           11m
$ kubectl get pods

Output

NAME                        READY   STATUS    RESTARTS   AGE
frontend-58b97ffdc4-lhgvb   1/1     Running   0          2m15s
mysql-6ddd846c5d-f6z5q      1/1     Running   0          13m

Create Network Policy for MySQL

Create a file called mysql-network-policy.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mysql-network-policy
spec:
  podSelector:
    matchLabels:
      app: mysql
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 3306

What this does:

  • Only Pods with label app=frontend can talk to Pods with label app=mysql on port 3306.

Apply it:

$ kubectl apply -f mysql-network-policy.yaml

Output:

networkpolicy.networking.k8s.io/mysql-network-policy created

To confirm the network policy is running:

$ kubectl get networkpolicies

Output

NAME                   POD-SELECTOR   AGE
mysql-network-policy   app=mysql      40s

Egress Control (Optional But Important)

Ingress controls incoming traffic. Egress controls outgoing traffic.

Let's create a policy to restrict pods from accessing external internet:

Create a file called deny-egress.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-egress
  namespace: secure-app
spec:
  podSelector: {}
  policyTypes:
  - Egress

Apply it:

$ kubectl apply -f deny-egress.yaml

Output:

networkpolicy.networking.k8s.io/deny-egress created

Now inside the pod, trying to curl google.com will fail:

$ curl google.com

Output

curl: (7) Failed to connect to google.com port 80: Connection refused

Points to Note

Keep the following points in mind while applying Network Policies in Kubernetes -

  • Network Policy is "allow only" once applied. Everything else is denied by default.
  • Policy enforcement depends on the CNI plugin. Make sure your CNI supports it.
  • Egress rules are critical to prevent Pods from talking to the internet.
  • Use labels carefully. Policies depend heavily on labels.
  • Start simple, then gradually build complex policies.

Best Practices for Network Policies

Here is a set of best practices for securing Kubernetes with network policies -

  • Apply "default deny all ingress" and "default deny all egress" first.
  • Allow traffic explicitly between required services.
  • Use namespaces to further isolate environments (dev, staging, prod).
  • Test policies carefully before rolling out to production.
  • Monitor traffic to identify missing or redundant policies.

Conclusion

By using Network Policies, we add a strong layer of security to our Kubernetes applications. It's like building walls and gates inside the cluster. We control traffic precisely and prevent unwanted access.

In this chapter, we learned how Network Policies work, how to apply them, and showcased a real-world example protecting a frontend and MySQL backend. The next step is to practice by writing your own policies and tightening security in your Kubernetes clusters.