Technical Insight How-to Guide

How to Set Up Linkerd Service Mesh in Kubernetes

A practical step-by-step guide to installing Linkerd in Kubernetes, enabling mTLS and authorization, adding observability, configuring traffic routing, retries, timeouts, and troubleshooting your service mesh.

Service mesh with Linkerd

Technical Context

Where this fits.

Knowledge area Kubernetes Section Operations Purpose How-to Guide

Linkerd provides a lightweight service mesh for Kubernetes that adds workload identity, mutual TLS, traffic visibility, reliability features, and authorization without requiring changes to application code.

In this guide, we’ll install Linkerd, add an application namespace to the mesh, verify mTLS, install the observability stack, configure traffic management using the Kubernetes Gateway API, and restrict service-to-service communication using Linkerd authorization policies.

By the end, the architecture will look roughly like this:

Diagram source
flowchart LR
    subgraph Kubernetes
        subgraph PodA["Frontend Pod"]
            A[Frontend] --> PA[Linkerd Proxy]
        end

        subgraph PodB["API Pod"]
            PB[Linkerd Proxy] --> B[API]
        end

        PA <-->|mTLS| PB

        CP[Linkerd Control Plane]
        CP -.-> PA
        CP -.-> PB
    end

The Linkerd proxies transparently intercept application traffic. When both workloads are part of the mesh, Linkerd automatically establishes mutually authenticated TLS connections between them.

Prerequisites

You need:

  • A running Kubernetes cluster
  • kubectl configured for the cluster
  • Permissions to install cluster-scoped resources
  • A Kubernetes version supported by the Linkerd release you intend to install
  • A compatible Kubernetes Gateway API version

Verify Kubernetes access first:

kubectl cluster-info
kubectl get nodes

All nodes should be Ready.

For the examples below, we’ll use a namespace called production:

kubectl create namespace production

If the namespace already exists, continue with the existing namespace.

Install the Linkerd CLI

The Linkerd CLI is used to install, validate, inspect, and troubleshoot the mesh.

On macOS:

brew install linkerd

On Linux, install the CLI using the installation method provided for the Linkerd distribution and version you intend to operate.

Verify the installation:

linkerd version

Before the control plane is installed, it is normal for the server version to be unavailable.

Install the Kubernetes Gateway API

Modern Linkerd releases use Kubernetes Gateway API resources for features including:

  • HTTP and gRPC routing
  • retries
  • timeouts
  • route-level authorization

First determine whether Gateway API is already installed:

kubectl get crd httproutes.gateway.networking.k8s.io

You can inspect the installed bundle version with:

kubectl get crds/httproutes.gateway.networking.k8s.io \
  -o "jsonpath={.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version}"

If Gateway API is not installed, install a version supported by your Linkerd release.

For example:

kubectl apply -f \
  https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml

Do not blindly upgrade Gateway API on an existing cluster. Other controllers may use the same CRDs, and Linkerd versions support specific Gateway API version ranges.

Run the Linkerd Pre-Installation Check

Before installing the control plane, validate the cluster:

linkerd check --pre

Resolve reported failures before continuing.

This verifies that the cluster and your current permissions satisfy Linkerd’s installation prerequisites.

Install the Linkerd Control Plane

Install the Linkerd CRDs first:

linkerd install --crds | kubectl apply -f -

Then install the control plane:

linkerd install | kubectl apply -f -

Watch the control-plane pods:

kubectl get pods -n linkerd

Run Linkerd’s full validation:

linkerd check

You can also inspect the deployments directly:

kubectl get deploy -n linkerd

For production, consider managing Linkerd with Helm or your GitOps workflow rather than relying on manually piped CLI manifests. The important point is to keep the installation configuration reproducible and version controlled.

Add an Application to the Mesh

Installing Linkerd does not automatically modify application workloads.

The Linkerd proxy must be added to pods that should participate in the mesh.

For namespace-based deployments, automatic injection is usually the simplest approach.

Annotate the namespace:

kubectl annotate namespace production linkerd.io/inject=enabled

