Linkerd’s network authentication policies are the bedrock of its zero-trust security model, granting services access to each other based on their identity, not just their IP address.

Let’s watch this in action. Imagine you have two services, frontend and backend, running in Kubernetes.

# frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: nginx:latest # Replace with your actual frontend image
        ports:
        - containerPort: 80

---
# backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: curlimages/curl:latest # Replace with your actual backend image
        command: ["sleep", "3600"] # Keep container running
        ports:
        - containerPort: 8080 # Assuming backend listens on 8080

First, we need to enable the Linkerd policy controller and then define a policy.

# Ensure Linkerd is installed with the policy controller enabled
linkerd install --set policyController.enabled=true | kubectl apply -f -

# Create a Kubernetes namespace for our demo
kubectl create ns demo

# Apply the deployments
kubectl apply -f frontend-deployment.yaml -n demo
kubectl apply -f backend-deployment.yaml -n demo

# Add the demo namespace to Linkerd's control plane
linkerd inject --enable-per-namespace-defaults -n demo | kubectl apply -f -

Now, by default, Linkerd’s policy controller will deny all traffic between services in the demo namespace because no explicit authorization policy exists. If you try to curl from frontend to backend right now, it will fail.

# Get the pod names
FRONTEND_POD=$(kubectl get pods -n demo -l app=frontend -o jsonpath='{.items[0].metadata.name}')
BACKEND_POD=$(kubectl get pods -n demo -l app=backend -o jsonpath='{.items[0].metadata.name}')

# Attempt to curl from frontend to backend (will likely fail)
kubectl exec -n demo $FRONTEND_POD -- curl -v http://backend:8080 # Replace 8080 if your backend uses a different port

You’ll see a connection refused or a timeout. This is Linkerd’s policy controller doing its job: blocking unauthorized access.

To allow frontend to talk to backend, we create a NetworkAuthenticationPolicy.

# frontend-to-backend-policy.yaml
apiVersion: policy.linkerd.io/v1alpha1
kind: NetworkAuthenticationPolicy
metadata:
  name: frontend-can-talk-to-backend
  namespace: demo
spec:
  target:
    selector:
      matchLabels:
        app: backend # This policy applies to the backend service
  ports:
    - port: 8080 # The port on the backend service we want to allow access to
  sources:
    - podSelector:
        matchLabels:
          app: frontend # This policy allows traffic originating from the frontend service

Apply this policy:

kubectl apply -f frontend-to-backend-policy.yaml -n demo

Now, if you run the kubectl exec command again, it should succeed.

kubectl exec -n demo $FRONTEND_POD -- curl http://backend:8080

This NetworkAuthenticationPolicy specifies that any pod in the demo namespace with the label app: frontend is allowed to connect to any pod in the demo namespace with the label app: backend on port 8080. Linkerd intercepts the connection attempt and, seeing a valid policy, permits it.

The core problem this solves is moving from coarse-grained network segmentation (e.g., firewall rules based on IP addresses) to fine-grained, identity-based authorization. Instead of saying "allow traffic from subnet X to subnet Y," you can say "allow the checkout service to call the payment service." This is crucial for zero-trust architectures where no component is inherently trusted.

Internally, Linkerd’s policy controller watches for NetworkAuthenticationPolicy resources. When a request arrives at a Linkerd-injected pod (the source), the proxy checks if there’s an applicable NetworkAuthenticationPolicy for the destination service and source identity. If a policy matches and permits the connection, the proxy allows it; otherwise, it’s denied. The target in the policy refers to the pods that receive traffic, and sources refers to the pods that initiate traffic.

The most surprising part is how granular you can get with ports. You can create a policy that allows a frontend to talk to a backend on port 80, but not on port 9000, even if both are exposed by the same backend service. This level of control prevents accidental or malicious access to sensitive internal ports.

The next step in hardening your service access is exploring AuthorizationPolicy which allows you to control access based on HTTP methods, paths, and headers, providing even deeper authorization beyond just network connectivity.

Want structured learning?

Take the full Linkerd course →