Skip to content

fix: bound the JWKS refresh rate and reject an unknown symbol policy (ADR 0066) - #104

Merged
kfeofantov merged 1 commit into
mainfrom
fix/bound-jwks-refetch-and-validate-policy
Sep 4, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
fix/bound-jwks-refetch-and-validate-policy

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Two places where input the agent does not trust decided something quietly.
Reasoning, alternatives and what changes for an operator are in
ADR 0066.

The JWKS refetch

jwt.ParseWithClaims asks for the key before it checks the signature, and
the handler calls Verify before it reads the body. So the kid comes out
of a token nobody has verified, and each distinct value a caller invented cost
two GETs to the API server — discovery and JWKS — serialized behind
CachingKeySource's one mutex with a ten-second ceiling each. The refresh rate
was a property of the caller.

Refreshes now happen at most once per 30s. The stamp is taken before the
attempt, so a refresh that failed is rate-limited on the same floor — an
unreachable API server is where retrying per request costs the most and helps
least. Against the node's one-minute scan interval, a key rotation is picked up
at most one round late, on a report the next pass re-sends anyway (ADR 0003).

The chart's NetworkPolicy already bounds who can reach the port (ADR 0039); that
is who, not how often, and a policy the CNI does not enforce is the case worth
surviving.

The symbol policy

profiling.thirdPartySymbols was compared to "keep" and everything else took
the other branch — so "Keep", "true" and every typo silently meant drop.
Failing closed is the right direction and the wrong report: this field governs
the allow-list deciding which frames may leave the node (ADR 0011 §4), and it is
the one control an operator cannot check by reading their own values file. An
unrecognized value now fails the start, the same shape as ADR 0040's rule for an
unset expectedSubject. Empty stays valid — it selects the default and is what
a default install renders.

One test had to change to keep proving its claim

TestAFailedRefreshKeepsTheKeysAlreadyInHand passed unchanged, but for the
wrong reason: with the floor in place its second lookup no longer attempted a
refresh at all, and "the refresh failed" and "no refresh was made" leave the
server's hit count identical. It now advances the clock past the floor and
counts attempts on the source itself.

Also carried here

The .gitignore fix that #103 was meant to land and did not — I staged it and
then ran a stray git stash/git stash pop before committing, which unstages,
so only internal/model/coverage.go went in. origin/main still has an
unanchored coverage.* that matches a source file in any directory and swallows
it in silence. One line, and it belongs on main more than it belongs in a third
PR of its own.

Checks

$ go test -race ./...        # green
$ golangci-lint run ./...    # 0 issues
$ go vet -tags e2e ./...     # clean
$ make chart-lint vulncheck  # clean

No docs/security.md change: nothing collected, stored or transmitted differs.

🤖 Generated with Claude Code

https://claude.ai/code/session_011YRKuL97KRndaopfpixrhp

…(ADR 0066)

Two places where input the agent does not trust decided something quietly.

The JWKS refetch. jwt.ParseWithClaims asks for the key before it checks the
signature, and the handler verifies before it reads the body — so the kid comes
from an unverified token and an unauthenticated caller chose how often the
controller queried its own API server, two GETs at a time, serialized behind one
mutex with a 10s ceiling each. Refreshes now happen at most once per 30s, and
the stamp is taken before the attempt so a failing refresh is bounded too. The
cost is a rotation picked up up to one node scan round late, on a payload that
is re-sent by the next pass.

The symbol policy. profiling.thirdPartySymbols was compared to "keep" and
everything else took the other branch, so "Keep" and every typo silently meant
"drop". It governs which frames may leave the node, so an unrecognized value now
fails the start, the same shape as ADR 0040's rule for an unset expectedSubject.

Also carries the .gitignore fix that PR #103 was meant to land and did not: an
unanchored `coverage.*` still matches a source file in any directory.

An existing test needed the clock advanced to keep proving its own claim: with
the floor in place, "the refresh failed" and "no refresh was attempted" look
alike from outside, so it now counts attempts on the source.

go test -race ./... green; golangci-lint 0 issues; chart-lint and vulncheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YRKuL97KRndaopfpixrhp
@kfeofantov
kfeofantov merged commit de80d2b into main Sep 4, 2026
3 checks passed
@kfeofantov
kfeofantov deleted the fix/bound-jwks-refetch-and-validate-policy branch September 4, 2026 06:29
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.

1 participant