Skip to content

Commit f6abccc

Browse files
authored
Cache API key hashing results on creation time (#74106)
The API key hashing result is now cached on the creation time of an API key, i.e. pre-warm the cache. Previously it is cached when the API key is authenticated for the first time. Since it is reasonable to assume that an API key will be used shortly after its creation, this change has following advantages: * It removes the need for expensive pbkdf2 hashing computation on authentication time and therefore reduces overall server load * It makes the first authentication faster We expect all keys to be used, that is, caching on creation time does not change the total number of keys need to be cached. Hence this PR does not introduce any extra logic to fine tune whether a key should be cached (for example, only cache if the load factor is lower than certain threshold etc.).
1 parent 3b69426 commit f6abccc

File tree

3 files changed

+54
-5
lines changed

3 files changed

+54
-5
lines changed

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ public void testAuthenticationReturns429WhenThreadPoolIsSaturated() throws IOExc
975975
final Settings settings = internalCluster().getInstance(Settings.class, nodeName);
976976
final int allocatedProcessors = EsExecutors.allocatedProcessors(settings);
977977
final ThreadPool threadPool = internalCluster().getInstance(ThreadPool.class, nodeName);
978+
final ApiKeyService apiKeyService = internalCluster().getInstance(ApiKeyService.class, nodeName);
978979

979980
final RoleDescriptor descriptor = new RoleDescriptor("auth_only", new String[] { }, null, null);
980981
final Client client = client().filterWithHeader(
@@ -987,6 +988,8 @@ public void testAuthenticationReturns429WhenThreadPoolIsSaturated() throws IOExc
987988

988989
assertNotNull(createApiKeyResponse.getId());
989990
assertNotNull(createApiKeyResponse.getKey());
991+
// Clear the auth cache to force recompute the expensive hash which requires the crypto thread pool
992+
apiKeyService.getApiKeyAuthCache().invalidateAll();
990993

991994
final List<NodeInfo> nodeInfos = client().admin().cluster().prepareNodesInfo().get().getNodes().stream()
992995
.filter(nodeInfo -> nodeInfo.getNode().getName().equals(nodeName))

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR
262262
final SecureString apiKey = UUIDs.randomBase64UUIDSecureString();
263263
final Version version = clusterService.state().nodes().getMinNodeVersion();
264264

265-
try (XContentBuilder builder = newDocument(apiKey, request.getName(), authentication, roleDescriptorSet, created, expiration,
265+
try (XContentBuilder builder = newDocument(apiKey, request.getName(), authentication,
266+
roleDescriptorSet, created, expiration,
266267
request.getRoleDescriptors(), version, request.getMetadata())) {
267268

268269
final IndexRequest indexRequest =
@@ -278,8 +279,11 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR
278279
TransportSingleItemBulkWriteAction.<IndexResponse>wrapBulkResponse(ActionListener.wrap(
279280
indexResponse -> {
280281
assert request.getId().equals(indexResponse.getId());
282+
final ListenableFuture<CachedApiKeyHashResult> listenableFuture = new ListenableFuture<>();
283+
listenableFuture.onResponse(new CachedApiKeyHashResult(true, apiKey));
284+
apiKeyAuthCache.put(request.getId(), listenableFuture);
281285
listener.onResponse(
282-
new CreateApiKeyResponse(request.getName(), request.getId(), apiKey, expiration));
286+
new CreateApiKeyResponse(request.getName(), request.getId(), apiKey, expiration));
283287
},
284288
listener::onFailure))));
285289
} catch (IOException e) {
@@ -291,15 +295,16 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR
291295
* package-private for testing
292296
*/
293297
XContentBuilder newDocument(SecureString apiKey, String name, Authentication authentication, Set<RoleDescriptor> userRoles,
294-
Instant created, Instant expiration, List<RoleDescriptor> keyRoles,
295-
Version version, @Nullable Map<String, Object> metadata) throws IOException {
298+
Instant created, Instant expiration, List<RoleDescriptor> keyRoles,
299+
Version version, @Nullable Map<String, Object> metadata) throws IOException {
296300
XContentBuilder builder = XContentFactory.jsonBuilder();
297301
builder.startObject()
298302
.field("doc_type", "api_key")
299303
.field("creation_time", created.toEpochMilli())
300304
.field("expiration_time", expiration == null ? null : expiration.toEpochMilli())
301305
.field("api_key_invalidated", false);
302306

307+
303308
byte[] utf8Bytes = null;
304309
final char[] keyHash = hasher.hash(apiKey);
305310
try {
@@ -1160,7 +1165,7 @@ final class CachedApiKeyHashResult {
11601165
this.hash = cacheHasher.hash(apiKey);
11611166
}
11621167

1163-
private boolean verify(SecureString password) {
1168+
boolean verify(SecureString password) {
11641169
return hash != null && cacheHasher.verify(password, hash);
11651170
}
11661171
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@
1010
import org.elasticsearch.ElasticsearchException;
1111
import org.elasticsearch.Version;
1212
import org.elasticsearch.action.ActionListener;
13+
import org.elasticsearch.action.DocWriteRequest;
1314
import org.elasticsearch.action.bulk.BulkAction;
15+
import org.elasticsearch.action.bulk.BulkItemResponse;
1416
import org.elasticsearch.action.bulk.BulkRequest;
17+
import org.elasticsearch.action.bulk.BulkResponse;
1518
import org.elasticsearch.action.get.GetRequest;
1619
import org.elasticsearch.action.index.IndexAction;
1720
import org.elasticsearch.action.index.IndexRequest;
1821
import org.elasticsearch.action.index.IndexRequestBuilder;
22+
import org.elasticsearch.action.index.IndexResponse;
1923
import org.elasticsearch.action.support.PlainActionFuture;
2024
import org.elasticsearch.client.Client;
2125
import org.elasticsearch.common.bytes.BytesArray;
2226
import org.elasticsearch.common.bytes.BytesReference;
27+
import org.elasticsearch.common.cache.Cache;
2328
import org.elasticsearch.common.util.concurrent.ListenableFuture;
2429
import org.elasticsearch.core.Tuple;
2530
import org.elasticsearch.common.settings.SecureString;
@@ -36,6 +41,7 @@
3641
import org.elasticsearch.common.xcontent.XContentType;
3742
import org.elasticsearch.common.xcontent.json.JsonXContent;
3843
import org.elasticsearch.index.get.GetResult;
44+
import org.elasticsearch.index.shard.ShardId;
3945
import org.elasticsearch.license.XPackLicenseState;
4046
import org.elasticsearch.test.ClusterServiceUtils;
4147
import org.elasticsearch.test.ESTestCase;
@@ -48,6 +54,7 @@
4854
import org.elasticsearch.xpack.core.security.SecurityContext;
4955
import org.elasticsearch.xpack.core.security.action.ApiKeyTests;
5056
import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest;
57+
import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse;
5158
import org.elasticsearch.xpack.core.security.authc.Authentication;
5259
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
5360
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
@@ -177,6 +184,40 @@ public void testCreateApiKeyUsesBulkIndexAction() {
177184
service.createApiKey(authentication, createApiKeyRequest, Set.of(), new PlainActionFuture<>());
178185
}
179186

187+
public void testCreateApiKeyWillCacheOnCreation() {
188+
final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build();
189+
final ApiKeyService service = createApiKeyService(settings);
190+
final Authentication authentication = new Authentication(
191+
new User(randomAlphaOfLengthBetween(8, 16), "superuser"),
192+
new RealmRef(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)),
193+
null);
194+
final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, null);
195+
when(client.prepareIndex(anyString())).thenReturn(new IndexRequestBuilder(client, IndexAction.INSTANCE));
196+
when(client.threadPool()).thenReturn(threadPool);
197+
doAnswer(inv -> {
198+
final Object[] args = inv.getArguments();
199+
@SuppressWarnings("unchecked")
200+
final ActionListener<BulkResponse> listener = (ActionListener<BulkResponse>) args[2];
201+
final IndexResponse indexResponse = new IndexResponse(
202+
new ShardId(INTERNAL_SECURITY_MAIN_INDEX_7, randomAlphaOfLength(22), randomIntBetween(0, 1)),
203+
createApiKeyRequest.getId(), randomLongBetween(1, 99), randomLongBetween(1, 99), randomIntBetween(1, 99), true);
204+
listener.onResponse(new BulkResponse(new BulkItemResponse[]{
205+
new BulkItemResponse(randomInt(), DocWriteRequest.OpType.INDEX, indexResponse)
206+
}, randomLongBetween(0, 100)));
207+
return null;
208+
}).when(client).execute(eq(BulkAction.INSTANCE), any(BulkRequest.class), any());
209+
210+
final Cache<String, ListenableFuture<CachedApiKeyHashResult>> apiKeyAuthCache = service.getApiKeyAuthCache();
211+
assertNull(apiKeyAuthCache.get(createApiKeyRequest.getId()));
212+
final PlainActionFuture<CreateApiKeyResponse> listener = new PlainActionFuture<>();
213+
service.createApiKey(authentication, createApiKeyRequest, Set.of(), listener);
214+
final CreateApiKeyResponse createApiKeyResponse = listener.actionGet();
215+
assertThat(createApiKeyResponse.getId(), equalTo(createApiKeyRequest.getId()));
216+
final CachedApiKeyHashResult cachedApiKeyHashResult = service.getFromCache(createApiKeyResponse.getId());
217+
assertThat(cachedApiKeyHashResult.success, is(true));
218+
cachedApiKeyHashResult.verify(createApiKeyResponse.getKey());
219+
}
220+
180221
public void testGetCredentialsFromThreadContext() {
181222
ThreadContext threadContext = threadPool.getThreadContext();
182223
assertNull(ApiKeyService.getCredentialsFromHeader(threadContext));

0 commit comments

Comments
 (0)