fix(pg-pkg): give each client its own client_version budget - #390
Merged
Conversation
`postguard_clients` held one process-wide set of seen `client_version` values capped at 64, shared by every client. That budget does not cover the fleet: `@e4a/pg-js` has published 55 versions and `E4A.PostGuard` 9, so one SDK's releases can spend the slots the other's real releases then miss, and every later release reads as `other` until the process restarts. `other` is only alertable while nothing legitimate lands there. One set per client now, still 64 each. The map is keyed on the post-allowlist `client` value, which is a closed set of 8, so the worst case is bounded at 8 x 64; keying it on the raw header field would hand out a fresh budget for every client name an attacker invents. `unknown` and `other` as `client_version` values still consume no slot, in any bucket, and a request whose `client` resolves to `other` or `unknown` spends that bucket like any other. The cap test now reads one client's versions off the exposition, and a second test fills one client's bucket and asserts a different client's fresh version is still emitted exactly. That second test is what fails if the map goes back to a shared set. Refs #388
Two points from review, both on the cap's design record. The rewritten doc comment dropped two facts that are still true per bucket: why 64 suffices at all (a process sees the versions in live use, not every version ever released) and the residual failure mode (once a bucket fills, that client's later real releases read as `other` until a restart). COMPATIBILITY.md's deprecation step 3 reads this metric as "no traffic", so a filled bucket is a wrong answer to that question, and this comment was the only place the caveat was written down. The comment singles out the key choice as what bounds memory, but no test held it: keying the budget on the raw header field again passes the whole suite. test_invented_client_names_share_one_budget closes that — it is red under exactly that mutation and green as shipped, while both existing tests pass either way because they only use allowlisted client names, where the raw and allowlisted keys coincide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rubenhensen
approved these changes
Aug 31, 2026
Open
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.
Closes #388.
postguard_clientskept one process-wide set of seenclient_versionvalues, capped at 64 and shared by every client. That budget does not cover the fleet:@e4a/pg-jshas published 55 versions andE4A.PostGuard9, which is 64 between those two SDKs before either add-in or the CLI appears. A busy deployment fills the set from live traffic and then reports every later real release asotheruntil the process restarts, andotheris only alertable while nothing legitimate lands there.This was decided on #384 and asked for in a comment there, but that PR merged as written.
What changed
All of it in
pg-pkg/src/middleware/metrics.rs.SEEN_CLIENT_VERSIONSis now aHashMap<String, HashSet<String>>: one set per client, still 64 each.clientvalue, which is a closed set of 8 (pg-js,pg-dotnet,pg4ol,pg4tb,cli,pg-cli,unknown,other), so the worst case is bounded at 8 x 64. Keying it on the raw header field would hand out a fresh 64-slot budget for every client name an attacker invents.client_version_labeltakes that allowlisted value as its first argument, socollect_metricscomputesallowlisted(client, KNOWN_CLIENTS)before it builds the label array rather than inside it.unknownandotherasclient_versionvalues still consume no slot, in any bucket. A request whoseclientresolves tootherorunknownspends that bucket like any other.@e4a/pg-jsversion count as headroom.Untouched: the
host/clientallowlists and their contents, theunknown/othersemantics, the shape gate onclient_version, the five labels,server.rs,util.rs,cryptify/src/metrics.rs,COMPATIBILITY.md,.github/workflows/.Tests
Both in the existing
test_get_metricsmodule, driving the real actix app as its neighbours do.test_client_version_cap_holds, rewritten: 70 distinct well-shaped versions for one client, then that client's distinctclient_versionvalues read off the exposition text are 64 plusother.test_a_full_client_does_not_spend_another_clients_budget, new: fillpg-js's bucket to 64, send a fresh well-shaped version aspg-dotnet, assert it is emitted exactly. Same host, path and status on both, so the client is the only dimension that differs.Test 1 passes under both designs, so test 2 is the only thing that distinguishes them. Reverting the map to a single shared set locally, keeping both tests:
Restored afterwards; the branch holds the map.
Acceptance check
Reviewed by dobby: the
code-commentsrule (clean) plus a correctness pass over the whole diff, which raised 2 non-blocking findings, both fixed in bdd2305 — the per-bucket caveat restored to the cap's doc comment, and a regression test added for the budget-key choice (red under the raw-key mutation the rest of the suite passes) —cargo testgreen across the workspace (pg-pkg 72/3/2/5, 0 failures),cargo clippy -- -D warningsandcargo fmt --checkclean — approve.