Skip to content

feat: support SCRAM auth for Postgres#8304

Open
killme2008 wants to merge 3 commits into
GreptimeTeam:mainfrom
killme2008:pg-scram-auth
Open

feat: support SCRAM auth for Postgres#8304
killme2008 wants to merge 3 commits into
GreptimeTeam:mainfrom
killme2008:pg-scram-auth

Conversation

@killme2008

Copy link
Copy Markdown
Member

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: add PgScramSha256Verifier (common.rs) holding iterations, salt, StoredKey, ServerKey, with a pg_scram_sha256:<iterations>:<salt>:<stored_key>:<server_key> credential format and a format_pg_scram_sha256_password_verifier helper. Debug redacts 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 returns Cleartext so external providers keep working.
  • Credential conversion: a stored plain: credential derives a verifier on the fly, and a pbkdf2_sha256: credential reuses its hash directly as the SCRAM SaltedPassword (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 (SASLSASLContinueSASLFinal) 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

  • Channel binding (SCRAM-SHA-256-PLUS) is not supported; the n/y gs2 flags are accepted and bound across messages, but authzid is not.
  • When any user is configured with mysql_native_password, Postgres auth falls back to cleartext for the whole instance.

Compatibility

  • No API or data-format breakage. The new credential format is additive; existing plain:, pbkdf2_sha256:, and mysql_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.

  • I have written the necessary rustdoc comments.
  • I have added the necessary unit tests and integration tests.
  • This PR requires documentation updates.
  • API changes are backward compatible.
  • Schema or data changes are backward compatible.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
@killme2008 killme2008 requested a review from a team as a code owner June 16, 2026 02:55
@github-actions github-actions Bot added size/M docs-not-required This change does not impact docs. labels Jun 16, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/auth/src/user_provider.rs Outdated
Comment on lines +373 to +383
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,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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:

  1. 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.
  2. Timing Leak / Username Enumeration: If the server is configured with hashed credentials (e.g., pbkdf2_sha256 or pg_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,
            })

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 UserProvider with postgres_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 (SASLSASLContinueSASLFinal) 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]>());
Comment on lines +413 to 416
if client_final.channel_binding != channel_binding {
return Ok(PgAuthenticationResult::Failed);
}

Comment thread src/auth/src/user_provider.rs Outdated
Comment on lines +373 to +379
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]>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread src/auth/src/common.rs
/// 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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-security docs-not-required This change does not impact docs. size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants