Skip to content

Commit

Permalink
Setting elastic password from stored hash
Browse files Browse the repository at this point in the history
In package installations, we will be generating the password of
the elastic user on installation and we will be storing the hash
of it to the elasticsearch.keystore.
This change will
- attempt to create the security index and elastic user document at the
time of the first successful authentication using the
the "promised" password
- if bootstrap.password setting is detected, won’t attempt to use
autoconfiguration.password_hash setting and proceed with the normal
authentication path
- if elastic user document exists, won't attempt to use
autoconfiguration.password_hash setting and proceed with the normal
authentication path
- if unable  to create the Security Index for any reason, returns
HTTP_SERVER_ERROR to indicate that the cluster is probably not ready yet

Resolves: elastic#75704
  • Loading branch information
BigPandaToo committed Aug 30, 2021
1 parent 179a3fc commit 0ffe056
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package org.elasticsearch.xpack.core.security.authc;

import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
Expand All @@ -20,7 +21,9 @@
import java.util.Map;
import java.util.stream.Collectors;

import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
import static org.elasticsearch.xpack.core.security.support.Exceptions.internalServerError;

/**
* The default implementation of a {@link AuthenticationFailureHandler}. This
Expand Down Expand Up @@ -160,6 +163,9 @@ private ElasticsearchSecurityException createAuthenticationError(final String me
} else {
containsNegotiateWithToken = false;
}
} else if (t instanceof ElasticsearchStatusException && ((ElasticsearchStatusException) t).status() == INTERNAL_SERVER_ERROR) {
ese = internalServerError(message, t, args);
containsNegotiateWithToken = false;
} else {
ese = authenticationError(message, t, args);
containsNegotiateWithToken = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ public static ElasticsearchSecurityException authorizationError(String msg, Obje
public static ElasticsearchSecurityException authorizationError(String msg, Exception cause, Object... args) {
return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, cause, args);
}

public static ElasticsearchSecurityException internalServerError(String msg, Throwable cause, Object... args) {
return new ElasticsearchSecurityException(msg, RestStatus.INTERNAL_SERVER_ERROR, cause, args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
Expand Down Expand Up @@ -60,6 +62,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;

import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin;
Expand Down Expand Up @@ -283,6 +286,25 @@ private void createReservedUser(String username, char[] passwordHash, RefreshPol
});
}

/**
* Asynchronous method to create a security index if necessary and a reserved user if one doesn't exist
*/
public void createReservedUserAndGetUserInfo(String username, char[] passwordHash, RefreshPolicy refresh,
ActionListener<ReservedUserInfo> listener) {
securityIndex.prepareIndexIfNeededThenExecute((e) -> { listener.onFailure(new ElasticsearchStatusException(e.getMessage(),
INTERNAL_SERVER_ERROR, e.getCause())); },
() -> { executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN,
client.prepareIndex(SECURITY_MAIN_ALIAS).setOpType(DocWriteRequest.OpType.CREATE)
.setId(getIdForUser(RESERVED_USER_TYPE, username))
.setSource(Fields.PASSWORD.getPreferredName(), String.valueOf(passwordHash),
Fields.ENABLED.getPreferredName(),
true, Fields.TYPE.getPreferredName(), RESERVED_USER_TYPE)
.setRefreshPolicy(refresh).request(),
listener.<IndexResponse>delegateFailure((l, indexResponse) -> getReservedUserInfo(username, l)),
client::index);
});
}

