Browse Source

[8.x] Default to `SSHA-256` as API key stored credential hasher (#120997) (#121229)

Nikolaj Volgushev 8 months ago
parent
commit
3e99d71e45

+ 5 - 0
docs/changelog/120997.yaml

@@ -0,0 +1,5 @@
+pr: 120997
+summary: Allow `SSHA-256` for API key credential hash
+area: Authentication
+type: enhancement
+issues: []

+ 64 - 0
docs/reference/settings/security-hash-settings.asciidoc

@@ -124,4 +124,68 @@ following:
                              initial input with SHA512 first.
 |=======================
 
+Furthermore, {es} supports authentication via securely-generated high entropy tokens,
+for instance <<security-api-create-api-key,API keys>>.
+Analogous to passwords, only the tokens' hashes are stored. Since the tokens are guaranteed
+to have sufficiently high entropy to resist offline attacks, secure salted hash functions are supported
+in addition to the password-hashing algorithms mentioned above.
 
+You can configure the algorithm for API key stored credential hashing
+by setting the <<static-cluster-setting,static>>
+`xpack.security.authc.api_key.hashing.algorithm` setting to one of the
+following
+
+[[secure-token-hashing-algorithms]]
+.Secure token hashing algorithms
+|=======================
+| Algorithm               | | | Description
+
+| `ssha256`               | | | Uses a salted `sha-256` algorithm. (default)
+| `bcrypt`                | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds.
+| `bcrypt4`               | | | Uses `bcrypt` algorithm with salt generated in 16 rounds.
+| `bcrypt5`               | | | Uses `bcrypt` algorithm with salt generated in 32 rounds.
+| `bcrypt6`               | | | Uses `bcrypt` algorithm with salt generated in 64 rounds.
+| `bcrypt7`               | | | Uses `bcrypt` algorithm with salt generated in 128 rounds.
+| `bcrypt8`               | | | Uses `bcrypt` algorithm with salt generated in 256 rounds.
+| `bcrypt9`               | | | Uses `bcrypt` algorithm with salt generated in 512 rounds.
+| `bcrypt10`              | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds.
+| `bcrypt11`              | | | Uses `bcrypt` algorithm with salt generated in 2048 rounds.
+| `bcrypt12`              | | | Uses `bcrypt` algorithm with salt generated in 4096 rounds.
+| `bcrypt13`              | | | Uses `bcrypt` algorithm with salt generated in 8192 rounds.
+| `bcrypt14`              | | | Uses `bcrypt` algorithm with salt generated in 16384 rounds.
+| `pbkdf2`                | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 10000 iterations.
+| `pbkdf2_1000`           | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 1000 iterations.
+| `pbkdf2_10000`          | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 10000 iterations.
+| `pbkdf2_50000`          | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 50000 iterations.
+| `pbkdf2_100000`         | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 100000 iterations.
+| `pbkdf2_500000`         | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                              pseudorandom function using 500000 iterations.
+| `pbkdf2_1000000`        | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 1000000 iterations.
+| `pbkdf2_stretch`        | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 10000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_1000`   | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 1000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_10000`  | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 10000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_50000`  | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 50000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 100000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 500000 iterations, after hashing the
+                             initial input with SHA512 first.
+| `pbkdf2_stretch_1000000`| | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
+                             pseudorandom function using 1000000 iterations, after hashing the
+                             initial input with SHA512 first.
+|=======================

+ 4 - 4
docs/reference/settings/security-settings.asciidoc

@@ -23,8 +23,8 @@ For more information about creating and updating the {es} keystore, see
 ==== General security settings
 `xpack.security.enabled`::
 (<<static-cluster-setting,Static>>)
-Defaults to `true`, which enables {es} {security-features} on the node. 
-This setting must be enabled to use Elasticsearch's authentication, 
+Defaults to `true`, which enables {es} {security-features} on the node.
+This setting must be enabled to use Elasticsearch's authentication,
 authorization and audit features. +
 +
 --
@@ -229,7 +229,7 @@ Defaults to `7d`.
 
 --
 NOTE:  Large real-time clock inconsistency across cluster nodes can cause problems
-with evaluating the API key retention period. That is, if the clock on the node 
+with evaluating the API key retention period. That is, if the clock on the node
 invalidating the API key is significantly different than the one performing the deletion,
 the key may be retained for longer or shorter than the configured retention period.
 
@@ -252,7 +252,7 @@ Sets the timeout of the internal search and delete call.
 `xpack.security.authc.api_key.hashing.algorithm`::
 (<<static-cluster-setting,Static>>)
 Specifies the hashing algorithm that is used for securing API key credentials.
-See <<password-hashing-algorithms>>. Defaults to `pbkdf2`.
+See <<secure-token-hashing-algorithms>>. Defaults to `ssha256`.
 
 [discrete]
 [[security-domain-settings]]

+ 41 - 0
server/src/main/java/org/elasticsearch/common/SecureRandomUtils.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.common;
+
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.core.CharArrays;
+
+import java.util.Arrays;
+import java.util.Base64;
+
+public final class SecureRandomUtils {
+    private SecureRandomUtils() {}
+
+    /**
+     * Returns a cryptographically secure Base64 encoded {@link SecureString} of {@code numBytes} random bytes.
+     */
+    public static SecureString getBase64SecureRandomString(int numBytes) {
+        byte[] randomBytes = null;
+        byte[] encodedBytes = null;
+        try {
+            randomBytes = new byte[numBytes];
+            SecureRandomHolder.INSTANCE.nextBytes(randomBytes);
+            encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(randomBytes);
+            return new SecureString(CharArrays.utf8BytesToChars(encodedBytes));
+        } finally {
+            if (randomBytes != null) {
+                Arrays.fill(randomBytes, (byte) 0);
+            }
+            if (encodedBytes != null) {
+                Arrays.fill(encodedBytes, (byte) 0);
+            }
+        }
+    }
+}

+ 42 - 5
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

@@ -240,7 +240,7 @@ public class XPackSettings {
 
     public static final List<String> DEFAULT_CIPHERS = JDK12_CIPHERS;
 
-    public static final Setting<String> PASSWORD_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting(
+    public static final Setting<String> PASSWORD_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting(
         "xpack.security.authc.password_hashing.algorithm",
         (s) -> {
             if (XPackSettings.FIPS_MODE_ENABLED.get(s)) {
@@ -251,7 +251,7 @@ public class XPackSettings {
         }
     );
 
-    public static final Setting<String> SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting(
+    public static final Setting<String> SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting(
         "xpack.security.authc.service_token_hashing.algorithm",
         (s) -> Hasher.PBKDF2_STRETCH.name()
     );
@@ -259,11 +259,48 @@ public class XPackSettings {
     /*
      * Do not allow insecure hashing algorithms to be used for password hashing
      */
-    public static Setting<String> defaultStoredHashAlgorithmSetting(String key, Function<Settings, String> defaultHashingAlgorithm) {
+    public static Setting<String> defaultStoredPasswordHashAlgorithmSetting(
+        String key,
+        Function<Settings, String> defaultHashingAlgorithm
+    ) {
         return new Setting<>(new Setting.SimpleKey(key), defaultHashingAlgorithm, Function.identity(), v -> {
-            if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
+            if (Hasher.getAvailableAlgoStoredPasswordHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
                 throw new IllegalArgumentException(
-                    "Invalid algorithm: " + v + ". Valid values for password hashing are " + Hasher.getAvailableAlgoStoredHash().toString()
+                    "Invalid algorithm: "
+                        + v
+                        + ". Valid values for password hashing are "
+                        + Hasher.getAvailableAlgoStoredPasswordHash().toString()
+                );
+            } else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) {
+                try {
+                    SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
+                } catch (NoSuchAlgorithmException e) {
+                    throw new IllegalArgumentException(
+                        "Support for PBKDF2WithHMACSHA512 must be available in order to use any of the PBKDF2 algorithms for the ["
+                            + key
+                            + "] setting.",
+                        e
+                    );
+                }
+            }
+        }, Property.NodeScope);
+    }
+
+    /**
+     * Similar to {@link #defaultStoredPasswordHashAlgorithmSetting(String, Function)} but for secure, high-entropy tokens so salted secure
+     * hashing algorithms are allowed, in addition to algorithms that are suitable for password hashing.
+     */
+    public static Setting<String> defaultStoredSecureTokenHashAlgorithmSetting(
+        String key,
+        Function<Settings, String> defaultHashingAlgorithm
+    ) {
+        return new Setting<>(new Setting.SimpleKey(key), defaultHashingAlgorithm, Function.identity(), v -> {
+            if (Hasher.getAvailableAlgoStoredSecureTokenHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
+                throw new IllegalArgumentException(
+                    "Invalid algorithm: "
+                        + v
+                        + ". Valid values for secure token hashing are "
+                        + Hasher.getAvailableAlgoStoredSecureTokenHash().toString()
                 );
             } else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) {
                 try {

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java

@@ -89,11 +89,11 @@ public class PutUserRequestBuilder extends ActionRequestBuilder<PutUserRequest,
     public PutUserRequestBuilder passwordHash(char[] passwordHash, Hasher configuredHasher) {
         final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHash);
         if (resolvedHasher.equals(configuredHasher) == false
-            && Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
+            && Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
             throw new IllegalArgumentException(
                 "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
                     + "The supported password hash algorithms are "
-                    + Hasher.getAvailableAlgoStoredHash().toString()
+                    + Hasher.getAvailableAlgoStoredPasswordHash().toString()
             );
         }
         if (request.passwordHash() != null) {

+ 15 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java

@@ -734,7 +734,7 @@ public enum Hasher {
      * an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()}
      */
     @SuppressForbidden(reason = "This is the only allowed way to get available values")
-    public static List<String> getAvailableAlgoStoredHash() {
+    public static List<String> getAvailableAlgoStoredPasswordHash() {
         return Arrays.stream(Hasher.values())
             .map(Hasher::name)
             .map(name -> name.toLowerCase(Locale.ROOT))
@@ -742,6 +742,20 @@ public enum Hasher {
             .collect(Collectors.toList());
     }
 
+    /**
+     * Returns a list of lower case String identifiers for the Hashing algorithm and parameter
+     * combinations that can be used for secure token hashing. The identifiers can be used to get
+     * an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()}
+     */
+    @SuppressForbidden(reason = "This is the only allowed way to get available values")
+    public static List<String> getAvailableAlgoStoredSecureTokenHash() {
+        return Arrays.stream(Hasher.values())
+            .map(Hasher::name)
+            .map(name -> name.toLowerCase(Locale.ROOT))
+            .filter(name -> (name.startsWith("pbkdf2") || name.startsWith("bcrypt") || name.equals("ssha256")))
+            .collect(Collectors.toList());
+    }
+
     /**
      * Returns a list of lower case String identifiers for the Hashing algorithm and parameter
      * combinations that can be used for password hashing in the cache. The identifiers can be used to get

+ 25 - 12
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

@@ -1475,7 +1475,7 @@ public class Security extends Plugin
         settingsList.add(TokenService.DELETE_INTERVAL);
         settingsList.add(TokenService.DELETE_TIMEOUT);
         settingsList.addAll(SSLConfigurationSettings.getProfileSettings());
-        settingsList.add(ApiKeyService.PASSWORD_HASHING_ALGORITHM);
+        settingsList.add(ApiKeyService.STORED_HASH_ALGO_SETTING);
         settingsList.add(ApiKeyService.DELETE_TIMEOUT);
         settingsList.add(ApiKeyService.DELETE_INTERVAL);
         settingsList.add(ApiKeyService.DELETE_RETENTION_PERIOD);
@@ -1821,17 +1821,30 @@ public class Security extends Plugin
                     + " ] setting."
             );
         }
-        Stream.of(ApiKeyService.PASSWORD_HASHING_ALGORITHM, XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM).forEach((setting) -> {
-            final var storedHashAlgo = setting.get(settings);
-            if (storedHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
-                // log instead of validation error for backwards compatibility
-                logger.warn(
-                    "Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. "
-                        + "Please set the appropriate value for [{}] setting.",
-                    setting.getKey()
-                );
-            }
-        });
+
+        final var serviceTokenStoredHashSettings = XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM;
+        final var serviceTokenStoredHashAlgo = serviceTokenStoredHashSettings.get(settings);
+        if (serviceTokenStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
+            // log instead of validation error for backwards compatibility
+            logger.warn(
+                "Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. "
+                    + "Please set the appropriate value for [{}] setting.",
+                serviceTokenStoredHashSettings.getKey()
+            );
+        }
+
+        final var apiKeyStoredHashSettings = ApiKeyService.STORED_HASH_ALGO_SETTING;
+        final var apiKeyStoredHashAlgo = apiKeyStoredHashSettings.get(settings);
+        if (apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("ssha256") == false
+            && apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
+            // log instead of validation error for backwards compatibility
+            logger.warn(
+                "[{}] is not recommended for stored API key hashing in a FIPS 140 JVM. The recommended hasher for [{}] is SSHA256.",
+                apiKeyStoredHashSettings,
+                apiKeyStoredHashSettings.getKey()
+            );
+        }
+
         final var cacheHashAlgoSettings = settings.filter(k -> k.endsWith(".cache.hash_algo"));
         cacheHashAlgoSettings.keySet().forEach((key) -> {
             final var setting = cacheHashAlgoSettings.get(key);

+ 2 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java

@@ -72,11 +72,11 @@ public class ChangePasswordRequestBuilder extends ActionRequestBuilder<ChangePas
     public ChangePasswordRequestBuilder passwordHash(char[] passwordHashChars, Hasher configuredHasher) {
         final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHashChars);
         if (resolvedHasher.equals(configuredHasher) == false
-            && Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
+            && Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
             throw new IllegalArgumentException(
                 "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
                     + "The supported password hash algorithms are "
-                    + Hasher.getAvailableAlgoStoredHash().toString()
+                    + Hasher.getAvailableAlgoStoredPasswordHash().toString()
             );
         }
         if (request.passwordHash() != null) {

+ 2 - 2
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java

@@ -52,12 +52,12 @@ public class TransportChangePasswordAction extends HandledTransportAction<Change
         final Hasher requestPwdHashAlgo = Hasher.resolveFromHash(request.passwordHash());
         final Hasher configPwdHashAlgo = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings));
         if (requestPwdHashAlgo.equals(configPwdHashAlgo) == false
-            && Hasher.getAvailableAlgoStoredHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) {
+            && Hasher.getAvailableAlgoStoredPasswordHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) {
             listener.onFailure(
                 new IllegalArgumentException(
                     "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
                         + "The supported password hash algorithms are "
-                        + Hasher.getAvailableAlgoStoredHash().toString()
+                        + Hasher.getAvailableAlgoStoredPasswordHash().toString()
                 )
             );
             return;

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

@@ -38,7 +38,6 @@ import org.elasticsearch.client.internal.Client;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.cache.Cache;
@@ -139,6 +138,7 @@ import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+import static org.elasticsearch.common.SecureRandomUtils.getBase64SecureRandomString;
 import static org.elasticsearch.core.Strings.format;
 import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING;
 import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY;
@@ -158,9 +158,9 @@ public class ApiKeyService implements Closeable {
     private static final Logger logger = LogManager.getLogger(ApiKeyService.class);
     private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ApiKeyService.class);
 
-    public static final Setting<String> PASSWORD_HASHING_ALGORITHM = XPackSettings.defaultStoredHashAlgorithmSetting(
+    public static final Setting<String> STORED_HASH_ALGO_SETTING = XPackSettings.defaultStoredSecureTokenHashAlgorithmSetting(
         "xpack.security.authc.api_key.hashing.algorithm",
-        (s) -> Hasher.PBKDF2.name()
+        (s) -> Hasher.SSHA256.name()
     );
     public static final Setting<TimeValue> DELETE_TIMEOUT = Setting.timeSetting(
         "xpack.security.authc.api_key.delete.timeout",
@@ -181,7 +181,7 @@ public class ApiKeyService implements Closeable {
     );
     public static final Setting<String> CACHE_HASH_ALGO_SETTING = Setting.simpleString(
         "xpack.security.authc.api_key.cache.hash_algo",
-        "ssha256",
+        Hasher.SSHA256.name(),
         Setting.Property.NodeScope
     );
     public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting(
@@ -217,9 +217,9 @@ public class ApiKeyService implements Closeable {
     private final ThreadPool threadPool;
     private final ApiKeyDocCache apiKeyDocCache;
 
-    // The API key secret is a Base64 encoded v4 UUID without padding. The UUID is 128 bits, i.e. 16 byte,
-    // which requires 22 digits of Base64 characters for encoding without padding.
-    // See also UUIDs.randomBase64UUIDSecureString
+    private static final int API_KEY_SECRET_NUM_BYTES = 16;
+    // The API key secret is a Base64 encoded string of 128 random bits.
+    // See getBase64SecureRandomString()
     private static final int API_KEY_SECRET_LENGTH = 22;
     private static final long EVICTION_MONITOR_INTERVAL_SECONDS = 300L; // 5 minutes
     private static final long EVICTION_MONITOR_INTERVAL_NANOS = EVICTION_MONITOR_INTERVAL_SECONDS * 1_000_000_000L;
@@ -245,7 +245,7 @@ public class ApiKeyService implements Closeable {
         this.securityIndex = securityIndex;
         this.clusterService = clusterService;
         this.enabled = XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.get(settings);
-        this.hasher = Hasher.resolve(PASSWORD_HASHING_ALGORITHM.get(settings));
+        this.hasher = Hasher.resolve(STORED_HASH_ALGO_SETTING.get(settings));
         this.settings = settings;
         this.inactiveApiKeysRemover = new InactiveApiKeysRemover(settings, client, clusterService);
         this.threadPool = threadPool;
@@ -545,7 +545,7 @@ public class ApiKeyService implements Closeable {
     ) {
         final Instant created = clock.instant();
         final Instant expiration = getApiKeyExpiration(created, request.getExpiration());
-        final SecureString apiKey = UUIDs.randomBase64UUIDSecureString();
+        final SecureString apiKey = getBase64SecureRandomString(API_KEY_SECRET_NUM_BYTES);
         assert ApiKey.Type.CROSS_CLUSTER != request.getType() || API_KEY_SECRET_LENGTH == apiKey.length()
             : "Invalid API key (name=[" + request.getName() + "], type=[" + request.getType() + "], length=[" + apiKey.length() + "])";
 

+ 66 - 15
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java

@@ -548,7 +548,7 @@ public class SecurityTests extends ESTestCase {
             .put(
                 XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash()
+                    Hasher.getAvailableAlgoStoredPasswordHash()
                         .stream()
                         .filter(alg -> alg.startsWith("pbkdf2") == false)
                         .collect(Collectors.toList())
@@ -567,7 +567,10 @@ public class SecurityTests extends ESTestCase {
             .put(
                 XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList())
+                    Hasher.getAvailableAlgoStoredPasswordHash()
+                        .stream()
+                        .filter(alg -> alg.startsWith("pbkdf2"))
+                        .collect(Collectors.toList())
                 )
             )
             .build();
@@ -581,7 +584,7 @@ public class SecurityTests extends ESTestCase {
             .put(
                 XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash()
+                    Hasher.getAvailableAlgoStoredPasswordHash()
                         .stream()
                         .filter(alg -> alg.startsWith("pbkdf2") == false)
                         .collect(Collectors.toList())
@@ -626,7 +629,7 @@ public class SecurityTests extends ESTestCase {
             .put(
                 XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash()
+                    Hasher.getAvailableAlgoStoredPasswordHash()
                         .stream()
                         .filter(alg -> alg.startsWith("pbkdf2") == false)
                         .collect(Collectors.toList())
@@ -646,19 +649,28 @@ public class SecurityTests extends ESTestCase {
             .put(
                 XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList())
+                    Hasher.getAvailableAlgoStoredPasswordHash()
+                        .stream()
+                        .filter(alg -> alg.startsWith("pbkdf2"))
+                        .collect(Collectors.toList())
                 )
             )
             .put(
                 XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList())
+                    Hasher.getAvailableAlgoStoredPasswordHash()
+                        .stream()
+                        .filter(alg -> alg.startsWith("pbkdf2"))
+                        .collect(Collectors.toList())
                 )
             )
             .put(
-                ApiKeyService.PASSWORD_HASHING_ALGORITHM.getKey(),
+                ApiKeyService.STORED_HASH_ALGO_SETTING.getKey(),
                 randomFrom(
-                    Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList())
+                    Hasher.getAvailableAlgoStoredPasswordHash()
+                        .stream()
+                        .filter(alg -> alg.startsWith("pbkdf2"))
+                        .collect(Collectors.toList())
                 )
             )
             .put(
@@ -683,13 +695,37 @@ public class SecurityTests extends ESTestCase {
         assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantCacheHash(key));
     }
 
-    public void testValidateForFipsNonFipsCompliantStoredHashAlgoWarningLog() throws IllegalAccessException {
-        String key = randomFrom(ApiKeyService.PASSWORD_HASHING_ALGORITHM, XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM).getKey();
+    public void testValidateForFipsNonFipsCompliantStoredHashAlgoWarningLog() {
+        String key = XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.getKey();
         final Settings settings = Settings.builder()
             .put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true)
-            .put(key, randomNonFipsCompliantStoredHash())
+            .put(key, randomNonFipsCompliantStoredPasswordHash())
             .build();
-        assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredHash(key));
+        assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredPasswordHash(key));
+    }
+
+    public void testValidateForFipsNonFipsCompliantApiKeyStoredHashAlgoWarningLog() {
+        var nonCompliant = randomFrom(
+            Hasher.getAvailableAlgoStoredPasswordHash()
+                .stream()
+                .filter(alg -> alg.startsWith("pbkdf2") == false && alg.startsWith("ssha256") == false)
+                .collect(Collectors.toList())
+        );
+        String key = ApiKeyService.STORED_HASH_ALGO_SETTING.getKey();
+        final Settings settings = Settings.builder().put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true).put(key, nonCompliant).build();
+        assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredApiKeyHash(key));
+    }
+
+    public void testValidateForFipsFipsCompliantApiKeyStoredHashAlgoWarningLog() {
+        var compliant = randomFrom(
+            Hasher.getAvailableAlgoStoredPasswordHash()
+                .stream()
+                .filter(alg -> alg.startsWith("pbkdf2") || alg.startsWith("ssha256"))
+                .collect(Collectors.toList())
+        );
+        String key = ApiKeyService.STORED_HASH_ALGO_SETTING.getKey();
+        final Settings settings = Settings.builder().put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true).put(key, compliant).build();
+        assertThatLogger(() -> Security.validateForFips(settings), Security.class);
     }
 
     public void testValidateForMultipleNonFipsCompliantCacheHashAlgoWarningLogs() throws IllegalAccessException {
@@ -1135,9 +1171,12 @@ public class SecurityTests extends ESTestCase {
         );
     }
 
-    private String randomNonFipsCompliantStoredHash() {
+    private String randomNonFipsCompliantStoredPasswordHash() {
         return randomFrom(
-            Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2") == false).collect(Collectors.toList())
+            Hasher.getAvailableAlgoStoredPasswordHash()
+                .stream()
+                .filter(alg -> alg.startsWith("pbkdf2") == false)
+                .collect(Collectors.toList())
         );
     }
 
@@ -1153,7 +1192,19 @@ public class SecurityTests extends ESTestCase {
         );
     }
 
-    private MockLog.SeenEventExpectation logEventForNonCompliantStoredHash(String settingKey) {
+    private MockLog.SeenEventExpectation logEventForNonCompliantStoredApiKeyHash(String settingKey) {
+        return new MockLog.SeenEventExpectation(
+            "cache hash not fips compliant",
+            Security.class.getName(),
+            Level.WARN,
+            "[*] is not recommended for stored API key hashing in a FIPS 140 JVM. "
+                + "The recommended hasher for ["
+                + settingKey
+                + "] is SSHA256."
+        );
+    }
+
+    private MockLog.SeenEventExpectation logEventForNonCompliantStoredPasswordHash(String settingKey) {
         return new MockLog.SeenEventExpectation(
             "stored hash not fips compliant",
             Security.class.getName(),

+ 1 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilderTests.java

@@ -76,7 +76,7 @@ public class ChangePasswordRequestBuilderTests extends ESTestCase {
     }
 
     public void testWithHashedPasswordNotHash() {
-        final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredHash()).toUpperCase(Locale.ROOT));
+        final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredPasswordHash()).toUpperCase(Locale.ROOT));
         final char[] hash = randomAlphaOfLength(20).toCharArray();
         final String json = Strings.format("""
             {

+ 1 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestBuilderTests.java

@@ -205,7 +205,7 @@ public class PutUserRequestBuilderTests extends ESTestCase {
     }
 
     public void testWithPasswordHashThatsNotReallyAHash() throws IOException {
-        final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredHash()).toUpperCase(Locale.ROOT));
+        final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredPasswordHash()).toUpperCase(Locale.ROOT));
         final char[] hash = randomAlphaOfLengthBetween(14, 20).toCharArray();
         final String json = Strings.format("""
             {