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: workerand then run kind create to kindly create the cluster
kind create cluster --name cni-cluster --config config.yamlso 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.0Modern, 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 10mHere, 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-clusterthen 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/16kind create cluster --name cni-cluster --config config.yamlTo install Calico we run
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yamlNow 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 39sNow 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 17mSo 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=ClusterIPsimilarly for database
kubectl expose pod database --name=database-svc --port=5432 --target-port=5432 --type=ClusterIPTo 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 31slet'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: 5432To 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 12sSo 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 outNow 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.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.