-
Notifications
You must be signed in to change notification settings - Fork 25.8k
Setting elastic password from stored hash #77036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
0ffe056
621c735
1aaf1f9
9fd474c
7260cb4
92a48eb
9c543fa
2fcca5c
c28a3ad
1e09d7a
e985d4e
42cd17a
43ab0f5
d0fafd6
25a97bb
6227d1f
a4424d9
dce0ef5
91d590a
5322487
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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 | ||
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seeing this code, I'm having second thoughts about the plan here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's the other way round, right ? We can simplify this very much by just returning 401 in all of our cases too. What we were trying to capture was a difference between:
The contract to the user is that
but maybe we're thinking this too much from the user's perspective? There is no mandate that a 401 is permanent or that the user should not retry the request etc. We were trying to use this to signal our logic.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks for writing this down! On point 2) above. That something else that already set the password could be an identical process racing to set the password after a successful validation of the hash in the keystore. The racing process could be on the same node, writing the same password hash in the index, or on a different node, potentially writing a different one, or it can even be a password change by the user through the API or the cmd line tool. In this general case (without knowing who set the elastic user in the index in the meantime) returning 401 or 200 are both wrong (the only two error codes we can currently return for authn failures). Authentication has to be retried, given the new state where there is an elastic user in the .security index (which would make authn validate that instead of the keystore value). Point 3) sounds right to me, apart that maybe we should return 503. But I missed that there are a bunch of other error conditions that should probably be 503 and are now 401, eg shards unavailable for the .security index (though those are "read" ops and we're going to return 503 for "write" ones). Overall, after this conversation, I still think returning 503 if the "promised" password cannot be persisted (because the index or document cannot be created, or because they have been created in the meantime by something else) is the best course of action, because this "write" op during authentication is the only exception (which is not the approach this PR implements, FWIW). |
||
| ese = authenticationError(message, t, args); | ||
| containsNegotiateWithToken = false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
@@ -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); | ||
| }); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here is wrong, I did some testing. This is tricky I need to think some more for the proper fix.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From #77036 (comment) , I think we should return 503 (slightly more suggestive than 500, I think) when the promised password cannot be persisted. This is what we set about to do, though back then I haven't considered that any other failures return 401. |
||
|
|
||
| /** | ||
| * 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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()); | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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 { | ||
|
|
@@ -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); | ||
|
|
@@ -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 { | ||
|
|
@@ -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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still don't like that a "get" does a "set", and that it takes a credentials parameter for it, which can be null but then the callees can't deal with a null parameter: this code will fail the lookup of the |
||
| 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); | ||
| } | ||
|
|
@@ -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); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.