Skip to content

security: cache derived SCRAM stored key #31054

Open
nguyen-andrew wants to merge 4 commits into
redpanda-data:devfrom
nguyen-andrew:scram-memo
Open

security: cache derived SCRAM stored key #31054
nguyen-andrew wants to merge 4 commits into
redpanda-data:devfrom
nguyen-andrew:scram-memo

Conversation

@nguyen-andrew

@nguyen-andrew nguyen-andrew commented Jul 8, 2026

Copy link
Copy Markdown
Member

HTTP Basic auth (PandaProxy, Schema Registry, Admin API) and Kafka SASL/PLAIN
rederive the credential's stored SCRAM key on every request: a PBKDF2-style
loop of >=4096 HMAC rounds, run on-reactor before the body is parsed. Under
high-rate authenticated traffic this dominates CPU. A cluster serving a few
hundred HTTP-produce req/s per shard through PandaProxy saturated its reactor
core almost entirely in this path (~45% of on-CPU time in
validate_scram_credential), starving heartbeats.

This memoizes the derivation with a bounded per-shard cache, keyed by an HMAC
of (algorithm, password, salt, iterations) and holding only the derived stored
key (already in the credential store). It is purely functional, so a password
change re-salts and re-keys and entries never go stale.

The cache is gated by the scram_credential_cache_enabled cluster config and
ships disabled by default. Because it caches the result of password
validation in memory, enabling it by default is deferred pending security team
review; until then it is strictly opt-in and out-of-the-box behavior is
unchanged.

Fixes CORE-16829

Security considerations (for review)

  • Only the derived stored key is cached — the value already persisted in the
    credential store; no plaintext or salted password is retained.
  • The cache key is an HMAC of (algorithm, password, salt, iterations) under a
    random per-boot key, so a memory dump exposes no offline-crackable material
    and keys can't be correlated across shards or reboots.
  • Every hit re-compares the cached stored key against the credential's live
    stored key, so a rotated credential (re-salted, hence re-keyed) or a
    cache-key collision fails closed. A false accept would require colliding both
    the HMAC key and the SCRAM stored key at once — two independent SHA
    collisions, one under a secret key — negligible and no weaker than SCRAM's
    existing assumptions.
  • Hits (~0.6 µs) and misses (~0.4 ms) are timing-distinguishable, but both
    correct and wrong passwords are cached, so a hit only reveals that an exact
    (user, password) was tried recently — not whether it is correct. Correctness
    still comes from the 200/401 response, and distinct guesses (a dictionary
    attack) always miss. Caching failed validations also keeps bad-credential
    floods cheap.

Microbench (--config=release, single core)

Repeat = same credential per call (steady-state auth); unique = fresh password
per call (cache-miss path).

Validation case Disabled (default) Enabled Speedup
SHA-256, correct password (repeat) 391 µs 558 ns ~700x
SHA-256, wrong password (repeat) 392 µs 568 ns ~690x
SHA-512, correct password (repeat) 1.04 ms 554 ns ~1,880x
SHA-256, unique password (cache miss) 390 µs 396 µs unchanged

End-to-end (single core, unbatched ~2 KiB HTTP produce)

Scenario Disabled (default) Enabled
Basic-auth produce 288 req/s, 3.43 ms/req 635 req/s, 1.56 ms/req
No-auth baseline (same path, no auth) 589 req/s, 1.68 ms/req 660 req/s, 1.50 ms/req
Basic-auth, 50% empty-body requests 336 req/s, 2.96 ms/req 1,354 req/s, 0.74 ms/req

The cache is disabled by default, so out-of-the-box throughput is the Disabled
column. Enabling it (scram_credential_cache_enabled=true) raises Basic-auth
throughput to match the no-auth baseline; toggling it back off returns to the
Disabled numbers, so the config gates the behavior cleanly in both directions.

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v26.1.x
  • v25.3.x
  • v25.2.x

Release Notes

  • none

Measures the per-call cost of the SCRAM password check that HTTP Basic
auth runs on every pandaproxy/schema registry/admin request. The salted
password is rederived per request (4096 HMAC rounds), which saturated
pandaproxy CPU under unbatched HTTP produce in a production incident:
~473us per SHA-256 validation, so a few thousand requests per second
exhaust a core before any request work happens. Covers the wrong
password (reject) path, which costs the same as an accept, SHA-512
(~3x SHA-256), and a unique-password case that always pays the full
derivation.
Exposes the password -> stored_key derivation as a reusable step so a
subsequent commit can memoize it.
Copilot AI review requested due to automatic review settings July 8, 2026 21:26
@nguyen-andrew nguyen-andrew requested a review from a team as a code owner July 8, 2026 21:26

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

This PR introduces a per-shard memoization layer for SCRAM password validation to eliminate repeated PBKDF2-style derivations on hot authentication paths (HTTP Basic auth and SASL/PLAIN), controlled via a new cluster configuration kill switch.

Changes:

  • Add security::scram_credential_cache and integrate it into validate_scram_credential with a thread_local per-shard cache gated by scram_credential_cache_enabled.
  • Refactor SCRAM algorithm code to expose derive_stored_key() and reuse it in password validation.
  • Add unit tests and an rpbench benchmark covering cached vs uncached behavior and correctness.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/v/security/tests/scram_credential_cache_test.cc New unit tests validating cache hit/miss behavior, key composition, eviction bounds, and cached credential validation.