Verify it:

kubectl get namespace production -o yaml

You should see:

metadata:
  annotations:
    linkerd.io/inject: enabled

Injection occurs when a pod is created, so restart existing deployments:

kubectl rollout restart deployment -n production

Watch the rollout:

kubectl get pods -n production -w

Verify Proxy Injection

Inspect the application pods:

kubectl get pods -n production

A pod that previously showed:

READY   1/1

will normally show:

READY   2/2

after injection.

Inspect the container names:

kubectl get pod <pod-name> -n production -o jsonpath='{.spec.containers[*].name}'

You should see the application container and:

linkerd-proxy

Validate all meshed workloads in the namespace:

linkerd check --proxy -n production

How Linkerd mTLS Works

One of Linkerd’s main advantages is that applications do not need to manage service-to-service TLS themselves.

For meshed traffic, Linkerd:

  1. Assigns a workload identity to each proxy.
  2. Issues short-lived certificates to proxies.
  3. Authenticates the destination.
  4. Encrypts proxy-to-proxy traffic with mTLS.
  5. Rotates workload certificates automatically.

The application can continue to use an ordinary Kubernetes Service URL:

http://api.production.svc.cluster.local

The actual pod-to-pod flow becomes:

Diagram source
sequenceDiagram
    participant A as Frontend
    participant PA as Linkerd Proxy A
    participant PB as Linkerd Proxy B
    participant B as API

    A->>PA: HTTP
    PA->>PB: mTLS
    PB->>B: HTTP
    B->>PB: HTTP Response
    PB->>PA: mTLS
    PA->>A: HTTP Response

This gives applications authenticated and encrypted service-to-service communication without embedding certificate-management logic into each application.

Install Linkerd Viz

Install the Viz extension:

linkerd viz install | kubectl apply -f -

Validate it:

linkerd viz check

You can also run:

linkerd check

to include checks for installed extensions.

View Application Traffic

Inspect the meshed deployments:

linkerd viz stat deploy -n production

Typical metrics include:

  • request rate
  • success rate
  • P50 latency
  • P95 latency
  • P99 latency

For example:

NAME       MESHED   SUCCESS   RPS   LATENCY_P50   LATENCY_P95   LATENCY_P99
frontend   1/1      99.98%    12.4  4ms           11ms          18ms
api        3/3      99.99%    35.8  3ms           8ms           14ms

These service-level metrics are available from the mesh without requiring each application to implement equivalent HTTP telemetry itself.

Open the Linkerd Dashboard

Start the dashboard:

linkerd viz dashboard &

The dashboard lets you inspect:

  • namespaces
  • deployments
  • pods
  • service relationships
  • success rates
  • request rates
  • latency

For production environments, do not expose the Viz dashboard publicly without appropriate authentication and access controls.

Verify mTLS

Inspect service-to-service relationships:

linkerd viz edges deployment -n production

You can also inspect a pod’s Linkerd identity:

linkerd identity -n production <pod-name>

An important distinction is that automatic mTLS authenticates traffic between meshed workloads, but encryption alone does not define which authenticated workload is allowed to call another workload.

That is the job of authorization policy.

From mTLS to Service Authorization

Assume our application contains:

frontend -> api

Both workloads run in production, and the frontend uses the Kubernetes ServiceAccount:

frontend

The API should accept requests from the frontend but reject arbitrary workloads.

Linkerd authorization builds on the identities established by mTLS:

Diagram source
flowchart LR
    F["frontend\nServiceAccount: frontend"]
    -->|"mTLS identity"| FP[Linkerd Proxy]
    -->|"Authorized"| AP[API Linkerd Proxy]
    --> API[API]

    X["other workload"]
    --> XP[Linkerd Proxy]
    -.->|"Denied"| AP

We will configure this in three steps:

  1. Define which API port is protected with a Server.
  2. Define which mTLS identity is trusted with MeshTLSAuthentication.
  3. Attach that authentication requirement with an AuthorizationPolicy.

Define the Protected API Server

Assume the API pods have:

labels:
  app: api

