RSS Amplifier

Ara's blog · May 21, 2026

Kubernetes Network Policies (K8s NetworkPolicies)

0
Sign in to vote or save

martirosyan.fr

To illustrate, consider a classic three-tier application hosted on a Kubernetes cluster, consisting of a frontend, a backend, and a database pods. In a production scenario, the frontend communicates with the backend, and the backend handles requests to and from the database. Because the backend acts as an intermediary, there is typically no architectural need for the frontend pods to communicate directly with the database pods.

To demonstrate network policies, we need a Container Network Interface (CNI) plugin that supports policy enforcement. While Calico and Cilium are the most popular options, my current local development environment uses a kind cluster running on Docker. By default, kind comes with its own minimal CNI, kindnet, which has limitations and does not support NetworkPolicy resources. Therefore, I will install the Calico add-on to enable network policy capabilities.

To begin, let's start with a minimal three-node setup using a config.yml file.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker

and then run kind create to kindly create the cluster

kind create cluster --name cni-cluster --config config.yaml

so this way we get one contol-plane and two worker nodes

> kubectl get nodes
NAME                        STATUS   ROLES           AGE     VERSION
cni-cluster-control-plane   Ready    control-plane   7m49s   v1.35.0
cni-cluster-worker          Ready    <none>          7m38s   v1.35.0
cni-cluster-worker2         Ready    <none>          7m38s   v1.35.0

Modern, advanced CNIs (like Calico or Cilium) rely on a DaemonSet to run a background pod on every node. This daemon's job is to watch the Kubernetes API, manage routing tables and update security/network policies across the cluster. Normally they live in kube-system namespace, so if we run to get the daemon sets in kube-system

> kubectl get ds -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kindnet      3         3         3       3            3           kubernetes.io/os=linux   10m
kube-proxy   3         3         3       3            3           kubernetes.io/os=linux   10m

Here, we see the default kindnet CNI that is automatically provisioned during cluster installation. As mentioned earlier, we want to disable kindnet and install Calico instead. We can achieve this by disabling the default CNI within our config.yaml file. First things first, let's delete the existing cluster.

kind delete cluster -n cli-cluster

then update the config.yml and run the kind create again

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
networking:
  disableDefaultCNI: true
  podSubnet: 192.168.0.0/16
kind create cluster --name cni-cluster --config config.yaml

To install Calico we run

kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yaml

Now the same kubectl get ds -n kube-system results in

NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
calico-node   3         3         0       3            0           kubernetes.io/os=linux   16s
kube-proxy    3         3         3       3            3           kubernetes.io/os=linux   39s

Now let's create the pods, let's start with a node image for angular

 kubectl run frontend --image=node --port 4200 --command -- sh -c "tail -f /dev/null"

We use this specific command arguments option to keep the container's primary process active. Without it, the container will finish its execution and transition to a 'Completed' status rather than staying in a 'Running' state. In the same way, let's spin up the pod for our Spring Boot application using a similar imperative approach.

kubectl run backend --image=eclipse-temurin:21-jre-alpine --port 8080 \
--command -- sh -c "tail -f /dev/null"

Finally for database

kubectl run database \
  --image=postgres \
  --port=5432 \
  --env="POSTGRES_USER=myuser" \
  --env="POSTGRES_PASSWORD=mypassword" \
  --env="POSTGRES_DB=mydb"

So we end up with the three pods shown in the picture.

kubectl get pods
NAME       READY   STATUS    RESTARTS   AGE
backend    1/1     Running   0          14m
database   1/1     Running   0          33s
frontend   1/1     Running   0          17m

So far, so good. Now, how do we get our pods to talk to each other? Services! That's right—we need to create ClusterIP services, because that is how our pods communicate within the cluster. Let's create one for the backend pod and one for the database.

 kubectl expose pod backend --name=backend-svc --port=8080 --target-port=8080 --type=ClusterIP

similarly for database

 kubectl expose pod database --name=database-svc --port=5432 --target-port=5432 --type=ClusterIP

To check the newly created services we can run

kubectl get services
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
backend-svc    ClusterIP   10.96.59.196    <none>        8080/TCP   8m8s
database-svc   ClusterIP   10.96.135.137   <none>        5432/TCP   31s

let's try now to connect from frontend to database

kubectl exec frontend -- sh -c '
apt update && \
apt install postgresql-client -y && \
env PGPASSWORD=mypassword \
psql -h database-svc -U myuser -d mydb -c "\l"
'

Here you go! All the databases are listed right on your screen.

                                                 List of databases
   Name    |
-----------+--
 mydb      |
 postgres  |
 template0 |
 template1 |
           |
(4 rows)

Similarly, we can connect to the database from any other pod, such as the backend. There are no restrictions. However, what we actually want is to restrict connections based on pod labels. Finally, we arrive at the main topic of this article: NetworkPolicies. To restrict access between Pods, we define a NetworkPolicy of type Ingress. This ensures that any Pods that do not match the specified pod selector are not allowed to connect to our database Pod. So let's create the netpol.yml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: access-database
spec:
  podSelector:
    matchLabels:
      run: database
  policyTypes:
    - Ingress
  ingress:
    - from:
      - podSelector:
          matchLabels:
            run: backend
      ports:
        - protocol: TCP
          port: 5432

To put it in nutshell it says that only backend Pods are allowed to connect to database Pods on port 5432. Everyone else is denied. Now let's apply the netpol.yml and then run get netpol

kubectl apply -f netpol.yml
kubectl get netpol
NAME              POD-SELECTOR   AGE
access-database   run=database   12s

So the access-database NetworkPolicy applies to all pods labeled run=database, and access is allowed only from pods labeled run=backend. Let’s see if that’s correct. Since the postgresql-client is already installed on the frontend pod, we can simply run the following command.

kubectl exec frontend -- sh -c '
env PGPASSWORD=mypassword \
psql -h database-svc -U myuser -d mydb -c "\l"
'

You will notice a continuously blinking cursor that hangs for a long time and never establishes a connection.

psql: error: connection to server at "database-svc" (10.96.135.137), port 5432 failed: Connection timed out

Now let’s try to connect from the backend Pod, and since our backend Pod uses an image based on Alpine Linux rather than Debian/Ubuntu, we use apk instead of apt to install the postgresql-client

kubectl exec backend -- sh -c '
apk add --no-cache postgresql-client && \
env PGPASSWORD=mypassword \
psql -h database-svc -U myuser -d mydb -c "\l"
'

Congratulations! You’ve managed to connect from the backend, but not from the frontend.

Read the original on martirosyan.fr

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.