K3s Network Policies let you define granular firewall rules for your Kubernetes pods, but most people think they’re just for blocking traffic, when they’re actually a powerful tool for allowing specific traffic and enforcing least-privilege network access.

Let’s see it in action. Imagine you have a frontend deployment and a backend deployment. By default, frontend can talk to backend without any restrictions.

# frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
# backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: api
        image: curlimages/curl:latest
        command: ["sleep", "3600"] # Keep the pod running
        ports:
        - containerPort: 80 # Port for potential internal communication, though curl doesn't strictly need it

If you deploy these, you can exec into a frontend pod and curl a backend pod’s IP address:

kubectl exec -it <frontend-pod-name> -- curl <backend-pod-ip>

Now, let’s introduce a Network Policy to restrict this. By default, K3s (and Kubernetes in general) doesn’t have any Network Policies applied, meaning all pods can communicate freely with all other pods within the cluster. Network Policies are opt-in.

Here’s a policy that denies all ingress traffic to the backend pods by default.

# deny-all-ingress-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress-backend
  namespace: default # Assuming your pods are in the default namespace
spec:
  podSelector:
    matchLabels:
      app: backend # This policy applies to pods with the label app=backend
  policyTypes:
  - Ingress # This policy defines rules for incoming traffic
  # No 'ingress' field means no traffic is allowed in

Apply this: kubectl apply -f deny-all-ingress-backend.yaml.

Now, if you try to curl from frontend to backend again, it will fail.

kubectl exec -it <frontend-pod-name> -- curl <backend-pod-ip>
# Output will likely be: curl: (7) Failed to connect to <backend-pod-ip> port 80: Connection refused

This is because the policyTypes: Ingress without any ingress rules explicitly states "allow no ingress traffic."

To fix this and allow only frontend to talk to backend, we create a new policy.

# allow-frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: backend # Apply to backend pods
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend # Allow traffic from pods labeled app=frontend
    ports:
    - protocol: TCP
      port: 80 # Allow traffic on TCP port 80

Apply this: kubectl apply -f allow-frontend-to-backend.yaml.

Now, frontend pods can successfully reach backend pods on port 80.

kubectl exec -it <frontend-pod-name> -- curl <backend-pod-ip>
# Output will be empty or show nginx welcome page if backend served one, indicating success.

This policy works by selecting the target pods (backend in this case) and then defining ingress rules. The from section specifies the sources of allowed traffic. Here, we’re saying traffic is allowed from any pod that has the label app: frontend. The ports section further refines this, specifying that only traffic on TCP port 80 is permitted.

The mental model here is that Network Policies are additive but also default-deny once a policy is applied. If a pod has any Network Policy selecting it that defines ingress rules, then only traffic matching those rules is allowed. If a pod has any Network Policy selecting it that defines egress rules, then only traffic matching those rules is allowed. If a policy specifies policyTypes: [Ingress] and no ingress rules, it effectively denies all ingress. If it specifies policyTypes: [Egress] and no egress rules, it denies all egress.

A common misconception is that you need to explicitly deny traffic. In reality, by applying a policy that allows specific traffic, you implicitly deny everything else. The key is that Network Policies are declarative and stateful at the pod level. Once a pod is selected by any Network Policy, its network access is governed by the union of all policies selecting it. If a pod is not selected by any Network Policy, it has unrestricted ingress and egress.

When you define a Network Policy, you’re not just saying "block this," you’re saying "this is the only traffic allowed." This is crucial for security. For example, if you have a database pod, you want to ensure only your application pods can connect to it, and ideally only on the specific database port.

Consider a scenario where you have a database pod and frontend and admin pods.

# database-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: database
  template:
    metadata:
      labels:
        app: database
    spec:
      containers:
      - name: postgres
        image: postgres:latest
        ports:
        - containerPort: 5432
# admin-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: admin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admin
  template:
    metadata:
      labels:
        app: admin
    spec:
      containers:
      - name: busybox
        image: busybox:latest
        command: ["sleep", "3600"]

And you want only frontend and admin to connect to database on port 5432.

# allow-app-to-database.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-database
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend # Allow from frontend
    - podSelector:
        matchLabels:
          app: admin # Allow from admin
    ports:
    - protocol: TCP
      port: 5432 # On port 5432

Apply this: kubectl apply -f allow-app-to-database.yaml.

Now, frontend and admin pods can connect to database on port 5432. Any other pod trying to connect will be blocked. If you try to connect from a frontend pod to database on port 80 (where it’s not listening), it will also fail.

The most surprising thing about Network Policies is how they interact with namespaces. By default, podSelector and namespaceSelector in an ingress rule are evaluated independently. If you specify podSelector but no namespaceSelector, the podSelector matches pods in the same namespace as the policy. If you want to allow traffic from pods in a different namespace, you must specify a namespaceSelector. Without it, a policy targeting pods in namespace-a won’t be able to receive traffic from pods in namespace-b, even if the podSelector matches.

The next concept you’ll encounter is egress policies, which work on the same principles but control outbound traffic from pods.

Want structured learning?

Take the full K3s course →