and the application listens on the named container port:

ports:
  - name: http
    containerPort: 8080

Create a Linkerd Server:

apiVersion: policy.linkerd.io/v1beta3
kind: Server
metadata:
  name: api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  port: http
  proxyProtocol: HTTP/1

Apply it:

kubectl apply -f api-server.yaml

Check it:

kubectl get server -n production

A critical behavior to understand is that a Server is a policy boundary. Once a Server selects a workload port, traffic must be authorized according to the applicable Linkerd policy.

Do not introduce a Server into production without also understanding which callers and Kubernetes probes need access.

Identify the Frontend mTLS Identity

Linkerd workload identities are derived from Kubernetes ServiceAccounts.

For the frontend ServiceAccount in the production namespace, the identity normally has the form:

frontend.production.serviceaccount.identity.linkerd.cluster.local

Confirm that the frontend deployment actually uses that ServiceAccount:

kubectl get deploy frontend -n production \
  -o jsonpath='{.spec.template.spec.serviceAccountName}'

If it uses the namespace’s default ServiceAccount, create and assign a dedicated one before building identity-based authorization:

kubectl create serviceaccount frontend -n production

Then configure the deployment:

spec:
  template:
    spec:
      serviceAccountName: frontend

Restart it if necessary:

kubectl rollout restart deploy/frontend -n production

Using dedicated ServiceAccounts is important because they become security identities in the mesh.

Define the Allowed mTLS Identity

Create a MeshTLSAuthentication resource:

apiVersion: policy.linkerd.io/v1alpha1
kind: MeshTLSAuthentication
metadata:
  name: frontend
  namespace: production
spec:
  identities:
    - "frontend.production.serviceaccount.identity.linkerd.cluster.local"

Apply it:

kubectl apply -f frontend-authentication.yaml

This resource describes an authenticated caller. It does not yet grant that caller access to anything.

Authorize the Frontend to Call the API

Create an AuthorizationPolicy targeting the API Server:

apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: frontend-to-api
  namespace: production
spec:
  targetRef:
    group: policy.linkerd.io
    kind: Server
    name: api

  requiredAuthenticationRefs:
    - group: policy.linkerd.io
      kind: MeshTLSAuthentication
      name: frontend

Apply it:

kubectl apply -f frontend-to-api.yaml

The effective model is now:

frontend ServiceAccount
        |
        | Linkerd mTLS identity
        v
MeshTLSAuthentication
        |
        v
AuthorizationPolicy
        |
        v
Server: api
        |
        v
API workload

Traffic matching the policy is accepted. Traffic to that protected Server without an applicable authorization is denied.

Verify Linkerd Authorization

Inspect the authorization configuration:

kubectl get server,meshtlsauthentication,authorizationpolicy \
  -n production

With Viz installed, inspect effective authorization for the API:

linkerd viz authz -n production deploy/api

This is one of the most useful commands when debugging Linkerd policy because it shows the server, route, authorization, unauthorized request rate, and traffic metrics together.

Test from the frontend:

kubectl exec -n production deploy/frontend -c <application-container> -- \
  curl -sS http://api

Then test from another meshed workload using a different ServiceAccount.

The second request should not be accepted by the protected API unless that identity has also been authorized.

Do Not Forget Kubernetes Health Probes

Authorization can affect readiness and liveness probes.

Kubelet health checks do not behave like ordinary meshed application-to-application requests. If you introduce restrictive Server or route policies and the pod suddenly becomes unready, inspect the probe configuration before assuming the application is broken.

Check:

kubectl describe pod <api-pod> -n production

and:

linkerd viz authz -n production deploy/api

For more granular per-route policies, explicitly account for the application’s health-check route and authorize the required probe traffic according to your cluster’s security model.

Avoid solving probe failures by broadly allowing all application traffic.

Configure Route-Level Authorization

Protecting the complete API port is useful, but HTTP-aware workloads can go further.

Suppose:

