Skip to content

Commit cb63f4f

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 1c5233d commit cb63f4f

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+
false)
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: 57 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,68 @@ 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+
const scram_credential& cred,
162+
const credential_password& password) {
163+
constexpr auto mech = scram_mechanism_traits<scram>::algorithm;
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>(cache, cred, password)) {
196+
sasl_mechanism = scram_sha256_authenticator::name;
159197
} 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;
198+
cred.stored_key().size() == scram_sha512::key_size
199+
&& match_password<scram_sha512>(cache, cred, password)) {
200+
sasl_mechanism = scram_sha512_authenticator::name;
164201
}
165202

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

169217
} // namespace security

src/v/security/scram_authenticator.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ class scram_authenticator final : public sasl_mechanism {
7979
template<>
8080
struct scram_mechanism_traits<scram_sha256> {
8181
static constexpr const char* name = "SCRAM-SHA-256";
82+
static constexpr scram_algorithm_t algorithm = scram_algorithm_t::sha256;
8283
};
8384

8485
template<>
8586
struct scram_mechanism_traits<scram_sha512> {
8687
static constexpr const char* name = "SCRAM-SHA-512";
88+
static constexpr scram_algorithm_t algorithm = scram_algorithm_t::sha512;
8789
};
8890

8991
struct scram_sha256_authenticator {
@@ -98,7 +100,23 @@ struct scram_sha512_authenticator {
98100
= scram_mechanism_traits<scram_sha512>::name;
99101
};
100102

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

115+
namespace detail {
116+
/// As above, against a caller-provided cache.
117+
std::optional<std::string_view> validate_scram_credential(
118+
const scram_credential& cred,
119+
const credential_password& password,
120+
scram_credential_cache* cache);
121+
} // namespace detail
104122
} // 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"
@@ -130,4 +132,65 @@ BOOST_AUTO_TEST_CASE(cache_tiny_capacity_does_not_crash) {
130132
}
131133
}
132134

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

0 commit comments

Comments
 (0)