Skip to content

Commit a1eec85

Browse files
committed
security: memoize scram password validation
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.
1 parent 624ba80 commit a1eec85

5 files changed

Lines changed: 149 additions & 9 deletions

File tree

src/v/config/configuration.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4178,6 +4178,16 @@ configuration::configuration()
41784178
},
41794179
std::vector<ss::sstring>{"BASIC"},
41804180
validate_http_authn_mechanisms)
4181+
, scram_credential_cache_enabled(
4182+
*this,
4183+
"scram_credential_cache_enabled",
4184+
"Whether to cache SCRAM password validation results for authentication "
4185+
"paths that receive a plaintext password on every request (HTTP Basic "
4186+
"authentication and SASL/PLAIN). When enabled, repeat authentications "
4187+
"skip the salted password derivation, which costs thousands of HMAC "
4188+
"operations per validation.",
4189+
{.needs_restart = needs_restart::no, .visibility = visibility::tunable},
4190+
true)
41814191
, enable_mpx_extensions(
41824192
*this,
41834193
"enable_mpx_extensions",

src/v/config/configuration.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ struct configuration final : public config_store {
712712

713713
// HTTP Authentication
714714
enterprise<property<std::vector<ss::sstring>>> http_authentication;
715+
property<bool> scram_credential_cache_enabled;
715716

716717
// MPX
717718
property<bool> enable_mpx_extensions;

src/v/security/scram_authenticator.cc

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
#include "security/scram_authenticator.h"
1212

1313
#include "base/vlog.h"
14+
#include "config/configuration.h"
1415
#include "random/secure_generators.h"
1516
#include "security/credential_store.h"
1617
#include "security/errc.h"
1718
#include "security/logger.h"
19+
#include "security/scram_credential_cache.h"
1820

1921
namespace security {
2022

@@ -148,22 +150,70 @@ scram_authenticator<T>::authenticate(bytes auth_bytes) {
148150
template class scram_authenticator<scram_sha256>;
149151
template class scram_authenticator<scram_sha512>;
150152

153+
namespace {
154+
155+
// Matches a password against a stored credential by rederiving its stored
156+
// key. When a cache is given, the derivation is memoized; a null cache
157+
// derives every time (the plain, un-memoized path).
158+
template<typename scram>
159+
bool match_password(
160+
scram_credential_cache* cache,
161+
scram_algorithm_t mech,
162+
const scram_credential& cred,
163+
const credential_password& password) {
164+
if (cache != nullptr) {
165+
auto cached = cache->get(
166+
mech, password, cred.salt(), cred.iterations());
167+
if (cached.has_value()) {
168+
return *cached == cred.stored_key();
169+
}
170+
}
171+
auto stored_key = scram::derive_stored_key(
172+
password(), cred.salt(), cred.iterations());
173+
auto valid = stored_key == cred.stored_key();
174+
if (cache != nullptr) {
175+
cache->put(
176+
mech,
177+
password,
178+
cred.salt(),
179+
cred.iterations(),
180+
std::move(stored_key));
181+
}
182+
return valid;
183+
}
184+
} // namespace
185+
186+
namespace detail {
187+
151188
std::optional<std::string_view> validate_scram_credential(
152-
const scram_credential& cred, const credential_password& password) {
189+
const scram_credential& cred,
190+
const credential_password& password,
191+
scram_credential_cache* cache) {
153192
std::optional<std::string_view> sasl_mechanism;
154193
if (
155-
cred.stored_key().size() == security::scram_sha256::key_size
156-
&& security::scram_sha256::validate_password(
157-
password, cred.stored_key(), cred.salt(), cred.iterations())) {
158-
sasl_mechanism = security::scram_sha256_authenticator::name;
194+
cred.stored_key().size() == scram_sha256::key_size
195+
&& match_password<scram_sha256>(
196+
cache, scram_algorithm_t::sha256, cred, password)) {
197+
sasl_mechanism = scram_sha256_authenticator::name;
159198
} else if (
160-
cred.stored_key().size() == security::scram_sha512::key_size
161-
&& security::scram_sha512::validate_password(
162-
password, cred.stored_key(), cred.salt(), cred.iterations())) {
163-
sasl_mechanism = security::scram_sha512_authenticator::name;
199+
cred.stored_key().size() == scram_sha512::key_size
200+
&& match_password<scram_sha512>(
201+
cache, scram_algorithm_t::sha512, cred, password)) {
202+
sasl_mechanism = scram_sha512_authenticator::name;
164203
}
165204

166205
return sasl_mechanism;
167206
}
207+
} // namespace detail
208+
209+
std::optional<std::string_view> validate_scram_credential(
210+
const scram_credential& cred, const credential_password& password) {
211+
if (config::shard_local_cfg().scram_credential_cache_enabled()) {
212+
static thread_local scram_credential_cache cache{
213+
scram_credential_cache::default_capacity};
214+
return detail::validate_scram_credential(cred, password, &cache);
215+
}
216+
return detail::validate_scram_credential(cred, password, nullptr);
217+
}
168218

169219
} // namespace security

src/v/security/scram_authenticator.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,23 @@ struct scram_sha512_authenticator {
9898
= scram_mechanism_traits<scram_sha512>::name;
9999
};
100100

101+
class scram_credential_cache;
102+
103+
/// Validates a plaintext password against a stored SCRAM credential, for
104+
/// non-SCRAM authentication such as HTTP Basic auth and SASL/PLAIN. Returns
105+
/// the credential's SASL mechanism name on success.
106+
///
107+
/// Consults the shard's scram_credential_cache to skip the expensive salted
108+
/// password derivation for recently validated (password, credential) pairs,
109+
/// unless disabled via the scram_credential_cache_enabled config.
101110
std::optional<std::string_view> validate_scram_credential(
102111
const scram_credential& cred, const credential_password& password);
103112

113+
namespace detail {
114+
/// As above, against a caller-provided cache.
115+
std::optional<std::string_view> validate_scram_credential(
116+
const scram_credential& cred,
117+
const credential_password& password,
118+
scram_credential_cache* cache);
119+
} // namespace detail
104120
} // namespace security

src/v/security/tests/scram_credential_cache_test.cc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#define BOOST_TEST_MODULE security
1212

1313
#include "bytes/bytes.h"
14+
#include "security/scram_algorithm.h"
15+
#include "security/scram_authenticator.h"
1416
#include "security/scram_credential_cache.h"
1517
#include "security/types.h"
1618
#include "test_utils/random_bytes.h"
@@ -115,4 +117,65 @@ BOOST_AUTO_TEST_CASE(cache_is_bounded) {
115117
BOOST_REQUIRE_LT(cache.stats().size, capacity * 5);
116118
}
117119

120+
BOOST_AUTO_TEST_CASE(cached_validate_scram_credential) {
121+
scram_credential_cache cache(16);
122+
auto cred = scram_sha256::make_credentials(
123+
password()(), scram_sha256::min_iterations);
124+
125+
auto mech = detail::validate_scram_credential(cred, password(), &cache);
126+
BOOST_REQUIRE(mech.has_value());
127+
BOOST_REQUIRE_EQUAL(*mech, scram_sha256_authenticator::name);
128+
BOOST_REQUIRE_EQUAL(cache.stats().hits, 0);
129+
130+
// Repeat validation is served from the cache with the same result.
131+
mech = detail::validate_scram_credential(cred, password(), &cache);
132+
BOOST_REQUIRE(mech.has_value());
133+
BOOST_REQUIRE_EQUAL(*mech, scram_sha256_authenticator::name);
134+
BOOST_REQUIRE_EQUAL(cache.stats().hits, 1);
135+
136+
// A wrong password fails, and keeps failing once its derivation is
137+
// cached.
138+
const credential_password wrong{"wrong-password"};
139+
BOOST_REQUIRE(
140+
!detail::validate_scram_credential(cred, wrong, &cache).has_value());
141+
BOOST_REQUIRE(
142+
!detail::validate_scram_credential(cred, wrong, &cache).has_value());
143+
}
144+
145+
BOOST_AUTO_TEST_CASE(cached_validate_scram_credential_sha512) {
146+
scram_credential_cache cache(16);
147+
auto cred = scram_sha512::make_credentials(
148+
password()(), scram_sha512::min_iterations);
149+
150+
for (int i = 0; i < 2; ++i) {
151+
auto mech = detail::validate_scram_credential(cred, password(), &cache);
152+
BOOST_REQUIRE(mech.has_value());
153+
BOOST_REQUIRE_EQUAL(*mech, scram_sha512_authenticator::name);
154+
}
155+
BOOST_REQUIRE_EQUAL(cache.stats().hits, 1);
156+
}
157+
158+
BOOST_AUTO_TEST_CASE(cached_validate_scram_credential_password_change) {
159+
scram_credential_cache cache(16);
160+
auto cred = scram_sha256::make_credentials(
161+
password()(), scram_sha256::min_iterations);
162+
163+
// Warm the cache with the old credential.
164+
BOOST_REQUIRE(detail::validate_scram_credential(cred, password(), &cache));
165+
BOOST_REQUIRE(detail::validate_scram_credential(cred, password(), &cache));
166+
167+
// The user's password is changed: the new credential gets a fresh salt.
168+
const credential_password new_password{"brand-new-password"};
169+
auto new_cred = scram_sha256::make_credentials(
170+
new_password(), scram_sha256::min_iterations);
171+
172+
// The stale cache entries do not interfere in either direction.
173+
BOOST_REQUIRE(
174+
detail::validate_scram_credential(new_cred, new_password, &cache)
175+
.has_value());
176+
BOOST_REQUIRE(
177+
!detail::validate_scram_credential(new_cred, password(), &cache)
178+
.has_value());
179+
}
180+
118181
} // namespace security

0 commit comments

Comments
 (0)