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.