/**
* Asynchronous method to put a user. A put user request without a password hash is treated as an update and will fail with a
* {@link ValidationException} if the user does not exist. If a password hash is provided, then we issue a update request with an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.env.Environment;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.xpack.core.XPackSettings;
Expand Down Expand Up @@ -59,14 +61,16 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
private final ReservedUserInfo bootstrapUserInfo;
public static final Setting<SecureString> BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("bootstrap.password",
KeyStoreWrapper.SEED_SETTING);
public static final Setting<SecureString> AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH =
SecureSetting.secureString("autoconfig.password_hash", null);
public static final Setting<SecureString> AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH =
SecureSetting.secureString("autoconfiguration.password_hash", null);

private final NativeUsersStore nativeUsersStore;
private final AnonymousUser anonymousUser;
private final boolean realmEnabled;
private final boolean anonymousEnabled;
private final SecurityIndexManager securityIndex;
private final boolean bootstrapPasswordExists;
private final boolean autoconfigHashExists;

private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName());

Expand All @@ -83,8 +87,12 @@ public ReservedRealm(Environment env, Settings settings, NativeUsersStore native
this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings);
this.securityIndex = securityIndex;
final Hasher reservedRealmHasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings));
final char[] hash = BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0 ? new char[0] :
reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings));
autoconfigHashExists = AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.exists(settings);
bootstrapPasswordExists = BOOTSTRAP_ELASTIC_PASSWORD.exists(settings);
final char[] hash = (bootstrapPasswordExists || autoconfigHashExists == false) ?
(BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0 ?
new char[0] : reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings))) :
AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.get(settings).getChars();
bootstrapUserInfo = new ReservedUserInfo(hash, true);
}

Expand All @@ -95,7 +103,7 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener<Authen
} else if (ClientReservedRealm.isReserved(token.principal(), config.settings()) == false) {
listener.onResponse(AuthenticationResult.notHandled());
} else {
getUserInfo(token.principal(), ActionListener.wrap((userInfo) -> {
getUserInfo(token.principal(), token.credentials(), ActionListener.wrap((userInfo) -> {
AuthenticationResult result;
if (userInfo != null) {
try {
Expand All @@ -113,7 +121,7 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener<Authen
Arrays.fill(userInfo.passwordHash, (char) 0);
}
} else {
result = AuthenticationResult.terminate("failed to authenticate user [" + token.principal() + "]", null);
result = AuthenticationResult.terminate("] [" + token.principal() + "]", null);
}
// we want the finally block to clear out the chars before we proceed further so we handle the result here
listener.onResponse(result);
Expand All @@ -134,7 +142,7 @@ protected void doLookupUser(String username, ActionListener<User> listener) {
} else if (AnonymousUser.isAnonymousUsername(username, config.settings())) {
listener.onResponse(anonymousEnabled ? anonymousUser : null);
} else {
getUserInfo(username, ActionListener.wrap((userInfo) -> {
getUserInfo(username, null, ActionListener.wrap((userInfo) -> {
if (userInfo != null) {
listener.onResponse(getUser(username, userInfo));
} else {
Expand Down Expand Up @@ -212,13 +220,25 @@ public void users(ActionListener<Collection<User>> listener) {
}


private void getUserInfo(final String username, ActionListener<ReservedUserInfo> listener) {
private void getUserInfo(final String username, @Nullable SecureString credentials, ActionListener<ReservedUserInfo> listener) {
if (securityIndex.indexExists() == false) {
listener.onResponse(getDefaultUserInfo(username));
if ((bootstrapPasswordExists == false && autoconfigHashExists) && username.equals(ElasticUser.NAME)
&& bootstrapUserInfo.verifyPassword(credentials)) {
nativeUsersStore.createReservedUserAndGetUserInfo(username, bootstrapUserInfo.passwordHash,
WriteRequest.RefreshPolicy.IMMEDIATE, listener);
} else {
listener.onResponse(getDefaultUserInfo(username));
}
} else {
nativeUsersStore.getReservedUserInfo(username, ActionListener.wrap((userInfo) -> {
if (userInfo == null) {
listener.onResponse(getDefaultUserInfo(username));
if ((bootstrapPasswordExists == false && autoconfigHashExists) && username.equals(ElasticUser.NAME)
&& bootstrapUserInfo.verifyPassword(credentials)) {
nativeUsersStore.createReservedUserAndGetUserInfo(username, bootstrapUserInfo.passwordHash,
WriteRequest.RefreshPolicy.IMMEDIATE, listener);
} else {
listener.onResponse(getDefaultUserInfo(username));
}
} else {
listener.onResponse(userInfo);
}
Expand Down Expand Up @@ -250,5 +270,6 @@ private ReservedUserInfo getDefaultUserInfo(String username) {

public static void addSettings(List<Setting<?>> settingsList) {
settingsList.add(BOOTSTRAP_ELASTIC_PASSWORD);
settingsList.add(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;

import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH;
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH;
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;

/**
Expand Down Expand Up @@ -53,7 +53,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
SecureString elasticPassword = new SecureString(generatePassword(20));
KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> new SecureString(new char[0]))
) {
nodeKeystore.setString(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword));
nodeKeystore.setString(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword));
nodeKeystore.save(env.configFile(), new char[0]);
terminal.print(Terminal.Verbosity.NORMAL, elasticPassword.toString());
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import java.util.Map;

import static org.elasticsearch.test.SecurityIntegTestCase.getFastStoredHashAlgoForTests;
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH;
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
Expand Down Expand Up @@ -98,7 +98,7 @@ public void testSuccessfullyGenerateAndStoreHash() throws Exception {
assertNotNull(keyStoreWrapper);
keyStoreWrapper.decrypt(new char[0]);
assertThat(keyStoreWrapper.getSettingNames(),
containsInAnyOrder(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed"));
containsInAnyOrder(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed"));
}

public void testExistingKeystoreWithWrongPassword() throws Exception {
Expand Down

0 comments on commit 0ffe056

Please sign in to comment.