Introduction to Linkerd Service Mesh Security
Linkerd is a lightweight, open-source service mesh designed to provide observability, reliability, and security to cloud-native applications without compromising performance. While its ease of installation often leads teams to deploy it quickly, the default configuration is intentionally permissive to ensure smooth adoption. Security hardening is the process of progressively tightening these defaults to achieve a zero-trust networking posture, where every service-to-service communication is authenticated, authorized, and encrypted.
This tutorial walks you through the complete security hardening journey for Linkerd — from understanding the built-in security primitives to implementing advanced authorization policies and operational best practices. Every example is drawn from real-world Kubernetes deployments and assumes you have Linkerd installed on your cluster.
What is Linkerd Security Hardening?
Security hardening in Linkerd refers to the deliberate configuration of mutual TLS enforcement, authorization policies, control plane protection, and certificate management to minimize the attack surface of your service mesh. It transforms Linkerd from a transparent communication layer into a strict security enforcement point that validates every request flowing through the mesh. Hardening involves moving from the default "permissive" mTLS mode to "strict" mode, defining fine-grained access rules between services, and ensuring that the control plane itself is protected against unauthorized access.
Why Security Hardening Matters
In modern distributed systems, the network boundary is no longer a reliable security perimeter. Microservices communicate directly across nodes, clusters, and even clouds, bypassing traditional firewalls. Without service mesh security hardening, any pod can call any other pod, TLS certificates may be accepted without proper validation, and the identity of services remains unverified. Hardening addresses these risks by:
- Eliminating unauthorized cross-service calls — preventing lateral movement within the cluster
- Ensuring encryption in transit — guaranteeing that all traffic is encrypted with validated certificates
- Enforcing identity-based access control — making service identity the sole basis for authorization, not network location
- Protecting the control plane — preventing tampering with mesh configuration and policy
- Meeting compliance requirements — satisfying PCI-DSS, HIPAA, SOC 2, and other regulatory standards
Core Security Features in Linkerd
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before diving into hardening procedures, it's essential to understand the three foundational security pillars that Linkerd provides out of the box. These features are always active in some form, but their enforcement level determines your actual security posture.
Mutual TLS (mTLS)
Linkerd automatically provisions TLS certificates to each pod through its identity system. These certificates are used to establish mutual TLS connections between services, where both the client and server validate each other's identity. By default, Linkerd runs in permissive mode, meaning it accepts both mTLS and plain TCP traffic. This allows gradual adoption without breaking existing connections. The identity system is backed by a root certificate stored in a Kubernetes Secret, and each pod receives a certificate signed by an intermediate CA that rotates regularly.
Under the hood, the linkerd-proxy sidecar container handles all TLS operations transparently. The proxy intercepts outbound traffic, initiates mTLS handshakes with destination proxies, and verifies the server's certificate against the trusted root. Simultaneously, inbound traffic is decrypted and authenticated before reaching the application container.
Identity-Based Authorization
Beyond encryption, Linkerd supports fine-grained authorization policies that control which services can communicate with each other. These policies are expressed through Kubernetes Custom Resources and are enforced by the proxy. Authorization works on the principle of service identity — not IP addresses or labels — making it resilient to pod churn, IP reassignments, and namespace changes. The identity of a service is derived from its Kubernetes ServiceAccount, and authorization rules reference these identities directly.
Authorization policies can restrict traffic based on:
- Source identity — which service account is making the request
- Destination path — specific HTTP routes or gRPC methods
- HTTP methods — GET, POST, PUT, DELETE, etc.
- Namespace boundaries — allowing or denying cross-namespace traffic
Certificate Management
Linkerd's identity system is built around a trust chain that starts with an issuer certificate. The control plane includes a built-in CA (Certificate Authority) that automatically issues and rotates certificates for all injected pods. Certificate rotation happens every 24 hours by default, and the proxies handle hot reloading without downtime. This ensures that even if a certificate is compromised, its usable window is limited. The trust root can also be replaced with an external CA provider like cert-manager, HashiCorp Vault, or AWS Private CA for enterprise environments.
Practical Security Hardening Steps
Now let's move from theory to practice. The following sections provide concrete commands and configuration files to progressively harden your Linkerd deployment. Execute these steps in order, verifying each stage before proceeding to the next.
Enforcing Strict mTLS
The single most impactful hardening action is switching from permissive to strict mTLS. This ensures that every pod in the mesh rejects plain TCP connections and only accepts traffic authenticated via TLS with valid certificates.
First, verify your current mTLS mode by checking the control plane configuration:
# Check the current global configuration
linkerd install --ha | grep -A5 "identity"
# Look for the "identity" section and note the "issuer" settings
To enforce strict mTLS across the entire mesh, you apply an authorization policy that denies all unauthenticated traffic. Create a file called global-strict-mtls.yaml:
# global-strict-mtls.yaml
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: global-strict-mtls
namespace: linkerd
spec:
targetRef:
group: policy.linkerd.io
kind: Mesh
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
---
apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
name: linkerd-identity
namespace: linkerd
spec:
identityDomain: "cluster.local"
identityDomainAliases:
- "cluster.local"
identities:
- "*"
Apply this policy to enforce strict mTLS mesh-wide:
# Apply the strict mTLS policy
kubectl apply -f global-strict-mtls.yaml
# Verify the policy is active
kubectl get authorizationpolicy -n linkerd global-strict-mtls
# Test that unauthenticated traffic is now blocked
# (Any plain TCP connection without Linkerd injection will fail)
To verify enforcement, you can deploy a test pod without Linkerd injection and attempt to reach an injected service. The connection will be refused. Conversely, all injected services continue to communicate seamlessly because they automatically present valid certificates.
# Deploy a test pod WITHOUT Linkerd injection
kubectl create deployment test-no-mesh --image=curlimages/curl -- sleep 3600
# Try to reach an injected service (this should fail)
kubectl exec deploy/test-no-mesh -- curl -s http://your-service:8080/health
# Expected result: Connection refused or timeout
Configuring Authorization Policies
With strict mTLS enforcing authentication, the next step is authorization — controlling exactly which services can communicate. Let's build a practical example with a microservices application consisting of a frontend, an API server, and a database service.
Assume the following services and service accounts:
- frontend (ServiceAccount:
frontend-sa, namespace:app) — serves user requests - api (ServiceAccount:
api-sa, namespace:app) — handles business logic - database (ServiceAccount:
db-sa, namespace:app) — stores data
We want the following traffic flow:
frontend→api(allowed)api→database(allowed)frontend→database(denied — frontend should never talk directly to the database)- Any other cross-service call (denied by default)
Create the authorization policies file app-authorization.yaml:
# app-authorization.yaml
---
# Allow frontend to call the API service
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: frontend-to-api
namespace: app
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: api-server
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "frontend-sa"
---
# Allow API to call the database service
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: api-to-database
namespace: app
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: database-server
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "api-sa"
---
# Create Server resources to define protected services
apiVersion: policy.linkerd.io/v1beta1
kind: Server
metadata:
name: api-server
namespace: app
spec:
podSelector:
matchLabels:
app: api
port: 8080
proxyProtocol: HTTP/1
---
apiVersion: policy.linkerd.io/v1beta1
kind: Server
metadata:
name: database-server
namespace: app
spec:
podSelector:
matchLabels:
app: database
port: 5432
proxyProtocol: TCP
Apply the policies and verify:
# Apply all authorization policies
kubectl apply -f app-authorization.yaml
# List all authorization policies in the app namespace
kubectl get authorizationpolicy -n app
# List Server resources
kubectl get server -n app
# Test from frontend to database (should be denied)
kubectl exec deploy/frontend -- curl -s http://database:5432
# Expected: 403 Forbidden or connection reset
# Test from frontend to API (should succeed)
kubectl exec deploy/frontend -- curl -s http://api:8080/health
# Expected: 200 OK
# Test from API to database (should succeed)
kubectl exec deploy/api -- nc -zv database 5432
# Expected: Connection successful
Securing the Control Plane
The Linkerd control plane itself runs as a set of pods that manage the mesh. These components — the identity controller, policy controller, destination service, and others — must be protected from unauthorized access and tampering. The control plane should run in its own namespace (typically linkerd) with strict RBAC policies.
First, ensure the control plane namespace has restricted access:
# Create RBAC for the linkerd namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: linkerd-admin-only
namespace: linkerd
subjects:
- kind: Group
name: platform-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
---
# Deny all other users access to linkerd namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deny-linkerd-access
namespace: linkerd
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
Next, protect the Linkerd control plane ports. Create a Server resource that restricts access to the control plane's administrative endpoints:
# control-plane-protection.yaml
apiVersion: policy.linkerd.io/v1beta1
kind: Server
metadata:
name: linkerd-control-plane
namespace: linkerd
spec:
podSelector:
matchLabels:
linkerd.io/control-plane-component: identity
port: 9990
proxyProtocol: HTTP/1
---
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: restrict-admin-access
namespace: linkerd
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: linkerd-control-plane
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "linkerd-identity"
- "linkerd-destination"
# Apply control plane protection
kubectl apply -f control-plane-protection.yaml
Additionally, ensure that the Linkerd CLI operations require proper authentication. By default, the linkerd CLI uses the kubeconfig of the current user. Restrict who can run administrative commands by limiting access to the linkerd namespace at the Kubernetes RBAC level.
Restricting Ingress Traffic
Traffic entering the mesh from external sources (ingress controllers, load balancers, API gateways) requires special attention. By default, ingress controllers are outside the mesh and their traffic appears unauthenticated. To securely integrate ingress with Linkerd, you must inject the ingress controller into the mesh and configure authorization policies that recognize its identity.
Example for NGINX ingress with Linkerd injection:
# Inject the ingress controller into the mesh
kubectl get deploy ingress-nginx-controller -n ingress-nginx -o yaml | linkerd inject - | kubectl apply -f -
# Create an authorization policy allowing ingress to reach the frontend
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: ingress-to-frontend
namespace: app
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: frontend-server
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "nginx-ingress-serviceaccount"
If you prefer to keep the ingress controller outside the mesh, you can use Linkerd's nginx integration or configure the ingress to forward traffic to Linkerd's inbound proxy port (4143 by default). However, injecting the ingress controller is the recommended approach as it provides end-to-end identity propagation.
Best Practices for Linkerd Security
The hardening steps above establish a baseline, but operational excellence requires ongoing discipline. The following best practices help maintain a strong security posture over time.
Principle of Least Privilege
Apply the principle of least privilege rigorously when designing authorization policies. Never create blanket "allow-all" rules, even during development. Instead, explicitly enumerate every allowed service-to-service communication path. Start with a deny-all default and add allow rules incrementally. Linkerd's Server and AuthorizationPolicy resources support this pattern natively — if no AuthorizationPolicy references a Server, all traffic to that Server is denied.
Use namespace-scoped policies to create isolation boundaries. For example, a payment-processing namespace should have no default routes to the general application namespace:
# Explicit deny for cross-namespace traffic
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-cross-namespace
namespace: payments
spec:
targetRef:
group: policy.linkerd.io
kind: Namespace
name: payments
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
# No 'allowed' section = deny all by default
Regular Certificate Rotation
Linkerd automatically rotates pod certificates every 24 hours, but the trust root and intermediate CA certificates require manual rotation or integration with an external CA. Plan for certificate lifecycle management:
- Root CA rotation: Rotate the root CA at least annually. Linkerd supports bundle secrets that allow multiple trust roots during rotation windows, enabling zero-downtime transitions.
- External CA integration: For production, integrate with cert-manager, Vault, or AWS Private CA to automate certificate issuance and gain audit trails.
- Monitor certificate expiry: Set up alerts for certificate expiration. Use
linkerd checkregularly to verify the identity system health.
Example of checking certificate status:
# Run the identity diagnostic
linkerd identity -n linkerd
# Check certificate expiration for all pods
linkerd check --proxy
# Detailed certificate inspection for a specific pod
kubectl exec deploy/frontend -c linkerd-proxy -- curl -s http://localhost:4191/identity | jq .
Monitoring and Auditing
Security is only as good as your visibility into violations. Linkerd provides rich observability data that can be leveraged for security monitoring:
- Proxy metrics: The Linkerd proxy exposes metrics on TLS handshake failures, authorization denials, and certificate errors. Scrape these with Prometheus and create alerts.
- Service dashboard: Use the Linkerd dashboard (
linkerd viz dashboard) to visually inspect traffic flows and identify unauthorized connections. - Audit logging: Configure the proxy to log authorization denials. These logs are invaluable for forensic analysis and detecting attempted attacks.
Set up Prometheus alert rules for security events:
# prometheus-alerts.yaml
groups:
- name: linkerd-security
rules:
- alert: LinkerdAuthDenial
expr: rate(linkerd_proxy_authorization_denied_total[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Authorization denied for {{ $labels.target_service }}"
description: "Service {{ $labels.source_service }} attempted unauthorized access to {{ $labels.target_service }}"
- alert: LinkerdTLSFailure
expr: rate(linkerd_proxy_tls_handshake_error_total[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "TLS handshake failures detected"
description: "Proxy on {{ $labels.pod }} is experiencing TLS handshake errors"
Namespace-Level Isolation
Use Kubernetes namespaces as security boundaries and enforce them with Linkerd policies. A common pattern is to create a "DMZ" namespace for ingress and a set of isolated backend namespaces that cannot communicate with each other except through explicitly allowed paths.
Implement namespace isolation with a deny-all policy per namespace, then selectively allow specific cross-namespace routes:
# namespace-isolation.yaml
# Deny all inbound traffic to the payments namespace
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all-inbound
namespace: payments
spec:
targetRef:
group: policy.linkerd.io
kind: Namespace
name: payments
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
---
# Allow only the order-service from the orders namespace to call payments
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-orders-to-payments
namespace: payments
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: payment-processor
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "order-service-sa"
namespaces:
- "orders"
Avoiding Common Pitfalls
Even experienced teams can fall into security traps. Watch out for these common mistakes:
- Leaving permissive mTLS in production: This is the biggest risk. Permissive mode exists for migration convenience only. Always enforce strict mTLS before going live.
- Using pod labels instead of ServiceAccounts for authorization: Labels are mutable and can be spoofed. ServiceAccount-based identity is cryptographically enforced and cannot be impersonated.
- Forgetting to inject the ingress controller: Traffic entering the mesh without injection is unauthenticated. If you cannot inject the ingress controller, use a dedicated gateway service that is injected and acts as the sole entry point.
- Overly broad authorization rules: Rules that allow
*or entire namespaces defeat the purpose of authorization. Be specific — list each service account explicitly. - Neglecting control plane RBAC: Anyone with access to the
linkerdnamespace can modify mesh-wide policies. Restrict this namespace to a small set of trusted platform engineers. - Disabling proxy sidecar injection selectively: Every uninjected pod is a security gap. Use namespace-wide injection labels and avoid per-pod opt-outs unless absolutely necessary.
Advanced Security Configurations
For enterprise environments with stringent security requirements, Linkerd supports integration with external security infrastructure and advanced network controls.
Integrating with External CA Providers
Instead of relying on Linkerd's built-in CA, you can bring your own root certificate and integrate with enterprise CA systems. This provides centralized certificate management, compliance with corporate PKI policies, and integration with hardware security modules (HSMs).
Example integration with cert-manager:
# 1. Create an issuer in cert-manager
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: linkerd-trust-root
namespace: linkerd
spec:
selfSigned: {}
---
# 2. Generate the root CA certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: linkerd-trust-root-cert
namespace: linkerd
spec:
isCA: true
commonName: root.linkerd.cluster.local
secretName: linkerd-trust-root-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: linkerd-trust-root
kind: Issuer
---
# 3. Install Linkerd with the external root CA
linkerd install \
--identity-external-issuer \
--identity-trust-annotations cert-manager.io/issuer-name=linkerd-trust-root \
--identity-issuer-certificate-file /path/to/ca-cert.pem \
--identity-issuer-key-file /path/to/ca-key.pem \
| kubectl apply -f -
For HashiCorp Vault integration, you would configure Linkerd to use Vault's PKI secrets engine as the identity issuer. The key advantage is that Vault handles automatic certificate renewal, revocation, and provides a full audit log of all certificate operations.
Network-Level Firewall Rules
While Linkerd provides application-layer security, defense in depth calls for network-layer controls as well. Use Kubernetes NetworkPolicy resources alongside Linkerd authorization policies to create redundant security layers:
# network-policy.yaml
# Additional network-level restriction
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network-policy
namespace: app
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: app
podSelector:
matchLabels:
app: api
This layered approach ensures that even if the Linkerd proxy were bypassed (a highly unlikely scenario), the Kubernetes network plugin would still enforce restrictions at the network level.
Multi-Cluster Security Considerations
When using Linkerd's multi-cluster extension, security hardening extends across cluster boundaries. Cross-cluster traffic must be authenticated and authorized just like intra-cluster traffic. The key considerations are:
- Shared trust root: All clusters in the mesh must share a common trust root so that certificates issued in one cluster are valid in another. Use an external CA to distribute the same root across clusters.
- Gateway authentication: The multi-cluster gateway service must be injected into the mesh and subject to authorization policies. Restrict which source clusters and service accounts can access gateway-exposed services.
- Flat network control: If clusters are connected via a flat network (e.g., VPN), ensure that Linkerd authorization policies are in place — network proximity does not imply authorization.
- Per-cluster policies: Each cluster maintains its own authorization policies. A service allowed in cluster A may still be denied in cluster B unless explicitly authorized.
Example of a cross-cluster authorization policy:
# Allow service from cluster-west to call service in cluster-east
apiVersion: policy.linkerd.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-cross-cluster
namespace: app
spec:
targetRef:
group: policy.linkerd.io
kind: Server
name: api-server-east
requiredAuthenticationRefs:
- name: linkerd-identity
kind: MeshTLSAuthentication
group: policy.linkerd.io
allowed:
- type: ServiceAccount
names:
- "frontend-sa"
namespaces:
- "app"
# Note: The identity domain includes cluster information
# when using a shared trust root
Conclusion
Linkerd service mesh security hardening transforms a convenient connectivity layer into a robust zero-trust enforcement platform. The journey begins with enforcing strict mTLS, proceeds through fine-grained authorization policies, and matures into comprehensive control plane protection, external CA integration, and multi-cluster security governance. Each step reduces the attack surface and brings your microservices architecture closer to the ideal of cryptographically verified, explicitly authorized service-to-service communication.
By following the practical steps and best practices outlined in this tutorial — from the initial MeshTLSAuthentication policy to advanced cross-cluster authorization — you establish a security posture where identity is the sole basis for trust, encryption is universal and validated, and every connection is subject to explicit allow rules. The operational practices of monitoring, certificate rotation, and namespace isolation ensure that this posture persists and adapts as your application landscape evolves.
Remember that security hardening is not a one-time project but an ongoing discipline. Regularly run linkerd check, review authorization policies for unnecessary permissiveness, audit proxy logs for denial patterns, and stay current with Linkerd security advisories. The investment in hardening pays dividends in reduced incident response times, simplified compliance audits, and a fundamentally more resilient service mesh architecture.