feat(jwtutil): RS256 signing + JWKS-based key resolution - #21
Merged
Conversation
…AC secret CreateToken now takes a KeyID + RSA private key and signs with RS256, stamping the key ID into the "kid" header. Validate/ValidateWithKeySource verify against an RSA public key and reject any non-RSA signing method up front, so algorithm confusion against a symmetric secret is no longer possible. Adds JWKSKeySource (jwtutil/jwks.go), a KeySource that resolves keys by "kid" from a remote JWKS document (authn's /.well-known/jwks.json), with lazily fetched, cached, and rotation-tolerant key resolution: a stale-but-known key is served if a refetch fails, and refetches are floored by a minimum interval so an unknown kid cannot become a request-rate stampede. Key-resolution failures surface as ErrKeyUnavailable, distinct from ErrTokenInvalid, so HTTP callers can answer 503 (retry) instead of 401 (re-authenticate) when the fault is ours and not the token's. server/use.UserConfig and e2e.SignUp move from a shared signing string to this KeySource-based API.
2 tasks
| return func(next http.Handler) http.Handler { | ||
| fn := func(wri http.ResponseWriter, req *http.Request) { | ||
| if keys == nil { | ||
| response.InternalError(wri, fmt.Errorf("no JWT key source configured")) |
| // credential: answer 503 so the client retries instead of | ||
| // re-authenticating against an authn that is simply down. | ||
| if errors.Is(err, jwtutil.ErrKeyUnavailable) { | ||
| response.ServiceUnavailable(wri, err) |
braghettos
added a commit
to krateo-platformops/authn
that referenced
this pull request
Aug 13, 2026
* fix(testdata): oauth RESTAction groups step must wait on userInfo The "groups" step reuses .token, which userInfo's response sets on the shared step context; without dependsOn the two can run out of order and groups fires before .token exists. * chore(deps): bump plumbing to pick up RS256/JWKS jwtutil API Pinned to the krateo-platformops/plumbing@feat/rs256-jwks-keysource commit pending release; re-pin to the tagged version once krateo-platformops/plumbing#21 is merged and tagged. * feat: sign JWTs with RS256 and publish the public key via JWKS authn signs access tokens with an RSA private key (RS256) instead of a shared HMAC secret, and publishes the matching public key as a JWKS at GET /.well-known/jwks.json (new internal/routes/jwks package). Every route that mints a token now carries the key through as PrivateKey+KeyID instead of a plain SigningKey string. The private key is read from a mounted file (--jwt-sign-key-file / JWT_SIGN_KEY_FILE), never from an env var, and authn now fails fast at startup (before any route is registered) if the key ID or key file is missing or unparseable — the old design failed silently per-request into a tokenless response instead. Chart: the Secret (authn-jwt-signing-key, key private.pem) is mounted as a volume rather than pulled in via envFrom; jwtSignKeySecretName becomes the jwt.{signKeySecretName,signKeySecretKey,mountPath,kid} block. Consumers that verify authn's tokens (snowplow, etc.) get the public key from the JWKS endpoint, not from a shared Secret — see docs/jwt-jwks.md. * fix(docs): jwt-jwks.md frontmatter type must come from the docs-standard registry "Reference" isn't a registered OKF type (see .github/DOCS-STANDARD.md's lint-docs TYPES set); "Integration" is the closest existing fit and matches rbac.md's precedent for an extension doc. Also list the file in llms.txt alongside rbac.md. * chore(deps): bump plumbing to v1.14.0 (released RS256/JWKS jwtutil) --------- Co-authored-by: Diego Braga <diego.braga86@gmail.com>
braghettos
added a commit
to krateo-platformops/snowplow
that referenced
this pull request
Aug 13, 2026
#146) * chore(deps): bump plumbing to pick up RS256/JWKS jwtutil API Pinned to the krateo-platformops/plumbing@feat/rs256-jwks-keysource commit pending release; re-pin to the tagged version once krateo-platformops/plumbing#21 is merged and tagged. * feat: verify JWTs against authn's JWKS instead of a shared signing key snowplow no longer holds a copy of authn's signing key. It resolves RS256 verification keys by "kid" from authn's JWKS endpoint (GET /.well-known/jwks.json) via a cached jwtutil.JWKSKeySource, defaulting to <url-authn>/.well-known/jwks.json unless --jwks-url/JWT_JWKS_URL overrides it. Fetches are lazy (first validation, not at boot) and cached, so snowplow starts and serves unauthenticated routes even if authn isn't up yet, and a key rotation on authn's side needs no snowplow redeploy. middleware.UserConfig and middleware.RefreshAuth take a jwtutil.KeySource in place of the raw signing-key string; the snowplow-local UserConfig mirror of plumbing's use.UserConfig (AC-D3.14 provenance guard) is re-audited line-by-line against upstream's new KeySource-based flow and re-pinned. Chart: helm/snowplow's jwtSignKeySecretName and its envFrom secretRef are gone; jwt.{jwksUrl,cacheTTL,minRefreshInterval,requestTimeout} configures the JWKS client instead. No Secret is mounted. * chore(deps): bump plumbing to v1.14.0 (released RS256/JWKS jwtutil) * test: fix e2e JWT setup for RS256 — use PEM RSA keys, not the HMAC secret The RS256 migration made plumbing's e2e.SignUp expect a PEM-encoded RSA private key (JWTSignKey) + a JWTKeyID, but six test setups still passed the old symmetric secret "abbracadabbra", so they failed with e2e.go:50: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key in TestResolveAPI, TestResolveWidgets(_Extras), TestGet, TestRESTAction, TestCallHandler. Generate an inline PKCS1 PEM RSA key per test (matching the already-fixed internal/rbac/rbac_test.go) and pass JWTSignKey + JWTKeyID: "test-kid". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJsLqtryCgWwEt8FnPE1se * chore: re-pin PinnedPlumbingVersion to v1.14.0 (plumbing #21 tagged) --------- Co-authored-by: Diego Braga <diego.braga86@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
CreateTokensigns with RS256 (RSA private key +KeyID) instead of HS256 over a shared secret; the key ID is stamped into the token'skidheader.Validate/newValidateWithKeySourceverify against an RSA public key and reject any non-RSA signing method before the key is even consulted — closes the algorithm-confusion path a shared-secret scheme is exposed to.JWKSKeySource(jwtutil/jwks.go) resolves verification keys bykidfrom a remote JWKS document (e.g. authn'sGET /.well-known/jwks.json): lazy first fetch, TTL + min-refresh-interval caching, and stale-key fallback on a failed refetch. Key-resolution faults surface asErrKeyUnavailable(distinct fromErrTokenInvalid) so an HTTP caller can answer 503/retry instead of 401/re-auth when the fault is ours, not the token's.server/use.UserConfigande2e.SignUpmove from a shared signing string to thisKeySourceAPI.Why
Foundation for the krateo asymmetric-key migration: authn will sign with an RSA private key and publish the public key via JWKS; consumers (snowplow, etc.) verify against that JWKS instead of sharing a symmetric secret with authn.
Consumers
authnandsnowplowboth currently pingithub.com/krateo-platformops/plumbing v1.13.0and already contain code written against this new API (in separate, still-unmerged PRs). Theirgo.mod/go.sumneed bumping to whatever tag this PR is released as, before those PRs can build — see the cross-repo ordering note left on those PRs.Test plan
go build ./...go test ./jwtutil/... ./server/use/...(fullgo test ./...has one pre-existing, unrelatedgo vetfailure inendpointsonmain)