Repository navigation
Add dcrConfig.rotationKey to re-register upstream DCR clients - #6825
Merged
Merged
Conversation
aron-muon
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
blkt,
jerm-dro,
jhrozek,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
October 14, 2026 16:41
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6825 +/- ##
=======================================
Coverage 79.68% 79.68%
=======================================
Files 815 815
Lines 82940 82955 +15
=======================================
+ Hits 66088 66100 +12
- Misses 16847 16850 +3
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
An embedded auth server resolves its upstream RFC 7591 registration once per process start and then serves the cached client_id from the credential store forever: the only invalidation is client_secret_expires_at, which public-client upstreams never send. When the upstream drops the registration, the rejection surfaces only in the user's browser, the proxy keeps presenting the dead client_id on every start, and the sole remedy is deleting the Redis row by hand and restarting. Add dcrConfig.rotationKey, an optional operator-chosen label that is folded into the DCR cache identity (DCRKey, the Redis key, the persisted row, the memory map key and the singleflight key) only when non-empty and is never sent to the upstream. Bumping it makes the next start register a fresh client; an empty value addresses exactly the pre-existing key, so existing rows need no migration. The run-config validates the same alphabet and length the CRD markers enforce, and the cache-hit and registration logs carry the key so operators can confirm which generation a replica runs. Related to stacklok#6821 (automatic recovery), stacklok#6496, stacklok#6508. Signed-off-by: Aron Gates <aron@muonspace.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aron-muon
force-pushed
the
dcr-rotation
branch
from
October 14, 2026 16:55
3f639a7 to
dd6ef24
Compare
4 of 10 tasks
jhrozek
approved these changes
Oct 15, 2026
1 task
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
An embedded auth server that registers its upstream OAuth client through RFC 7591
Dynamic Client Registration resolves the registration once per process start and
serves it from the credential store on every later start. The only invalidation
is the upstream's
client_secret_expires_at, which public-client upstreams neversend, so when the upstream drops or invalidates the registration the proxy keeps
presenting the dead
client_idindefinitely. The rejection happens in the user'sbrowser on the upstream's
/authorizepage, so nothing in ToolHive observes it.Today the only remedy is deleting the
dcr:row from Redis by hand andrestarting (#6821 covers the automatic recovery path; this change adds the
operator-driven one, which is also needed when the upstream does not
authenticate clients before validating grants or when a rotation is wanted on
request).
dcrConfig.rotationKey, an optional operator-chosen label(
^[A-Za-z0-9._-]{1,64}$) onMCPExternalAuthConfigupstream providers andon the
authserver.DCRUpstreamConfigrun-config. It is never sent to theupstream.
storage.DCRKey,redisDCRKey,the memory-store map key, the persisted row and the resolver's singleflight
key) only when non-empty. Changing it makes the next start register a fresh
client; an empty value addresses exactly the pre-existing key, so existing
rows stay reachable and no migration is needed.
dcr: cache hitanddcr: registered new clientrecords so an operator can confirm which generation a replica runs.
for YAML/CLI configs) and regenerate CRDs and CRD API docs.
Related to #6821, #6496, #6508.
Type of change
Test plan
task test) — every package touched here passes with-race(
pkg/auth/dcr,pkg/authserver,pkg/authserver/storage,pkg/authserver/runner,cmd/thv-operator/pkg/controllerutil,cmd/thv-operator/api/...); the remainingfailures on the dev machine are pre-existing environment failures in untouched
packages (in-cluster runtime detection, root user). New coverage:
TestResolveDCRCredentials_RotationKeyForcesFreshRegistration,TestFlightKeyOf_RotationKey,TestRedisStorage_DCRCredentials_RotationKeyRoundTrip,the
redisDCRKey with rotation keyformat case, rotation-key entries in thememory/store distinct-key tests, run-config validation cases,
newDCRRequestpropagation and the operator CRD-to-run-config conversion.
task test-e2e)task lint-fix)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.rotationKeyis a new optional string field onDCRUpstreamConfig; existingobjects are unaffected. The persisted row gains an omitempty field and the Redis
key format is unchanged for an empty rotation key.
Changes
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.gorotationKeyonDCRUpstreamConfigwith Pattern/MaxLength markerscmd/thv-operator/pkg/controllerutil/authserver.gopkg/authserver/config.gorotation_keyonDCRUpstreamConfig; alphabet/length validationpkg/authserver/runner/dcr_adapter.godcr.Requestpkg/auth/dcr/request.go,resolver.go,store.goRequest.RotationKey; key construction, singleflight key, log attributespkg/authserver/storage/types.go,redis_keys.go,redis.goDCRKey.RotationKey; key encoding when non-empty; persistedkey_rotation_keydeploy/charts/operator-crds/**,docs/operator/crd-api.mddocs/arch/11-auth-server-storage.mdDoes this introduce a user-facing change?
Yes. Operators can set
dcrConfig.rotationKeyon an embedded auth server'supstream provider (CRD or YAML run-config). Changing the value makes the proxy
register a new client with the upstream at its next start instead of reusing
the cached registration. The previous registration's row stays in storage, and
on upstreams that bind refresh tokens to the client, users signed in through the
previous client re-authenticate once.
Special notes for reviewers
path (probe the cached client at startup, replace the row) is being handled
under Cached upstream DCR registration never recovers after the upstream evicts it #6821; the two do not touch the same lines and can land in either
order. A time-based
maxRegistrationAgewould also fit on top of that work.previous key value, and the row carries no TTL unless the upstream asserted
client_secret_expires_at, which matches the existing behaviour for anykey-format change (see the note in
redis_keys.go).RotationKeyis appended to the Redis key and the singleflight key as alength-prefixed segment after the hex scopes hash, so the empty and non-empty
shapes cannot be confused.
🤖 Generated with Claude Code