Repository navigation
Require a trust mode for private_key_jwt delegates - #7004
Merged
Merged
Conversation
jhrozek
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
October 30, 2026 11:28
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7004 +/- ##
=======================================
Coverage 80.37% 80.38%
=======================================
Files 849 849
Lines 86715 86720 +5
=======================================
+ Hits 69701 69706 +5
Misses 17009 17009
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ChrisJBurns
previously approved these changes
Oct 30, 2026
A delegate client may exchange a user's self-issued ToolHive token for
a delegated token asserting that user's sub. Its token_exchange block
picks which subject tokens it accepts:
any every self-issued token, for any user, whichever client
originally obtained it
restricted only tokens issued to the client itself or to one of
its allowed_source_clients
If the delegate is compromised, its private key is stolen, or a bug
hands it tokens meant for other clients, "any" lets it turn every
token it can get into a delegated token for its own audiences. Scope
and audience narrowing and the lifetime cap still apply in both modes;
there is no single-use enforcement in either. The only signal for
blanket trust is a startup WARN.
A client that omits the block gets "any". Only clients that set
grant_types were required to declare the block, so a private_key_jwt
client without grant_types still inherited blanket trust silently, for
example when a templating step dropped the block.
Require the block for private_key_jwt clients too. The implicit legacy
rule now applies only to secret-authenticated clients without
grant_types, the shape that predates the newer client metadata:
client before after
secret, no grant_types, no block implicit any implicit any
secret, grant_types, no block startup error startup error
private_key_jwt, grant_types, no block startup error startup error
private_key_jwt, no grant_types,
no block implicit any startup error
any client, block declared declared mode declared mode
The startup WARN marked a client explicit_any=true only when it set
grant_types, on the reasoning that such a client had to declare its
mode. A private_key_jwt client now also has to, so the WARN marks it
explicit_any=true as well, instead of reporting it like a client that
inherited the legacy rule. Both checks share trustModeRequired so they
cannot drift apart. redeems_id_jag still depends only on grant_types
and the JWT-bearer grant.
A private_key_jwt delegate with neither grant_types nor a
token_exchange block, accepted since v0.59.0, now fails validation at
startup. Add token_exchange.self_issued_subject_tokens: any to keep the
old behaviour, or restricted to narrow it.
Co-authored-by: Mecatl <noreply@mecatl.dev>
jhrozek
force-pushed
the
stable-client-v2-f-1
branch
from
October 30, 2026 15:58
48f8fd6 to
4e8a286
Compare
ChrisJBurns
approved these changes
Oct 30, 2026
ChrisJBurns
approved these changes
Oct 30, 2026
4 of 11 tasks
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.
Summary
A delegate client can exchange a user's self-issued ToolHive token for a delegated token asserting that user's
sub. Itstoken_exchange.self_issued_subject_tokensmode decides which subject tokens it accepts:anytakes every self-issued token for any user, whichever client obtained it;restrictedtakes only tokens issued to the client itself or to itsallowed_source_clients. A client that omits the block getsany, and the only signal is a startup WARN.Only clients that set
grant_typeshad to declare the block. Aprivate_key_jwtdelegate (added in #6952, released in v0.59.0) withoutgrant_typesstill inherited blanket trust silently, for example when a templating step dropped the block.private_key_jwtis part of the newer client metadata, so no pre-existing config depends on that fallback.requireDeclaredTrustModenow requires thetoken_exchangeblock forprivate_key_jwtdelegates too, on both theRunConfigand the resolvedConfigvalidation paths. The implicit legacy rule stays only for secret-authenticated clients withoutgrant_types.explicit_any=trueonly when it setgrant_types. It now also marksprivate_key_jwtclients, which must declare their mode as well, instead of reporting them like clients that inherited the legacy rule. Both checks share a newtrustModeRequiredhelper so they cannot drift apart.redeems_id_jagis now set independently ofexplicit_any, still fromgrant_typesand the JWT-bearer grant alone.docs/arch/17-token-exchange-delegation.md, theDelegateClientRunConfig.TokenExchangedoc comment and the generated swagger/SDK schemas describe the new rule.grant_types, no blockany+ WARNany+ WARNgrant_types, no blockprivate_key_jwt,grant_types, no blockprivate_key_jwt, nogrant_types, no blockany+ WARNPart of #6893
Type of change
Test plan
task test)task test-e2e)task lint-fix)Unit tests were scoped to
pkg/authserver, the only package with logic changes, using the Taskfile'sgo test -raceflags. New cases cover aprivate_key_jwtclient without the block (rejected on both validation paths) and aprivate_key_jwtclient with explicitanyinTestWarnBlanketSelfIssuedExchange(explicit_any=true, plusredeems_id_jag=truewhen JWT-bearer is enabled).task lint-fixreports 0 issues.Does this introduce a user-facing change?
Yes. Before: v0.59.0 accepted a
private_key_jwtdelegate client with neithergrant_typesnor atoken_exchangeblock and gave it blanketanytrust. After: the auth server refuses to start withdelegate_clients: client_id "<id>": private_key_jwt clients must declare a token_exchange block with self_issued_subject_tokens ("any" or "restricted"). To keep the old behaviour, addtoken_exchange: {self_issued_subject_tokens: any}; to narrow it, userestrictedwithallowed_source_clients.Also: the startup WARN for a
private_key_jwtdelegate inanymode now carriesexplicit_any=true.Special notes for reviewers
private_key_jwtdelegates yet, so there is no CEL rule to update here. When the CRD gainstokenEndpointAuthMethod, its CEL rule (grantTypesrequirestokenExchange) must also requiretokenExchangeforprivate_key_jwt, so the CRD cannot produce a config the server refuses.TestIntegration_JWTBearerGrant_NoJTISyntheticKeyDoesNotCollideWithJTI(400 invalid_grant, "assertion invalid or could not be verified"). That test configures no delegate clients and does not reach the changed code; it passed 3/3 in isolation and the next full-package run passed. It looks like a pre-existing flake under-raceload.🤖 Generated with Claude Code