feat: add outbound mTLS-required default policy - #4617
Open
gnarlex wants to merge 1 commit into
Open
Conversation
Problem The outbound proxy always falls back to a cleartext connection when a target endpoint has no mesh identity (i.e. is not meshed). Operators who want to restrict a workload to only communicate with meshed targets (or targets outside the cluster) cannot enforce this: unmeshed, in-cluster destinations are silently reached in the clear. Solution Add `LINKERD2_PROXY_OUTBOUND_DEFAULT_POLICY`, the outbound counterpart to `LINKERD2_PROXY_INBOUND_DEFAULT_POLICY`. Whether a target is meshed is only known once an endpoint is resolved (a meshed endpoint carries a mesh identity in its discovery metadata), so the policy is enforced at connect time by a new `RequireMeshIdentity` guard in the shared TCP connect stack. The guard refuses a cleartext connection when discovery provided no mesh identity for the endpoint (`ConditionalClientTls::None(NotProvidedByServiceDiscovery)`). Supported values: - `all-unauthenticated` (default): unchanged; cleartext permitted. - `all-authenticated`: require mTLS for every outbound endpoint. - `cluster-authenticated`: require mTLS only for endpoints within `LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS`; egress outside the cluster is permitted in the clear. Other cleartext reasons (loopback, identity disabled, ingress without discovery) are never refused, and the default (`all-unauthenticated`) is byte-for-byte unchanged. Validation Added unit tests for env parsing (accepted values, the cluster-networks requirement, rejected values, unset) and behavioral tests for the connect-time guard (in-cluster unmeshed refused; out-of-cluster unmeshed permitted under cluster-authenticated; `all-authenticated` refuses any unmeshed target; non-discovery cleartext permitted; default never refuses). Fixes linkerd/linkerd2#15615 Signed-off-by: Alexander Klein <hello@gnarlex.com>
gnarlex
force-pushed
the
alex/outbound-default-policy-mtls
branch
from
August 28, 2026 14:08
47df3b4 to
83dec2c
Compare
gnarlex
added a commit
to gnarlex/linkerd2
that referenced
this pull request
Aug 28, 2026
Add the control-plane surface for the proxy's outbound default policy (`LINKERD2_PROXY_OUTBOUND_DEFAULT_POLICY`), mirroring the existing inbound default policy: the `proxy.defaultOutboundPolicy` Helm value, the `--default-outbound-policy` CLI flag, and the `config.linkerd.io/default-outbound-policy` pod annotation, all injected onto proxy containers via the shared proxy partial. Control-plane components (destination, identity, proxy-injector) pin the value to `all-unauthenticated` so a restrictive global default cannot lock out the control plane. The proxy implements the outbound default policy as an mTLS requirement enforced at connect time on resolved endpoints, and only supports a three-value subset of the inbound vocabulary: - `all-unauthenticated` (default): permit cleartext to unmeshed targets; byte-for-byte unchanged from today's behavior. - `all-authenticated`: require a mesh identity (mTLS) for every outbound endpoint. - `cluster-authenticated`: require a mesh identity only for endpoints within clusterNetworks. Validation for the `--default-outbound-policy` flag and the `config.linkerd.io/default-outbound-policy` annotation is therefore narrowed to that subset; `deny`, `cluster-unauthenticated` and `audit` remain inbound-only. The Helm values documentation is updated to match. Inbound policy validation is unchanged. Enforcement of `all-authenticated`/`cluster-authenticated` across multiple control planes additionally requires relaxing the destination controller's identity-advertisement gate; that is deliberately left as a follow-up. Relates to linkerd#15615 Relates to linkerd/linkerd2-proxy#4617 Signed-off-by: Alex Klein <alex@codesphere.com>
gnarlex
added a commit
to gnarlex/linkerd2
that referenced
this pull request
Aug 28, 2026
Add the control-plane surface for the proxy's outbound default policy (`LINKERD2_PROXY_OUTBOUND_DEFAULT_POLICY`), mirroring the existing inbound default policy: the `proxy.defaultOutboundPolicy` Helm value, the `--default-outbound-policy` CLI flag, and the `config.linkerd.io/default-outbound-policy` pod annotation, all injected onto proxy containers via the shared proxy partial. Control-plane components (destination, identity, proxy-injector) pin the value to `all-unauthenticated` so a restrictive global default cannot lock out the control plane. The proxy implements the outbound default policy as an mTLS requirement enforced at connect time on resolved endpoints, and only supports a three-value subset of the inbound vocabulary: - `all-unauthenticated` (default): permit cleartext to unmeshed targets; byte-for-byte unchanged from today's behavior. - `all-authenticated`: require a mesh identity (mTLS) for every outbound endpoint. - `cluster-authenticated`: require a mesh identity only for endpoints within clusterNetworks. Validation for the `--default-outbound-policy` flag and the `config.linkerd.io/default-outbound-policy` annotation is therefore narrowed to that subset; `deny`, `cluster-unauthenticated` and `audit` remain inbound-only. The Helm values documentation is updated to match. Inbound policy validation is unchanged. Enforcement of `all-authenticated`/`cluster-authenticated` across multiple control planes additionally requires relaxing the destination controller's identity-advertisement gate; that is deliberately left as a follow-up. Relates to linkerd#15615 Relates to linkerd/linkerd2-proxy#4617 Signed-off-by: Alexander Klein <hello@gnarlex.com>
gnarlex
added a commit
to gnarlex/linkerd2
that referenced
this pull request
Aug 28, 2026
…plane The destination controller only advertised a `TlsIdentity` for a pod when the pod's control-plane-ns label matched the controller's own namespace. As a result, a pod meshed by a *different* Linkerd control plane was reported as unmeshed (no identity), even when all control planes share a trust root. This blocks the proxy's outbound default policy (`all-authenticated` / `cluster-authenticated`), which requires mTLS to targets that advertise a mesh identity: pods meshed by a peer control plane would be refused even though they are meshed. Relax the identity gate from `controllerNSLabel == controllerNS` to `controllerNSLabel != ""`, matching the protocol-hint gate immediately above it. The identity SAN is already built from the pod's own control-plane-ns label, so under a shared trust root it is correct for cross-control-plane peers with no further change. This leaves the threaded `controllerNS` value unused in the endpoint translators, so it is removed from `newEndpointTranslator`, `newEndpointProfileTranslator` and `createWeightedAddr` and their call sites (`Config.ControllerNS` is unchanged). This assumes a single shared trust root/domain across all control planes (Variant A). Selectively trusting meshes with distinct trust roots (via a trust-domain annotation) remains future work, as noted in the TODO. Adds a test asserting identity is advertised for a pod whose control-plane-ns differs from the controller's, with a SAN derived from the pod's own label, and documents the shared-root requirement on the `defaultOutboundPolicy` Helm value. Relates to linkerd#15615 Relates to linkerd/linkerd2-proxy#4617 Signed-off-by: Alex Klein <alex@codesphere.com>
thesw4rm
reviewed
Aug 29, 2026
thesw4rm
left a comment
There was a problem hiding this comment.
I'm not currently associated to this project. I was hoping to make my first contribution here. Please feel free to ignore my review!
Comment on lines
+53
to
+61
| let connect = ConnectTcp::new( | ||
| self.config.proxy.connect.keepalive, | ||
| self.config.proxy.connect.user_timeout, | ||
| )); | ||
| ); | ||
| let requirement = Requirement::new( | ||
| self.config.default_policy, | ||
| self.config.cluster_networks.clone(), | ||
| ); | ||
| let connect = PreventLoopback(RequireMeshIdentity::new(connect, requirement)); |
There was a problem hiding this comment.
Brand new to this project, with some Rust experience. Can we fix the variable names?
Suggested change
| let connect = ConnectTcp::new( | |
| self.config.proxy.connect.keepalive, | |
| self.config.proxy.connect.user_timeout, | |
| )); | |
| ); | |
| let requirement = Requirement::new( | |
| self.config.default_policy, | |
| self.config.cluster_networks.clone(), | |
| ); | |
| let connect = PreventLoopback(RequireMeshIdentity::new(connect, requirement)); | |
| let connect = ConnectTcp::new( | |
| self.config.proxy.connect.keepalive, | |
| self.config.proxy.connect.user_timeout, | |
| ); | |
| let tlsAuthRequirement = Requirement::new( | |
| self.config.default_policy, | |
| self.config.cluster_networks.clone(), | |
| ); | |
| self.clone().with_stack( | |
| PreventLoopback(RequireMeshIdentity::new(connect, requirement)) | |
| ) |
| } | ||
|
|
||
| #[derive(Clone, Debug)] | ||
| enum Requirement { |
There was a problem hiding this comment.
Requirement on its own might not be descriptive enough. Maybe
Suggested change
| enum Requirement { | |
| enum TLSAuthRequirement |
| #[derive(Clone, Debug)] | ||
| pub struct RequireMeshIdentity<S> { | ||
| inner: S, | ||
| requirement: Requirement, |
There was a problem hiding this comment.
Suggested change
| requirement: Requirement, | |
| tlsAuthRequirement: TLSAuthRequirement, |
gnarlex
marked this pull request as ready for review
August 31, 2026 12:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The outbound proxy always falls back to a cleartext connection when a target endpoint has no mesh identity (i.e. is not meshed). Operators who want to restrict a workload to only communicate with meshed targets (or targets outside the cluster) cannot enforce this: unmeshed, in-cluster destinations are silently reached in the clear.
Solution
Add
LINKERD2_PROXY_OUTBOUND_DEFAULT_POLICY, the outbound counterpart toLINKERD2_PROXY_INBOUND_DEFAULT_POLICY. Whether a target is meshed is only known once an endpoint is resolved (a meshed endpoint carries a mesh identity in its discovery metadata), so the policy is enforced at connect time by a newRequireMeshIdentityguard in the shared TCP connect stack. The guard refuses a cleartext connection when discovery provided no mesh identity for the endpoint(
ConditionalClientTls::None(NotProvidedByServiceDiscovery)).Supported values:
all-unauthenticated(default): unchanged; cleartext permitted.all-authenticated: require mTLS for every outbound endpoint.cluster-authenticated: require mTLS only for endpoints withinLINKERD2_PROXY_POLICY_CLUSTER_NETWORKS; egress outside the cluster is permitted in the clear.Other cleartext reasons (loopback, identity disabled, ingress without discovery) are never refused, and the default (
all-unauthenticated) is byte-for-byte unchanged.Validation
Added unit tests for env parsing (accepted values, the cluster-networks requirement, rejected values, unset) and behavioral tests for the connect-time guard (in-cluster unmeshed refused; out-of-cluster unmeshed permitted under cluster-authenticated;
all-authenticatedrefuses any unmeshed target; non-discovery cleartext permitted; default never refuses).Fixes linkerd/linkerd2#15615