Polyaxon v3 is coming →

Kubernetes network policies: an actionable guide

Use Kubernetes network policies to control pod traffic, restrict lateral movement, and make cluster networking less permissive by default.

July 8, 2022by Polyaxon

Kubernetes networking is permissive unless you make it stricter. In many clusters, pods can talk to far more things than they need. Network policies give teams a way to narrow that surface.

For ML workloads, that matters because jobs often touch data stores, artifact stores, registries, and internal APIs. A compromised or misconfigured pod should not get a free tour of the cluster.

Why do you need network policies?

Without applicable NetworkPolicies, Pods are non-isolated for both ingress and egress. Routing, firewalls, and other controls can still affect connectivity, but a namespace boundary alone does not block Pod traffic. Apply policies to reduce communication to the paths your applications need.

Network policies restrict traffic between selected Pods and other endpoints. For a three-tier application, a browser-facing frontend may need the backend API, while only the backend needs the database. Restricting that path reduces the access available if the frontend is compromised. Network policy complements application authentication and authorization; it does not replace them.

Envision an infrastructure with three services: a frontend, a backend, and a database. With an open network, a hacker needs only to get access to any service, from which they can then access the database. If you lock down network access so that only the backend can access the database, it's no longer enough for the hacker to get access to the frontend service - they need to get access to the backend if they want access to the database. This is one of the most important reasons for implementing strict network policies.

How do network policies work?

The cluster's networking implementation must support and enforce NetworkPolicy. Creating a policy object on a cluster without an enforcing implementation has no effect. Check your CNI or managed Kubernetes documentation before running this example; the Kubernetes NetworkPolicy documentation describes the model and its prerequisites.

Once you've got the network plug-in set up, you can define your network policies. They're defined just like any other resource you work with in Kubernetes, and they require standard fields such as apiVersion, kind, metadata, and spec. Next, you either define one of the two forms of isolation possible in Kubernetes or specify both, as ingress and egress can be defined together or separately. It's worth noting that even though they may be specified in the same file, they're still referred to as separate forms of isolation.

Once you've defined your manifest file, it's a simple matter of using kubectl apply to apply it. When creating your network policy, you can choose your ingress selectors in three different ways: CIDR (selection of IPs), podSelector, and namespaceSelector. This gives you very granular control over where traffic is allowed to come from.

Policies are additive: the allowed traffic is the union of applicable rules, regardless of creation order. There is no explicit deny rule that overrides another policy's allow rule. An additional broad allow policy can therefore reopen access you intended to restrict.

For a connection between two Pods, the source's egress rules and destination's ingress rules must both permit it when those directions are isolated. Reply traffic for an allowed connection is permitted; you do not need a separate rule for its response packets.

Implementing a network policy

To get a solid idea of how network policies can be implemented, it's best to see an example.

Use a disposable namespace in a cluster with NetworkPolicy enforcement. The frontend, backend, and database below are all represented by NGINX servers so the experiment only requires HTTP. The database is a stand-in, not a real database deployment. Other cluster-wide policies can affect the results, so establish baseline connectivity first.

kubectl config current-context
kubectl create namespace network-policy-demo

Start by creating a file called frontend.yaml and paste the following into it:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
  labels:
    app: frontend
    component: server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
      component: server
  template:
    metadata:
      labels:
        app: frontend
        component: server
    spec:
      containers:
        - name: nginx
          image: nginx:stable
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
    component: server
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

This manifest creates a Deployment with one NGINX Pod and a Service that selects it. The component: server selector keeps the probe Pods added below out of the Service's endpoints. The stable tag is convenient for this disposable example; use approved image versions or digests for maintained deployments.

Save this file in an empty directory, and create two copies of it. Name one of them backend.yaml, and change all occurrences of frontend to backend. Name the other database.yaml, and change all occurrences of frontend to database. Deploy these three files and wait for their Pods:

kubectl apply --namespace network-policy-demo -f frontend.yaml -f backend.yaml -f database.yaml
kubectl rollout status --namespace network-policy-demo deployment/frontend-deployment --timeout=120s
kubectl rollout status --namespace network-policy-demo deployment/backend-deployment --timeout=120s
kubectl rollout status --namespace network-policy-demo deployment/database-deployment --timeout=120s