src/v/security/tests/scram_credential_bench.cc New perf benchmark exercising repeated/unique password validation cases to quantify caching impact.
src/v/security/tests/scram_algorithm_test.cc Adds a test ensuring derive_stored_key() matches stored keys produced by credential creation.
src/v/security/tests/BUILD Wires new test and benchmark targets into the build.
src/v/security/scram_credential_cache.h Declares the per-shard SCRAM derivation cache and its keying/metrics interface.
src/v/security/scram_credential_cache.cc Implements HMAC-keyed cache entries backed by utils::chunked_kv_cache.
src/v/security/scram_authenticator.h Documents cached validation behavior and introduces a detail::validate_scram_credential overload for caller-provided caches.
src/v/security/scram_authenticator.cc Implements cached derivation path and config-gated use of a per-thread cache.
src/v/security/scram_algorithm.h Adds derive_stored_key() and refactors validate_password() to use it.
src/v/security/BUILD Adds the new cache implementation to the security library and links required deps.
src/v/config/configuration.h Declares the new scram_credential_cache_enabled configuration property.
src/v/config/configuration.cc Defines the new configuration property and its description/default.

Comment thread src/v/security/scram_credential_cache.cc Outdated
Comment thread src/v/security/scram_credential_cache.h Outdated
Per-shard memoization of SCRAM password derivations, keyed by an HMAC
(random per-instance key) of (mechanism, password, salt, iterations) so
plaintext passwords are not retained, holding only the derived stored
key: the same datum already persisted in the credential store. The
mapping is purely functional, so entries never go stale: updating a
credential generates a fresh salt, which changes the cache key. Built on
the S3-FIFO chunked_kv_cache, whose scan resistance keeps one-shot
wrong-password entries from displacing hot legitimate ones.
@nguyen-andrew

Copy link
Copy Markdown
Member Author

Force push to address Copilot comments.

@nguyen-andrew nguyen-andrew self-assigned this Jul 8, 2026
@vbotbuildovich

vbotbuildovich commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

CI test results

test results on build#86904
test_status test_class test_method test_arguments test_kind job_url passed reason test_history
FLAKY(PASS) ShadowLinkTopicFailoverTests test_producer_ids_failover {"storage_mode": "tiered_cloud"} integration https://buildkite.com/redpanda/redpanda/builds/86904#019f43fb-b592-4841-ae9b-a6f7097397b7 19/21 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0079, p0=0.1462, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3917, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ShadowLinkTopicFailoverTests&test_method=test_producer_ids_failover
test results on build#86936
test_status test_class test_method test_arguments test_kind job_url passed reason test_history
FLAKY(PASS) NodeWiseRecoveryTest test_recovery_local_data_missing {"wait_for_final_manifest_uploads": true} integration https://buildkite.com/redpanda/redpanda/builds/86936#019f48a5-8cb9-4fde-94e4-69a6b46cdedd 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0555, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1574, p1=0.1805, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=NodeWiseRecoveryTest&test_method=test_recovery_local_data_missing
FLAKY(FAIL) ShadowLinkingRandomOpsTest test_node_operations {"failures": true, "workload_set": "cloud_combos"} integration https://buildkite.com/redpanda/redpanda/builds/86936#019f48a5-8cbc-468b-9578-e4647d82e748 9/11 Test FAILS after retries.Significant increase in flaky rate(baseline=0.0008, p0=0.0082, reject_threshold=0.0100) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ShadowLinkingRandomOpsTest&test_method=test_node_operations

@pgellert

pgellert commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

/microbench

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Performance change detected in https://buildkite.com/redpanda/redpanda/builds/86918#019f4614-84b6-4266-9e37-2288a79527c5:

Performance changes detected in 10 tests
cloud_storage_rpbench.cstore_bench.cs_iteration_recompute_end_test_1000: inst -> -0.05pct
wasm_transform_rpbench.WasmBenchTest_BatchSize1_RecordSize1_KiB.IdentityTransform: inst -> -0.04pct
async_algorithm_rpbench.algo_bench.sync_std_for_each_big: allocs -> +infpct
role_store_bench_rpbench.role_store_bench.put_role: inst -> +0.06pct
authorizer_bench_rpbench.authorizer_bench.describe_10k_immediate_deny: inst -> +0.75pct
authorizer_bench_rpbench.authorizer_bench.describe_10k_prefix_acls: inst -> +0.50pct
authorizer_bench_rpbench.authorizer_bench.describe_10k_prefix_acls_allowed: inst -> +0.60pct
authorizer_bench_rpbench.authorizer_bench.describe_10k_prefix_acls_no_match: inst -> +1.78pct
authorizer_bench_rpbench.authorizer_bench.describe_10k_prefix_acls_two_groups: inst -> +0.26pct
authorizer_bench_rpbench.authorizer_bench.describe_literal_grants_10k: inst -> -0.87pct

See https://redpandadata.atlassian.net/wiki/x/LQAqLg for docs

@nguyen-andrew nguyen-andrew requested a review from ballard26 July 9, 2026 17:22
validate_scram_credential now consults a shard-local
scram_credential_cache, skipping the salted password derivation for
repeat authentications. HTTP Basic auth (pandaproxy, schema registry,
admin API) and SASL/PLAIN pay that derivation on every request; in
production, unbatched HTTP produce through pandaproxy at a few hundred
requests per second saturated a core on it. Cache hits skip the
derivation entirely; the uncached path is unchanged.

The scram_credential_cache_enabled cluster config (default true) is an
operational kill switch.
@nguyen-andrew

Copy link
Copy Markdown
Member Author

Force push to change default behavior to disabled.

@nguyen-andrew nguyen-andrew changed the title security: cache SCRAM password validation security: cache derived SCRAM stored key Jul 9, 2026
@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Retry command for Build#86936

please wait until all jobs are finished before running the slash command

/ci-repeat 1
skip-redpanda-build
skip-units
skip-rebase
tests/rptest/tests/shadow_linking_rnot_test.py::ShadowLinkingRandomOpsTest.test_node_operations@{"failures":true,"workload_set":"cloud_combos"}

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants