feat: support SCRAM auth for Postgres#8304
Conversation
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request implements PostgreSQL SCRAM-SHA-256 authentication support across the auth and server modules, including a new verifier, trait methods for retrieving Postgres auth info, and the SASL handshake logic in the Postgres server handler. A critical security review comment highlights a potential CPU exhaustion DoS and timing leak vulnerability when handling unknown users, where expensive PBKDF2 computations are performed on the fly. The reviewer suggests directly generating random keys to mitigate this issue.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let salt = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>(); | ||
| let password = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>(); | ||
| let verifier = PgScramSha256Verifier::from_password( | ||
| &password, | ||
| &salt, | ||
| crate::DEFAULT_PBKDF2_SHA256_ITERATIONS, | ||
| )?; | ||
| Ok(PgAuthInfo::ScramSha256 { | ||
| verifier, | ||
| user_info: None, | ||
| }) |
There was a problem hiding this comment.
Security Issue: CPU Exhaustion DoS & Timing Leak for Unknown Users
For unknown users, the current implementation generates a random password and derives a SCRAM verifier on the fly using PgScramSha256Verifier::from_password. This internally runs PBKDF2-HMAC-SHA256 with 4096 iterations.
This introduces two significant issues:
- Denial of Service (DoS): An attacker can flood the server with connection requests using random/unknown usernames. This forces the server to perform expensive PBKDF2 computations for every single request, leading to 100% CPU exhaustion and rendering the database unavailable.
- Timing Leak / Username Enumeration: If the server is configured with hashed credentials (e.g.,
pbkdf2_sha256orpg_scram_sha256), verifying a known user is extremely fast (microseconds) because the salted password is already stored. However, verifying an unknown user takes milliseconds because of the on-the-fly PBKDF2 derivation. This timing difference allows an attacker to easily enumerate valid usernames.
Solution
Instead of deriving a verifier from a random password, we can directly generate a completely random stored_key and server_key of 32 bytes. This avoids running PBKDF2 entirely for unknown users, making the handshake extremely fast and matching the performance of known hashed users.
let salt = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>().to_vec();
let stored_key = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>().to_vec();
let server_key = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>().to_vec();
let verifier = PgScramSha256Verifier::new(
crate::DEFAULT_PBKDF2_SHA256_ITERATIONS,
salt,
stored_key,
server_key,
)?;
Ok(PgAuthInfo::ScramSha256 {
verifier,
user_info: None,
})There was a problem hiding this comment.
Pull request overview
Implements SCRAM-SHA-256 authentication for the PostgreSQL wire protocol, allowing PostgreSQL clients to authenticate without sending cleartext passwords when all configured users support SCRAM.
Changes:
- Adds a SCRAM-SHA-256 verifier type and
pg_scram_sha256:<iterations>:<salt>:<stored_key>:<server_key>credential format, plus helpers to derive/format verifiers. - Extends
UserProviderwithpostgres_auth_info()and implements it for static and watch-file providers to decide between SCRAM and cleartext fallback. - Implements the server-side SASL SCRAM exchange (
SASL→SASLContinue→SASLFinal) in the Postgres auth handler and adds integration/unit tests.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/servers/tests/postgres/mod.rs | Adds SCRAM auth integration test coverage and a helper to start a server with a specific user provider. |
| src/servers/src/postgres/auth_handler.rs | Adds SCRAM SASL authentication state machine and message parsing/verification. |
| src/auth/src/user_provider/watch_file_user_provider.rs | Implements postgres_auth_info() for watch-file user provider. |
| src/auth/src/user_provider/static_user_provider.rs | Implements postgres_auth_info() for static user provider. |
| src/auth/src/user_provider.rs | Introduces PgAuthInfo, adds SCRAM verifier support to credential parsing, and SCRAM-capability logic. |
| src/auth/src/lib.rs | Re-exports SCRAM-related auth types/helpers. |
| src/auth/src/common.rs | Adds PgScramSha256Verifier implementation and formatter helper. |
| src/auth/Cargo.toml | Adds new dependencies needed for SCRAM (e.g., hmac, rand). |
| config/standalone.example.toml | Documents the new pg_scram_sha256 credential verifier format and Postgres cleartext fallback behavior. |
| config/frontend.example.toml | Documents the new pg_scram_sha256 credential verifier format and Postgres cleartext fallback behavior. |
| config/config.md | Updates configuration documentation to include the new verifier format and fallback notes. |
| Cargo.lock | Updates lockfile for new dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
|
|
||
| let login_info = LoginInfo::from_client_info(client); | ||
| let server_nonce = BASE64.encode(rand::random::<[u8; 18]>()); |
| if client_final.channel_binding != channel_binding { | ||
| return Ok(PgAuthenticationResult::Failed); | ||
| } | ||
|
|
| let salt = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>(); | ||
| let password = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>(); | ||
| let verifier = PgScramSha256Verifier::from_password( | ||
| &password, | ||
| &salt, | ||
| crate::DEFAULT_PBKDF2_SHA256_ITERATIONS, | ||
| )?; |
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
- Verify the client-final nonce matches the server-issued nonce, per RFC 5802 transcript validation, instead of only checking the channel-binding field. - Replace the per-connection PBKDF2 over a random password for unknown users with a deterministic mock verifier keyed by the username and a process-wide secret. This avoids a CPU-exhaustion DoS on unknown usernames and removes a username-enumeration oracle: the SCRAM server-first salt and iteration count are now stable per username and indistinguishable from a real user, with no PBKDF2 cost and random keys that never accept a proof. Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
| fn to_pg_scram_sha256_verifier(&self) -> Option<PgScramSha256Verifier> { | ||
| match self { | ||
| PasswordVerifier::PlainText(password) => { | ||
| let salt = rand::random::<[u8; PG_SCRAM_SHA256_KEY_LEN]>(); |
There was a problem hiding this comment.
Plaintext-backed users still look different from unknown users here. For an existing plain: or unprefixed credential, this path runs PBKDF2 and generates a fresh s= value on every startup, while mock_for_unknown_user() is cheap and stable per username. Both the server-first timing and salt are observable before proof verification, so the common static_user_provider:cmd:user=password case still exposes a username oracle and lets a guessed valid username force PBKDF2 per connection. Could we either derive/cache a stable plaintext SCRAM verifier per user, or keep plaintext-backed credentials on the cleartext path?
| /// and the random keys guarantee the client proof never matches. | ||
| pub fn mock_for_unknown_user(username: &[u8]) -> Self { | ||
| Self { | ||
| iterations: DEFAULT_PBKDF2_SHA256_ITERATIONS, |
There was a problem hiding this comment.
i= is also observable in the SCRAM server-first message, but the mock verifier always uses DEFAULT_PBKDF2_SHA256_ITERATIONS while stored pbkdf2_sha256 and pg_scram_sha256 credentials can use any accepted iteration count. A candidate username with a non-default stored verifier is therefore distinguishable from an unknown username before the client proves the password. Could the SCRAM path require a uniform iteration count for all configured users, or derive the mock iteration from provider-wide policy instead of the default?
I hereby agree to the terms of the GreptimeDB CLA.
Refer to a related PR or issue link (optional)
What's changed and what's your intention?
Implement SCRAM-SHA-256 authentication for the PostgreSQL protocol, so Postgres clients can authenticate without sending the password in cleartext.
What changed
src/auth: addPgScramSha256Verifier(common.rs) holdingiterations,salt,StoredKey,ServerKey, with apg_scram_sha256:<iterations>:<salt>:<stored_key>:<server_key>credential format and aformat_pg_scram_sha256_password_verifierhelper.Debugredacts all secret fields and all comparisons are constant-time.UserProvider::postgres_auth_info(): a new method that decides, per connection, whether to offer SCRAM or fall back to cleartext. Implemented for the static and watch-file providers; the default impl returnsCleartextso external providers keep working.plain:credential derives a verifier on the fly, and apbkdf2_sha256:credential reuses its hash directly as the SCRAMSaltedPassword(both are PBKDF2-HMAC-SHA256 / 32 bytes), so existing users can authenticate via SCRAM without re-hashing.src/servers/src/postgres/auth_handler.rs: implement the server side of the SASL exchange (SASL→SASLContinue→SASLFinal) with a per-connection auth state machine, client-first / client-final parsing, and client-proof verification.How it works
PostgreSQL negotiates a single auth method at connection startup. To avoid leaking user or verifier existence, the server offers SCRAM only when every configured user can do SCRAM (i.e. none is
mysql_native_password); otherwise it falls back to cleartext for all users. Unknown usernames are answered with a random throwaway verifier and still run the full handshake, so a failed login is indistinguishable from a wrong password.Limitations
SCRAM-SHA-256-PLUS) is not supported; then/ygs2 flags are accepted and bound across messages, butauthzidis not.mysql_native_password, Postgres auth falls back to cleartext for the whole instance.Compatibility
plain:,pbkdf2_sha256:, andmysql_native_password:credentials keep working. Cleartext behavior is preserved as a fallback.PR Checklist
Please convert it to a draft if some of the following conditions are not met.