Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
74118b1
WIP
n1v0lg Jan 14, 2025
f3e00ca
Separate stored hash method
n1v0lg Jan 20, 2025
b2269e8
More
n1v0lg Jan 20, 2025
df2b60c
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 20, 2025
f3aeba9
Fix NPE
n1v0lg Jan 20, 2025
2ea9110
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 28, 2025
0fde9ec
Tweak
n1v0lg Jan 28, 2025
82a1ce7
Clean up randomness
n1v0lg Jan 28, 2025
a1094cc
Update docs/changelog/120997.yaml
n1v0lg Jan 28, 2025
14d5774
Undo
n1v0lg Jan 28, 2025
1d8b4b1
Merge branch 'api-key-stored-hash' of github.com:n1v0lg/elasticsearch…
n1v0lg Jan 28, 2025
61a960c
Docs etc
n1v0lg Jan 28, 2025
22bb9ce
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 28, 2025
872ab87
More docs
n1v0lg Jan 28, 2025
4346eb8
Merge branch 'api-key-stored-hash' of github.com:n1v0lg/elasticsearch…
n1v0lg Jan 28, 2025
064bbca
Link to FIPS docs
n1v0lg Jan 28, 2025
5d19199
Docs nits
n1v0lg Jan 28, 2025
ede049c
es
n1v0lg Jan 28, 2025
18db8e6
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 28, 2025
7fd23da
Handle FIPS mode
n1v0lg Jan 28, 2025
ed954c2
Fix docs
n1v0lg Jan 28, 2025
daaef9b
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 28, 2025
94746ae
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 29, 2025
de8c8f6
Fix and test
n1v0lg Jan 29, 2025
949cb0b
Merge branch 'api-key-stored-hash' of github.com:n1v0lg/elasticsearch…
n1v0lg Jan 29, 2025
62a447e
One more
n1v0lg Jan 29, 2025
eb97f47
Javadoc
n1v0lg Jan 29, 2025
35ba1bd
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 29, 2025
2389206
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 29, 2025
681a971
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 29, 2025
5099653
Merge branch 'main' into api-key-stored-hash
n1v0lg Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/120997.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 120997
summary: Allow `SSHA-256` for API key credential hash
area: Authentication
type: enhancement
issues: []
64 changes: 64 additions & 0 deletions docs/reference/settings/security-hash-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
|=======================
8 changes: 4 additions & 4 deletions docs/reference/settings/security-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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. +
+
--
Expand Down Expand Up @@ -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.

Expand All @@ -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]]
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public Iterator<Setting<?>> settings() {

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)) {
Expand All @@ -251,19 +251,56 @@ public Iterator<Setting<?>> settings() {
}
);

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()
);

/*
* 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<>(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<>(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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ public PutUserRequestBuilder email(String email) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,14 +735,28 @@ private static boolean verifyBcryptHash(SecureString text, char[] hash) {
* 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))
.filter(name -> (name.startsWith("pbkdf2") || name.startsWith("bcrypt")))
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1472,7 +1472,7 @@ public static List<Setting<?>> getSettings(List<SecurityExtension> securityExten
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);
Expand Down Expand Up @@ -1818,17 +1818,30 @@ static void validateForFips(Settings settings) {
+ " ] 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ public ChangePasswordRequestBuilder password(char[] password, Hasher hasher) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene
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;
Expand Down
Loading