Create two diagnostic Pods with BusyBox's built-in wget. Their app labels represent a frontend and backend source, without installing tools inside the application containers:

kubectl run frontend-probe --namespace network-policy-demo --image=busybox:1.37 --labels=app=frontend,component=probe --command -- sleep 3600
kubectl run backend-probe --namespace network-policy-demo --image=busybox:1.37 --labels=app=backend,component=probe --command -- sleep 3600
kubectl wait --namespace network-policy-demo --for=condition=Ready pod/frontend-probe pod/backend-probe --timeout=120s
kubectl exec --namespace network-policy-demo frontend-probe -- wget -T 5 -qO- http://backend
kubectl exec --namespace network-policy-demo frontend-probe -- wget -T 5 -qO- http://database
kubectl exec --namespace network-policy-demo backend-probe -- wget -T 5 -qO- http://database

All three requests should return the NGINX welcome page before the policy is applied. If they fail, resolve DNS, readiness, routing, and any pre-existing policy restrictions before attributing a later failure to this policy.

To do this, save the following to a file called access-backend-database.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-from-backend
  namespace: network-policy-demo
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 80

This policy isolates ingress to Pods labeled app: database in network-policy-demo. It allows TCP port 80 from Pods labeled app: backend in the same namespace: a podSelector alone in a peer does not select other namespaces. No egress isolation is added by this policy. Labels are selectors rather than authenticated identities, so control who can create or relabel Pods in trusted namespaces.

The frontend is absent because the policy lists allowed traffic. It is not an explicit denial of the frontend: another policy selecting the database could still allow that source. Audit all policies that select the destination when troubleshooting an unexpected successful connection.

Apply the policy and repeat both negative and positive checks:

kubectl apply -f access-backend-database.yaml
kubectl exec --namespace network-policy-demo frontend-probe -- wget -T 5 -qO- http://database
kubectl exec --namespace network-policy-demo backend-probe -- wget -T 5 -qO- http://database
kubectl exec --namespace network-policy-demo frontend-probe -- wget -T 5 -qO- http://backend

After the networking implementation has applied the policy, the expected results are:

RequestBefore the policyAfter the policy
Frontend probe → databaseWelcome pageConnection blocked, commonly a timeout
Backend probe → databaseWelcome pageWelcome page
Frontend probe → backendWelcome pageWelcome page

A blocked connection alone is insufficient evidence: the backend-to-database request confirms that the destination is healthy and the allowed path remains usable. If the frontend request still succeeds, check policy enforcement, labels, namespace, and other allow policies. If you later isolate egress too, include the required DNS and dependency paths in that design.

This is a simple example of how a network policy that restricts traffic between services can be implemented. You can make stricter policies with namespace selectors and IP ranges. Policies can be used to lock down your entire cluster, ensuring that only approved paths can be used between services.

Further options

What you've seen in the previous section is an example of a simple network policy. You can get more specific with selectors such as namespaceSelector and ipBlock. Combining namespaceSelector and podSelector in the same peer requires both selectors to match; placing them in separate peers permits either. That distinction matters when allowing one application's Pods from another namespace.

You can also target a range of ports using endPort, provided the networking implementation supports it. The following fragment would widen the example's allowed TCP ports from 80 through 8088; only add such a range when the destination needs it:

ingress:
  - from:
      - podSelector:
          matchLabels:
            app: backend
    ports:
      - protocol: TCP
        port: 80
        endPort: 8088

When finished, remove the disposable namespace and all the resources created for this walkthrough:

kubectl delete namespace network-policy-demo

Conclusion

Network policies are a practical way to reduce unnecessary pod-to-pod and pod-to-service access. Start with the traffic the workload actually needs, then deny the rest.

For ML workloads, that means being explicit about data stores, artifact stores, registries, tracking APIs, and service endpoints. Polyaxon centralizes many of those connections, but the cluster network still needs real boundaries.