Linkerd’s policy controller is the gatekeeper that enforces zero-trust security by dictating which services can talk to each other and what they can do.
Let’s see it in action. Imagine we have two services: frontend and backend. By default, with no policies, frontend can talk to backend without issue.
Here’s a kubectl command to list the services in our default namespace:
kubectl get svc -n default
Output might look like this:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend ClusterIP 10.43.10.123 <none> 80/TCP 5m
backend ClusterIP 10.43.20.456 <none> 8080/TCP 5m
Now, let’s deploy a simple curl pod to act as our frontend client:
apiVersion: v1
kind: Pod
metadata:
name: frontend-client
namespace: default
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ["sleep", "3600"]
Apply this with kubectl apply -f frontend-client.yaml.
From within the frontend-client pod, we can reach the backend service:
kubectl exec -it frontend-client -n default -- curl http://backend:8080
If the backend service responds with something like "Hello from backend!", this confirms uninhibited communication.
Now, let’s introduce Linkerd’s policy controller. First, ensure Linkerd is installed with the policy controller enabled. If you installed Linkerd using the default linkerd install | kubectl apply -f -, the policy controller is likely already running. You can verify with:
kubectl get pods -n linkerd | grep policy
You should see a linkerd-policy pod running.
The core of Linkerd’s policy is the NetworkPolicy resource. This isn’t a standard Kubernetes NetworkPolicy; it’s Linkerd’s custom resource definition (CRD) specifically for fine-grained authorization.
Let’s create a policy that denies all traffic by default. This is the foundation of zero-trust: assume no access until explicitly granted.
apiVersion: policy.linkerd.io/v1alpha1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: default
spec:
# Applies to all pods in the 'default' namespace
target:
selector: {}
# No servers are allowed to receive traffic
servers: []
Apply this: kubectl apply -f deny-all.yaml.
After applying this policy, try the curl command again from frontend-client:
kubectl exec -it frontend-client -n default -- curl http://backend:8080
This time, you’ll likely get a timeout or a connection refused error. This is because our deny-all policy, by default, blocks all ingress traffic to all pods in the default namespace. The policy controller intercepts the request and, finding no explicit allowance, denies it.
Now, let’s explicitly allow the frontend service to communicate with the backend service. We need a policy that targets the backend service and specifies which clients (servers in Linkerd’s terminology, referring to the source of the traffic) are allowed to reach it.
apiVersion: policy.linkerd.io/v1alpha1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: default
spec:
# This policy applies to the 'backend' service
target:
name: backend
namespace: default
# Define which servers (clients) are allowed to connect to the backend
servers:
- port: 8080 # The port on the backend service
# Allow traffic from pods with the label 'app: frontend'
# Linkerd uses the pod's own labels to identify the client.
# This is a key aspect of its identity-based authorization.
clients:
- namespace: default
selector:
matchLabels:
app: frontend
Apply this: kubectl apply -f allow-frontend-to-backend.yaml.
To make this policy effective, our frontend pods (or the frontend-client pod in this example) must have the label app: frontend. If frontend-client doesn’t have this label, you’d need to add it. For a real frontend deployment, you’d ensure its Deployment manifest includes labels: { app: frontend }.
Let’s assume our frontend-client pod has the app: frontend label. If not, you’d edit its pod spec or recreate it with the label.
Now, try the curl command again:
kubectl exec -it frontend-client -n default -- curl http://backend:8080
This time, the request should succeed, and you’ll see the "Hello from backend!" message. The policy controller saw that the request originated from a pod labeled app: frontend in the default namespace, and it was destined for the backend service on port 8080. Since our allow-frontend-to-backend policy explicitly permits this, the traffic is allowed.
The mental model here is that Linkerd’s policy controller operates on a "deny by default" principle. You define NetworkPolicy resources that specify allowances. Any traffic not explicitly allowed by a NetworkPolicy is implicitly denied.
The target in a NetworkPolicy refers to the destination service. The servers block within target specifies the ports on that destination service. The clients block within servers defines who is allowed to connect to that specific port on the target service. Linkerd uses the Kubernetes labels of the source pod (the client) to match against the selector in the clients block. This is how Linkerd enforces authorization based on the identity (defined by labels) of the communicating services.
A crucial, often overlooked, aspect is how Linkerd’s policy controller integrates with Kubernetes Service accounts. While NetworkPolicy selectors often use pod labels, Linkerd can also authorize based on the Service Account a pod is running as. This provides a more robust identity for authorization, especially in complex environments where pod labels might be less stable or more general. To leverage this, you would typically see clients specified with a serviceAccount field, matching the serviceAccountName of the client pod, rather than just pod labels.
The next concept you’ll likely encounter is implementing egress policies, controlling what services can reach outside the cluster.