Summary
When a relay is configured with --server-tls-root, a peer presenting any certificate that chains to
that root is admitted to whatever path it dials, with publish and subscribe both unscoped
beneath it. The authorization endpoint is told that some certificate was verified — mtls: true —
and never which one. So an endpoint cannot distinguish one certificate holder from another, and
certificate-scoped entitlement is not merely unimplemented but not expressible through the
--auth-api contract.
The part that is easy to miss is that the root is not a property of the certificate. It is chosen by
the client, in the URL:
// rs/moq-relay/src/auth.rs
pub async fn verify_mtls(&self, path: &str, transport: Option<Transport>) -> Result<AuthToken, AuthError> {
let (root, tier) = self.resolve_mtls(path, transport).await?;
let mut token = AuthToken::unrestricted(Path::new(&root).to_owned());
token.tier = tier;
Ok(token)
}
and unrestricted grants everything beneath that root:
pub fn unrestricted(root: PathOwned) -> Self {
Self {
root,
subscribe: PathPrefixes::from(vec![Path::new("").to_owned()]),
publish: PathPrefixes::from(vec![Path::new("").to_owned()]),
...
}
}
resolve_mtls does consult the auth API, and it fails closed on an API error, so the endpoint is not
powerless — it can alias the path, set a tier, or refuse. But the only thing it can key any of that on
is the path the client asked for. The party being scoped picks the scope.
What we measured
A relay with --server-tls-root <affiliate CA> and --auth-api, an estate of two tenants, and one
client holding a valid affiliate certificate and no token at all. Each cell is a 30 s subscription;
the figure is payload bytes delivered, taken from the subscriber's capture rather than from a relay log
line.
| Dialled |
Result |
wbd/cnn — the channel this affiliate licenses |
7,260,560 bytes |
wbd/cnn-intl — licensed by no affiliate |
6,736,040 bytes |
wbd/tnt — licensed by other affiliates |
7,263,380 bytes |
wbd/nobody — licensed by nobody |
2,408,656 bytes |
rival/cnn — the other broadcaster's tenant |
7,093,804 bytes |
Every cell served. The same client with a token instead of a certificate is refused on four of those
five, with zero payload bytes, exactly as the grants say it should be — so the token path is working
and it is the certificate path that bypasses it.
Why this is worth changing rather than documenting
The comment in resolve_mtls states the rationale plainly — "mTLS peers are already trusted (the cert
is the credential)" — and for the case it was written for, cluster peers dialling /, that is right.
The difficulty is that the same switch is the natural one to reach for when an operator wants mutual
authentication for subscribers, and there it silently converts a per-affiliate credential into a
master key for every tenant on the relay. Nothing warns at startup, nothing appears in the session, and
the authorization endpoint — the one component that holds the licensing matrix — cannot see that it has
been bypassed.
It also fails in a direction that is hard to detect from the outside: everything works. The affiliate
gets its channel. The misconfiguration only becomes visible when someone dials a path they should not
have, which is exactly the case nobody tests.
What we would suggest
Minimally, and without changing any default:
- Carry the peer's certificate identity in
AuthApiRequest. A subject or a SAN, or the leaf's
SHA-256 fingerprint, as one more optional query parameter beside kid. That alone makes
certificate-scoped entitlement expressible: the endpoint can return a narrower public.subscribe
for a subscriber certificate, or refuse outright, using the same reply shape it already has.
- Consider not granting
publish unscoped to an mTLS peer by default, or making the grant
configurable separately from subscribe. A subscriber certificate that can publish into the tenant
it is reading is a larger grant than the deployment probably intends.
- Warn at startup when
--server-tls-root is set alongside --auth-api. The combination is not
wrong, but it means the API's answer is advisory for one class of peer and binding for the other,
and that is worth one line on stderr. We are not claiming a precedent for this: an earlier
version of this issue said the relay already warns about configurations where nothing can
authenticate, and that was wrong. It does not. Every tracing::warn! in Auth::new on
fd4f5d82e is a deprecation notice for a superseded flag (--auth-tls-*, --auth-key-dir with a
URL, --auth-public-api), and the remaining checks are anyhow::ensure! hard errors on mutually
exclusive flags. There is no existing diagnostic for a configuration that is merely hazardous, so
this asks for a new class of warning rather than one more instance of an established one — which
may make it a larger ask than (1) or (2), and it should be judged on the hazard rather than on
consistency with something that isn't there.
We are happy to prepare the AuthApiRequest change if the direction is acceptable; the reply-side
contract needs no change for (1), only the request.
What we are not claiming
- This does not contradict the documented split of mTLS for data-plane peers and JWTs for subscribers.
It is the cost of ignoring that split, and the point is that the relay does not make the split
enforceable.
- We have not tested a cluster deployment. Every measurement above is one relay, and how this
interacts with cluster-peer trust is not something we can speak to.
- Certificate revocation was not measured. An all-or-nothing credential has no per-channel
de-provisioning behaviour to measure, so we cannot say how quickly withdrawing a certificate takes
effect.
Summary
When a relay is configured with
--server-tls-root, a peer presenting any certificate that chains tothat root is admitted to whatever path it dials, with
publishandsubscribeboth unscopedbeneath it. The authorization endpoint is told that some certificate was verified —
mtls: true—and never which one. So an endpoint cannot distinguish one certificate holder from another, and
certificate-scoped entitlement is not merely unimplemented but not expressible through the
--auth-apicontract.The part that is easy to miss is that the root is not a property of the certificate. It is chosen by
the client, in the URL:
and
unrestrictedgrants everything beneath that root:resolve_mtlsdoes consult the auth API, and it fails closed on an API error, so the endpoint is notpowerless — it can alias the path, set a tier, or refuse. But the only thing it can key any of that on
is the path the client asked for. The party being scoped picks the scope.
What we measured
A relay with
--server-tls-root <affiliate CA>and--auth-api, an estate of two tenants, and oneclient holding a valid affiliate certificate and no token at all. Each cell is a 30 s subscription;
the figure is payload bytes delivered, taken from the subscriber's capture rather than from a relay log
line.
wbd/cnn— the channel this affiliate licenseswbd/cnn-intl— licensed by no affiliatewbd/tnt— licensed by other affiliateswbd/nobody— licensed by nobodyrival/cnn— the other broadcaster's tenantEvery cell served. The same client with a token instead of a certificate is refused on four of those
five, with zero payload bytes, exactly as the grants say it should be — so the token path is working
and it is the certificate path that bypasses it.
Why this is worth changing rather than documenting
The comment in
resolve_mtlsstates the rationale plainly — "mTLS peers are already trusted (the certis the credential)" — and for the case it was written for, cluster peers dialling
/, that is right.The difficulty is that the same switch is the natural one to reach for when an operator wants mutual
authentication for subscribers, and there it silently converts a per-affiliate credential into a
master key for every tenant on the relay. Nothing warns at startup, nothing appears in the session, and
the authorization endpoint — the one component that holds the licensing matrix — cannot see that it has
been bypassed.
It also fails in a direction that is hard to detect from the outside: everything works. The affiliate
gets its channel. The misconfiguration only becomes visible when someone dials a path they should not
have, which is exactly the case nobody tests.
What we would suggest
Minimally, and without changing any default:
AuthApiRequest. A subject or a SAN, or the leaf'sSHA-256 fingerprint, as one more optional query parameter beside
kid. That alone makescertificate-scoped entitlement expressible: the endpoint can return a narrower
public.subscribefor a subscriber certificate, or refuse outright, using the same reply shape it already has.
publishunscoped to an mTLS peer by default, or making the grantconfigurable separately from
subscribe. A subscriber certificate that can publish into the tenantit is reading is a larger grant than the deployment probably intends.
--server-tls-rootis set alongside--auth-api. The combination is notwrong, but it means the API's answer is advisory for one class of peer and binding for the other,
and that is worth one line on stderr. We are not claiming a precedent for this: an earlier
version of this issue said the relay already warns about configurations where nothing can
authenticate, and that was wrong. It does not. Every
tracing::warn!inAuth::newonfd4f5d82eis a deprecation notice for a superseded flag (--auth-tls-*,--auth-key-dirwith aURL,
--auth-public-api), and the remaining checks areanyhow::ensure!hard errors on mutuallyexclusive flags. There is no existing diagnostic for a configuration that is merely hazardous, so
this asks for a new class of warning rather than one more instance of an established one — which
may make it a larger ask than (1) or (2), and it should be judged on the hazard rather than on
consistency with something that isn't there.
We are happy to prepare the
AuthApiRequestchange if the direction is acceptable; the reply-sidecontract needs no change for (1), only the request.
What we are not claiming
It is the cost of ignoring that split, and the point is that the relay does not make the split
enforceable.
interacts with cluster-peer trust is not something we can speak to.
de-provisioning behaviour to measure, so we cannot say how quickly withdrawing a certificate takes
effect.