GET /api/v1/*

should be callable by the frontend, while administrative routes require different policy.

Create an inbound HTTPRoute attached to the Linkerd Server:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-read
  namespace: production
spec:
  parentRefs:
    - group: policy.linkerd.io
      kind: Server
      name: api

  rules:
    - matches:
        - method: GET
          path:
            type: PathPrefix
            value: /api/v1/

You can then target that route with an AuthorizationPolicy instead of authorizing the entire Server.

For example:

apiVersion: policy.linkerd.io/v1alpha1
kind: AuthorizationPolicy
metadata:
  name: frontend-api-read
  namespace: production
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-read

  requiredAuthenticationRefs:
    - group: policy.linkerd.io
      kind: MeshTLSAuthentication
      name: frontend

This gives you HTTP-aware authorization while still basing trust on the caller’s cryptographic mesh identity.

For a first rollout, service-level policy is easier to operate. Introduce route-level authorization only where the additional granularity is justified.

Configure HTTP Routing with Gateway API

Suppose the API has two implementations:

api-stable
api-canary

Both are Kubernetes Services:

kubectl get svc -n production

For example:

api
api-stable
api-canary

An outbound HTTPRoute attached to the api Service can control where client requests are sent.

Header-Based Canary Routing

The following rule sends requests containing:

x-canary: true

to the canary service:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-routing
  namespace: production
spec:
  parentRefs:
    - name: api
      kind: Service
      group: ""
      port: 80

  rules:
    - matches:
        - headers:
            - name: x-canary
              value: "true"

      backendRefs:
        - name: api-canary
          port: 80

    - backendRefs:
        - name: api-stable
          port: 80

Apply it:

kubectl apply -f api-routing.yaml

Normal requests go to api-stable, while:

curl -H "x-canary: true" http://api.production

is routed to api-canary.

Configure Weighted Canary Traffic

Gateway API can also distribute traffic between multiple backends by weight:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-canary
  namespace: production
spec:
  parentRefs:
    - name: api
      kind: Service
      group: ""
      port: 80

  rules:
    - backendRefs:
        - name: api-stable
          port: 80
          weight: 90

        - name: api-canary
          port: 80
          weight: 10

Apply it:

kubectl apply -f api-canary.yaml

A rollout can then progress through weights such as:

99 / 1
90 / 10
75 / 25
50 / 50
0 / 100

without modifying the application.

Configure Request Timeouts

A mesh can enforce client-side request timeouts so that calls do not wait indefinitely for an unhealthy backend.

For example:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api
  namespace: production
spec:
  parentRefs:
    - name: api
      kind: Service
      group: ""
      port: 80

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api

      timeouts:
        request: 10s
        backendRequest: 3s

      backendRefs:
        - name: api
          port: 80

request limits the total request time, while backendRequest limits an individual backend attempt.

Because these are outbound policies, the calling workload must be meshed for Linkerd to enforce them.

Configure Retries

Retries can improve reliability for transient failures, but they must be bounded.

A route can carry Linkerd retry configuration, for example:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api
  namespace: production
  annotations:
    retry.linkerd.io/http: "502-504"
    retry.linkerd.io/limit: "2"
    retry.linkerd.io/timeout: "2s"
spec:
  parentRefs:
    - name: api
      kind: Service
      group: ""
      port: 80

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api

      backendRefs:
        - name: api
          port: 80

Be conservative with retries.

A failed request can otherwise become an amplification mechanism during an outage:

1 original request
+ N retry requests

Prefer retries for operations that are safe to repeat and combine them with explicit timeouts.

Observe Route-Level Behavior

Inspect outbound route metrics:

linkerd viz stat-outbound deploy/frontend -n production

Route-level metrics are useful when investigating:

  • a slow endpoint
  • a failing route
  • retry behavior
  • timeout behavior
  • canary traffic

ServiceProfiles vs Gateway API

Older Linkerd configurations often use ServiceProfile resources for route definitions, retries, timeouts, and route metrics.

They remain relevant to existing environments, but new configuration should generally use the standard Gateway API resources where supported.

Check for existing ServiceProfiles:

kubectl get serviceprofile -A

This matters during migration because an existing ServiceProfile can take precedence over an outbound HTTPRoute targeting the same Service.

If a new HTTPRoute appears to be ignored, check for a ServiceProfile early in the investigation.

Inspect Live Traffic

View deployment statistics:

linkerd viz stat deploy -n production

Inspect routes:

linkerd viz routes deploy/api -n production

Inspect service relationships:

linkerd viz edges deployment -n production

Inspect live traffic:

linkerd viz tap deploy/api -n production

tap is especially useful when troubleshooting because it lets you observe requests without enabling verbose application logging.

Use it carefully in production because request metadata may contain sensitive information.

Production Installation Considerations

For production, treat Linkerd as a platform component.

Version-control at least:

  • Linkerd version
  • Gateway API version
  • installation values
  • trust-anchor management
  • identity issuer configuration
  • HA configuration
  • proxy resource settings
  • CNI configuration if used
  • observability integration
  • Server resources
  • authentication resources
  • authorization policies
  • routing and reliability policies
  • upgrade procedure

Use dedicated Kubernetes ServiceAccounts for workloads whose identities need to be distinguished by authorization policy.

A service mesh is much easier to secure when application identity is intentional rather than every workload using the default ServiceAccount.

Troubleshooting Linkerd

Start with:

linkerd check

This should normally be the first diagnostic command.

Check Proxy Injection

linkerd check --proxy -n production

Then inspect the pod:

kubectl describe pod <pod> -n production

Verify that linkerd-proxy exists.

Check Proxy Logs

kubectl logs deploy/api \
  -n production \
  -c linkerd-proxy

Check Application Metrics

linkerd viz stat deploy -n production

Look for:

  • reduced success rate
  • increased latency
  • unexpected request volume

Inspect Live Requests

linkerd viz tap deploy/api -n production

Inspect Traffic Relationships

linkerd viz edges deployment -n production

This helps determine whether both ends of a connection are actually meshed.

Check Authorization

linkerd viz authz -n production deploy/api

Then inspect the policy resources:

kubectl get server,authorizationpolicy,meshtlsauthentication \
  -n production

If traffic stopped immediately after adding a Server, authorization policy is one of the first things to inspect.

Also check readiness and liveness probes.

Check Gateway API Resources

kubectl get httproute -A

Then:

kubectl describe httproute <route-name> -n production

Pay attention to resource status conditions.

Also verify that the installed Gateway API version is supported by your Linkerd version.

A Practical Rollout Strategy

Avoid enabling the mesh and restrictive policy across every namespace at once.

A safer progression is:

Diagram source
flowchart LR
    A[Install Control Plane]
    --> B[Mesh Test Namespace]
    --> C[Validate Traffic]
    --> D[Validate mTLS]
    --> E[Enable Observability]
    --> F[Assign Dedicated ServiceAccounts]
    --> G[Add Authorization]
    --> H[Add Routing Policies]
    --> I[Production Rollout]

Start with a non-critical namespace:

kubectl annotate namespace test \
  linkerd.io/inject=enabled

Verify:

linkerd check --proxy -n test

Observe normal traffic before introducing restrictive authorization.

Then add Server and authorization policies for one service at a time.

Final Verification

Run:

linkerd check

Validate application proxies:

linkerd check --proxy -n production

Check traffic:

linkerd viz stat deploy -n production

Check mTLS relationships:

linkerd viz edges deployment -n production

Check authorization:

linkerd viz authz -n production deploy/api

Inspect routing resources:

kubectl get httproute -n production

You now have a Kubernetes service mesh providing:

  • transparent proxying
  • workload identity
  • automatic mTLS
  • service-to-service authorization
  • service metrics
  • request-level observability
  • traffic routing
  • canary deployments
  • retries
  • timeouts

The important security progression is:

Encrypt -> Authenticate -> Authorize

mTLS gives the mesh cryptographic workload identity. Authorization policy turns those identities into explicit rules about which services may communicate.

That is where a service mesh starts becoming a practical zero-trust control plane rather than simply an encrypted application network.

Related Articles