Linkerd is a lightweight CNCF service mesh that provides automatic mTLS between injected pods, fine-grained authorization policies (Server, HTTPRoute, AuthorizationPolicy), and observability. Unlike Istio's envoy-based architecture, Linkerd uses a purpose-built Rust proxy (linkerd2-proxy). Security testing Linkerd focuses on validating that mTLS is actually enforced (not just opportunistic), that authorization policies have no gaps, that uninjected pods cannot communicate with injected services, that the control plane identity issuer is protected, and that multicluster links do not expose cross-cluster credentials. This guide covers systematic Linkerd security assessment.
# Verify which pods have Linkerd proxies injected
kubectl get pods -A -o json 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
for item in r['items']:
ns = item['metadata']['namespace']
name = item['metadata']['name']
containers = [c['name'] for c in item['spec'].get('containers',[])]
injected = 'linkerd-proxy' in containers
if not injected and not ns.startswith('kube-') and ns != 'linkerd':
print(f'NOT INJECTED: {ns}/{name}')
"
# Use linkerd CLI to check proxy injection status
linkerd check --proxy 2>/dev/null | grep -E "FAIL|WARN|OK"
# Verify mTLS is active on a specific deployment
linkerd viz tap deploy/payment-service -n production 2>/dev/null | \
grep -E "tls|plaintext" | head -20
# Look for: tls=true (mTLS active) vs plaintext (no mTLS)
# Check mTLS via linkerd-multicluster or linkerd edges
linkerd viz edges deployment -n production 2>/dev/null | \
column -t | grep -v "SECURED"
# Any row without "SECURED" = plaintext traffic within the mesh
# Verify trust anchors and identity certificates
linkerd check --pre 2>/dev/null | grep -E "identity|trust|certificate"
kubectl get secret linkerd-identity-issuer -n linkerd \
-o json 2>/dev/null | python3 -c "
import json,sys,base64
s = json.load(sys.stdin)
for k,v in s.get('data',{}).items():
print(f'{k}: {base64.b64decode(v).decode()[:100]}')
"
# Linkerd enforces mTLS between injected pods — but by default,
# uninjected pods can still reach injected services over plaintext
# unless Server + AuthorizationPolicy resources explicitly block them
# Test: deploy an uninjected pod and attempt to reach an injected service
kubectl run attacker --image=curlimages/curl --restart=Never \
--annotations='linkerd.io/inject=disabled' \
-n production -- sleep 3600 2>/dev/null
# From the uninjected pod, attempt to call an injected service
kubectl exec attacker -n production -- \
curl -s http://payment-service.production.svc.cluster.local:8080/health 2>/dev/null
# If this returns data: uninjected pod can reach the service over plaintext
# This means mTLS provides encryption but NOT authentication/authorization
# for uninjected sources unless Server resources are configured
# Test: namespace without automatic injection annotation
kubectl get namespace production -o json 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
annotations = r.get('metadata',{}).get('annotations',{})
inject = annotations.get('linkerd.io/inject','disabled')
print(f'Namespace injection: {inject}')
# 'enabled' = all pods auto-injected
# 'disabled' or missing = opt-in only (gaps possible)
"
# Linkerd policy: Server (defines a port), HTTPRoute (HTTP-level rules),
# AuthorizationPolicy (who can access via which route)
# Test for gaps in policy coverage
# List Server resources (define protected ports)
kubectl get servers -A 2>/dev/null | grep -v "^linkerd"
# Ports without Server resources have no Linkerd policy enforcement
# Check default behavior when no policy defined for a port
# Default is 'all-unauthenticated' — allows all traffic without mTLS requirement
kubectl get server -n production payment-server -o json 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
spec = r.get('spec',{})
print(f'Port: {spec.get(\"port\",\"?\")}')
print(f'PodSelector: {spec.get(\"podSelector\",{})}')
"
# List AuthorizationPolicies — check for overly broad rules
kubectl get authorizationpolicies -A 2>/dev/null
kubectl get authorizationpolicies -n production -o json 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
for p in r.get('items',[]):
name = p['metadata']['name']
spec = p.get('spec',{})
clients = spec.get('requiredAuthenticationRefs',[])
print(f'Policy: {name}')
if not clients:
print(' WARNING: no authentication required — allows all clients')
"
# Check for MeshTLSAuthentication allowing all service accounts in namespace
kubectl get meshtlsauthentications -A 2>/dev/null | grep "all"
linkerd.io/inject: enabled on all application namespaces — prevents uninjected podslinkerd-identity-issuer in the linkerd namespacelinkerd upgrade --identity-issuer-certificate-file| Security Test | Method | Risk |
|---|---|---|
| Uninjected pod reaches injected service | Deploy pod with inject=disabled, curl injected service | High |
| Ports with no Server resource | Compare service ports to Server resources — gaps = unprotected | High |
| AuthorizationPolicy allows all clients | Check for empty requiredAuthenticationRefs in policies | High |
| Identity issuer secret readable | kubectl get secret linkerd-identity-issuer -n linkerd | High |
| Viz dashboard without authentication | curl :8084 (default Linkerd viz port) returns UI | Medium |
| Namespace missing auto-injection | Check namespace annotations for inject=enabled | Medium |
Ironimo tests Linkerd deployments for mTLS enforcement gaps, uninjected pod bypass, authorization policy coverage, identity issuer exposure, control plane API security, multicluster credential exposure, and viz dashboard access — covering the complete Linkerd attack surface.
Start free scan