Skip to content

feat: add outbound mTLS-required default policy - #4617

Open
gnarlex wants to merge 1 commit into
linkerd:mainfrom
gnarlex:alex/outbound-default-policy-mtls
Open

feat: add outbound mTLS-required default policy#4617
gnarlex wants to merge 1 commit into
linkerd:mainfrom
gnarlex:alex/outbound-default-policy-mtls

Conversation

@gnarlex

@gnarlex gnarlex commented Aug 28, 2026

Copy link
Copy Markdown

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

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
gnarlex force-pushed the alex/outbound-default-policy-mtls branch from 47df3b4 to 83dec2c Compare August 28, 2026 14:08
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 thesw4rm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

@thesw4rm thesw4rm Aug 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

@thesw4rm thesw4rm Aug 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
requirement: Requirement,
tlsAuthRequirement: TLSAuthRequirement,

@gnarlex
gnarlex marked this pull request as ready for review August 31, 2026 12:22
@gnarlex
gnarlex requested a review from a team as a code owner August 31, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an option (policy) to enforce mTLS for outbound traffic

2 participants