Repository navigation
Managed role provisioning sends SCRAM-SHA-256 verifiers, never a password, and only when one changes (#3910) - #3940
Merged
Conversation
…d only when one changes (#3910) Managed provisioning interpolated every role's plaintext password into CREATE ROLE and ALTER ROLE ... PASSWORD literals on every start, so any surface that records statement text could capture the live credentials: an unmarked same-named role made the DO block raise every start, and log_min_error_statement wrote the whole batch, passwords included, to the store's own log, which the store-log sweep keeps for 400 days and the viewer role reads through get_store_log. - ScramSha256Verifier computes PostgreSQL's SCRAM-SHA-256 verifier client-side (PBKDF2-HMAC-SHA-256 plus the RFC 5802/7677 keys). PostgreSQL stores a verifier as-is, which is what psql's \password does. - BuildProvisioningSql takes verifiers and REFUSES anything else, a plain password first of all, so no future caller can send one again. - EnsureProvisionedAsync reads pg_authid and re-asserts a role's password only when its stored verifier no longer accepts the credential file, so a steady-state start sends no PASSWORD clause at all. - Docs: the README, provision-roles.sql (verifier or \password for BYO) and the #3899 comments that still said provisioning sends passwords every start. Tests: the RFC 7677 worked exchange; the builder's refusals and per-role re-assert; the re-assert plan; and a live test on the bundled runtime that runs the real provisioning twice. It checks the derivation against the server in both directions and the unmarked-role failure path (the log carries verifiers, never a password). It also checks that every role logs in with its credential file, and that a second start leaves the stored verifiers byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif
erikdarlingdata
enabled auto-merge (squash)
September 22, 2026 23:50
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…3920, #3927, #3931, #3932, #3940, #3942, #3946, #3947, #3950, #3952, #3955, #3956, #3957, #3964, #3965, #3966, #3968, #3972, #3975, #3979, #3980, #3981, #3983, #3984, #3985) (#3989) The wave's fix PRs deliberately carried no CHANGELOG edits (parallel-agent hot-spot protocol); each agent reported its entry and this commit lands them together, byte-verified against origin/dev. Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv Co-authored-by: Claude Fable 5 <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.
Closes #3910.
CHANGELOG: this PR carries no CHANGELOG edit. Its entry lands with the batched splice.
Why
Managed role provisioning sent every role's plaintext password on every service start:
CREATE ROLE ... PASSWORD '<pw>'inside the guard's DO block, thenALTER ROLE admin/viewer/mcp LOGIN NOSUPERUSER PASSWORD '<pw>'. Any surface that records statement text could capture the live credentials of all three login roles.The concrete path: an unmarked role with the same name (an operator-created
mcp, say) makes the DO block raise on every start. The defaultlog_min_error_statement = errorthen writesSTATEMENT:and the whole batch, all three passwords included, to the store's own log. The store-log sweep kept that for 400 days, and theviewerrole reads it throughget_store_log. #3899'strack_utility = offand #3920's log masking each close one surface. This fix removes the plaintext at the source.What changes
ScramSha256Verifiercomputes PostgreSQL's SCRAM-SHA-256 verifier client-side: PBKDF2-HMAC-SHA-256 plus the RFC 5802/7677 keys, a 16-byte random salt, and 4096 iterations. PostgreSQL stores a string already in that format as-is, which is what psql's\passworddoes. The password never appears in any statement. A leaked verifier is not a login: the passwords are 32 random alphanumerics.BuildProvisioningSqltakes verifiers and refuses anything else, a plain password first of all. That is where "never the password" is enforced, not just hoped for; the verifier's shape admits no quote, so the interpolation stays escaping-safe.EnsureProvisionedAsyncreadspg_authid.rolpassword(it is the superuser owner) and re-asserts a role's password only when the stored verifier no longer accepts the credential file. That happens for a missing role, an MD5 hash from an older store, or a regenerated file. TheCREATEbranch still carries a fresh verifier, for a role that does not exist yet. The start log says which roles were re-asserted.provision-roles.sql, which now recommends a verifier or psql's\passwordso a BYO store's passwords stay out of every statement too; and the Field stores cannot tell us which query is slow: preload pg_stat_statements in the managed store and surface it by role #3899 comments that said provisioning sends passwords every start.Test plan
v=, and the server's client check (SHA-256 of ClientProof XOR ClientSignature equals StoredKey) holds for the derived keys.DARLING_TEST_PGRUNTIME), the realEnsureProvisionedAsyncon a migrated cluster:Darling.Tests(12,503) andLite.Tests(5,124) after rebasing onto The store's statement text and store log keep no literal: a one-pass SQL lexer, an allowlist reader, masking for every retained log class, and stricter conf parsing (#3899) #3920 and Store-object convergence re-runs hourly on a plain-PostgreSQL store too, not only at restart (#3913) #3932. The only Darling failures are the three runtime-version pins my local rig (18.4) can't meet against dev's 18.6 pin. The one Lite failure was the knownAnalysisPassTokenThreadingTestsflake, which passes on re-run; this PR touches no Lite code.🤖 Generated with Claude Code
https://claude.ai/code/session_018Z4T51J4E6L8fjriPHpsif