Linkerd’s "skip ports" feature is surprisingly powerful because it lets you selectively opt-out specific ports from the mesh, rather than forcing you to mesh everything or nothing.

Let’s see it in action. Imagine you have a legacy application on port 8080 that doesn’t play well with Linkerd’s injected sidecar (maybe it does its own TLS, or has weird connection handling). You want to keep the rest of your application’s traffic meshed for observability and security, but bypass port 8080.

Here’s how you’d configure that in your Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    metadata:
      annotations:
        linkerd.io/inject: enabled
        linkerd.io/skip-ports: "8080" # <-- This is the magic
    spec:
      containers:
      - name: my-app-container
        image: my-app-image:latest
        ports:
        - containerPort: 8080
        - containerPort: 9090 # Another port, which will be meshed

When Linkerd’s linkerd-inject process (or the mutating webhook) sees linkerd.io/skip-ports: "8080", it tells the injected linkerd-proxy sidecar to not intercept traffic destined for or originating from TCP port 8080 within the pod. Traffic on port 9090, however, will be fully meshed.

The core problem this solves is the "all or nothing" dilemma when introducing a service mesh. Early on, or with certain types of applications, you might not want all traffic to go through the proxy. This could be due to:

  • Legacy Applications: Applications with custom networking stacks, self-signed TLS, or connection pooling that break when intercepted.
  • Performance-Sensitive Services: For extremely high-throughput, low-latency services where even the minimal proxy overhead is unacceptable for specific communication channels.
  • External Integrations: Services that communicate with external systems on specific ports where you don’t want mesh policies applied.
  • Non-HTTP/gRPC Traffic: While Linkerd supports TCP, some protocols might have edge cases or be entirely outside the scope of what you want to observe or secure via the mesh.

When Linkerd injects its sidecar, it modifies the pod’s network namespace. For the ports not skipped, it sets up iptables rules that redirect incoming and outgoing traffic to the linkerd-proxy container. The linkerd-proxy then makes intelligent routing decisions, applies policies, and forwards the traffic to its final destination (either another pod’s actual application container or the network).

By specifying linkerd.io/skip-ports, you’re essentially telling the injection process: "For this particular pod, do not add iptables rules for the specified ports." This means traffic on those ports bypasses the linkerd-proxy entirely and goes directly to and from the application container.

The linkerd.io/skip-ports annotation accepts a comma-separated string of port numbers. You can specify multiple ports, like "8080,8081,9900".

Consider a scenario where your application listens on 8080 for its main API and also on 9090 for Prometheus metrics. You want the API to be meshed, but the Prometheus metrics endpoint to be directly accessible without proxy overhead.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-metrics-app
spec:
  template:
    metadata:
      annotations:
        linkerd.io/inject: enabled
        linkerd.io/skip-ports: "9090" # Skip the metrics port
    spec:
      containers:
      - name: my-app-container
        image: my-app-image:latest
        ports:
        - containerPort: 8080 # This port will be meshed
        - containerPort: 9090 # This port will be skipped

In this case, all traffic to 8080 will be intercepted by the linkerd-proxy for features like mTLS, retries, and observability. However, requests to 9090 will bypass the proxy, flowing directly to your application container. This allows you to scrape metrics directly without any potential for proxy-induced latency or configuration issues.

The linkerd-proxy itself runs as a separate container within your pod. When traffic is not skipped, the iptables rules are configured to route traffic to the linkerd-proxy’s listening address on 127.0.0.1, and then the proxy decides whether to send it back to the application container on the original port or to another destination. For skipped ports, this redirection step is simply omitted.

It’s important to note that skipping a port means Linkerd has no visibility into that traffic. No mTLS, no retries, no request metrics, no tracing. It’s as if the service mesh isn’t there for that specific communication channel.

When you use linkerd.io/skip-ports, the injected proxy still initializes and runs, but its iptables configuration is modified. The linkerd-proxy container itself listens on a range of ports for its internal operations and for handling meshed traffic, but the iptables rules are the gatekeepers that determine what traffic actually reaches it. If a port is skipped, the iptables rules that would normally DNAT (Destination Network Address Translation) traffic for that port to the proxy are absent.

After successfully configuring linkerd.io/skip-ports and applying your deployment, the next thing you’ll likely encounter is needing to manage policies for the traffic that is still being meshed.

Want structured learning?

Take the full Linkerd course →