Prevent default passwords in production mode (elastic/elasticsearch#4724)

Adds a new `xpack.security.authc.accept_default_password` setting that defaults to `true`. If it is set to false, then the default password is not accepted in the reserved realm.

Adds a bootstrap check that the above setting must be set to `false` if security is enabled.  

Adds docs for the new setting and bootstrap.

Changed `/_enable` and `/_disable`, to store a blank password if the user record did not previously exist, which is interpreted to mean "treat this user as having the default password". The previous functionality would explicitly set the user's password to `changeme`, which would then prevent the new configuration setting from doing its job.

For any existing reserved users that had their password set to `changeme`, migrates them to the blank password (per above paragraph)

Closes: elastic/elasticsearch#4333

Original commit: elastic/x-pack-elasticsearch@db64564093
This commit is contained in:
Tim Vernum 2017-02-08 16:19:55 +11:00 committed by GitHub
parent aadbe81767
commit 734a4ee66d
14 changed files with 599 additions and 61 deletions

View File

@ -14,6 +14,7 @@ import org.elasticsearch.action.support.DestructiveOperations;
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.Nullable;
@ -108,6 +109,7 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
import org.elasticsearch.xpack.security.authz.store.ReservedRolesStore;
import org.elasticsearch.xpack.security.bootstrap.DefaultPasswordBootstrapCheck;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
@ -416,6 +418,7 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
AnonymousUser.addSettings(settingsList);
RealmSettings.addSettings(settingsList, extensionsService == null ? null : extensionsService.getExtensions());
NativeRolesStore.addSettings(settingsList);
ReservedRealm.addSettings(settingsList);
AuthenticationService.addSettings(settingsList);
AuthorizationService.addSettings(settingsList);
settingsList.add(CompositeRolesStore.CACHE_SIZE_SETTING);
@ -452,7 +455,10 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
public List<BootstrapCheck> getBootstrapChecks() {
if (enabled) {
return Collections.singletonList(new SSLBootstrapCheck(sslService, settings, env));
return Arrays.asList(
new DefaultPasswordBootstrapCheck(settings),
new SSLBootstrapCheck(sslService, settings, env)
);
} else {
return Collections.emptyList();
}

View File

@ -5,6 +5,12 @@
*/
package org.elasticsearch.xpack.security.authc.esnative;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
@ -12,14 +18,18 @@ import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.common.GroupedActionListener;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.xpack.security.SecurityTemplateService;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
/**
* Performs migration steps for the {@link NativeRealm} and {@link ReservedRealm}.
* When upgrading an Elasticsearch/X-Pack installation from a previous version, this class is responsible for ensuring that user/role
* data stored in the security index is converted to a format that is appropriate for the newly installed version.
*
* @see SecurityTemplateService
*/
public class NativeRealmMigrator {
@ -48,31 +58,34 @@ public class NativeRealmMigrator {
*/
public void performUpgrade(@Nullable Version previousVersion, ActionListener<Boolean> listener) {
try {
if (shouldDisableLogstashUser(previousVersion)) {
logger.info("Upgrading security from version [{}] - new reserved user [{}] will default to disabled",
previousVersion, LogstashSystemUser.NAME);
// Only clear the cache is authentication is allowed by the current license
// otherwise the license management checks will prevent it from completing successfully.
final boolean clearCache = licenseState.isAuthAllowed();
nativeUsersStore.ensureReservedUserIsDisabled(LogstashSystemUser.NAME, clearCache, new ActionListener<Void>() {
@Override
public void onResponse(Void aVoid) {
listener.onResponse(true);
}
@Override
public void onFailure(Exception e) {
listener.onFailure(e);
}
});
} else {
List<BiConsumer<Version, ActionListener<Void>>> tasks = collectUpgradeTasks(previousVersion);
if (tasks.isEmpty()) {
listener.onResponse(false);
} else {
final GroupedActionListener<Void> countDownListener = new GroupedActionListener<>(
ActionListener.wrap(r -> listener.onResponse(true), listener::onFailure),
tasks.size(),
Collections.emptyList()
);
logger.info("Performing {} security migration task(s)", tasks.size());
tasks.forEach(t -> t.accept(previousVersion, countDownListener));
}
} catch (Exception e) {
listener.onFailure(e);
}
}
private List<BiConsumer<Version, ActionListener<Void>>> collectUpgradeTasks(@Nullable Version previousVersion) {
List<BiConsumer<Version, ActionListener<Void>>> tasks = new ArrayList<>();
if (shouldDisableLogstashUser(previousVersion)) {
tasks.add(this::doDisableLogstashUser);
}
if (shouldConvertDefaultPasswords(previousVersion)) {
tasks.add(this::doConvertDefaultPasswords);
}
return tasks;
}
/**
* If we're upgrading from a security version where the {@link LogstashSystemUser} did not exist, then we mark the user as disabled.
* Otherwise the user will exist with a default password, which is desirable for an <em>out-of-the-box</em> experience in fresh installs
@ -82,4 +95,67 @@ public class NativeRealmMigrator {
return previousVersion != null && previousVersion.before(LogstashSystemUser.DEFINED_SINCE);
}
private void doDisableLogstashUser(@Nullable Version previousVersion, ActionListener<Void> listener) {
logger.info("Upgrading security from version [{}] - new reserved user [{}] will default to disabled",
previousVersion, LogstashSystemUser.NAME);
// Only clear the cache is authentication is allowed by the current license
// otherwise the license management checks will prevent it from completing successfully.
final boolean clearCache = licenseState.isAuthAllowed();
nativeUsersStore.ensureReservedUserIsDisabled(LogstashSystemUser.NAME, clearCache, listener);
}
/**
* Old versions of X-Pack security would assign the default password content to a user if it was enabled/disabled before the password
* was explicitly set to another value. If upgrading from one of those versions, then we want to change those users to be flagged as
* having a "default password" (which is stored as blank) so that {@link ReservedRealm#ACCEPT_DEFAULT_PASSWORD_SETTING} does the
* right thing.
*/
private boolean shouldConvertDefaultPasswords(@Nullable Version previousVersion) {
return previousVersion != null && previousVersion.before(Version.V_6_0_0_alpha1_UNRELEASED);
}
@SuppressWarnings("unused")
private void doConvertDefaultPasswords(@Nullable Version previousVersion, ActionListener<Void> listener) {
nativeUsersStore.getAllReservedUserInfo(ActionListener.wrap(
users -> {
final List<String> toConvert = users.entrySet().stream()
.filter(entry -> hasOldStyleDefaultPassword(entry.getValue()))
.map(entry -> entry.getKey())
.collect(Collectors.toList());
if (toConvert.isEmpty()) {
listener.onResponse(null);
} else {
GroupedActionListener countDownListener = new GroupedActionListener(
ActionListener.wrap((r) -> listener.onResponse(null), listener::onFailure),
toConvert.size(), Collections.emptyList()
);
toConvert.forEach(username -> {
logger.debug(
"Upgrading security from version [{}] - marking reserved user [{}] as having default password",
previousVersion, username);
resetReservedUserPassword(username, countDownListener);
});
}
}, listener::onFailure)
);
}
/**
* Determines whether the supplied {@link NativeUsersStore.ReservedUserInfo} has its password set to be the default password, without
* having the {@link NativeUsersStore.ReservedUserInfo#hasDefaultPassword} flag set.
*/
private boolean hasOldStyleDefaultPassword(NativeUsersStore.ReservedUserInfo userInfo) {
return userInfo.hasDefaultPassword == false && Hasher.BCRYPT.verify(ReservedRealm.DEFAULT_PASSWORD_TEXT, userInfo.passwordHash);
}
/**
* Sets a reserved user's password back to blank, so that the default password functionality applies.
*/
void resetReservedUserPassword(String username, ActionListener<Void> listener) {
final ChangePasswordRequest request = new ChangePasswordRequest();
request.username(username);
request.passwordHash(new char[0]);
nativeUsersStore.changePassword(request, true, listener);
}
}

View File

@ -41,11 +41,13 @@ import org.elasticsearch.index.engine.DocumentMissingException;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.xpack.common.GroupedActionListener;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityTemplateService;
import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequestBuilder;
import org.elasticsearch.xpack.security.action.user.DeleteUserRequest;
import org.elasticsearch.xpack.security.action.user.PutUserRequest;
import org.elasticsearch.xpack.security.authc.support.Hasher;
@ -89,7 +91,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
}
private static final String USER_DOC_TYPE = "user";
private static final String RESERVED_USER_DOC_TYPE = "reserved-user";
static final String RESERVED_USER_DOC_TYPE = "reserved-user";
private final Hasher hasher = Hasher.BCRYPT;
private final AtomicReference<State> state = new AtomicReference<>(State.INITIALIZED);
@ -210,6 +212,15 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
* with a hash of the provided password.
*/
public void changePassword(final ChangePasswordRequest request, final ActionListener<Void> listener) {
changePassword(request, false, listener);
}
/**
* This version of {@link #changePassword(ChangePasswordRequest, ActionListener)} exists to that the {@link NativeRealmMigrator}
* can force change passwords before the security mapping is {@link #canWrite ready for writing}
* @param forceWrite If <code>true</code>, allow the change to take place even if the store is currently read-only.
*/
void changePassword(final ChangePasswordRequest request, boolean forceWrite, final ActionListener<Void> listener) {
final String username = request.username();
assert SystemUser.NAME.equals(username) == false && XPackUser.NAME.equals(username) == false : username + "is internal!";
if (state() != State.STARTED) {
@ -218,7 +229,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
} else if (isTribeNode) {
listener.onFailure(new UnsupportedOperationException("users may not be created or modified using a tribe node"));
return;
} else if (canWrite == false) {
} else if (canWrite == false && forceWrite == false) {
listener.onFailure(new IllegalStateException("password cannot be changed as user service cannot write until template and " +
"mappings are up to date"));
return;
@ -449,7 +460,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
client.prepareUpdate(SecurityTemplateService.SECURITY_INDEX_NAME, RESERVED_USER_DOC_TYPE, username)
.setDoc(Requests.INDEX_CONTENT_TYPE, User.Fields.ENABLED.getPreferredName(), enabled)
.setUpsert(XContentType.JSON,
User.Fields.PASSWORD.getPreferredName(), String.valueOf(ReservedRealm.DEFAULT_PASSWORD_HASH),
User.Fields.PASSWORD.getPreferredName(), "",
User.Fields.ENABLED.getPreferredName(), enabled)
.setRefreshPolicy(refreshPolicy)
.execute(new ActionListener<UpdateResponse>() {
@ -620,12 +631,14 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
Map<String, Object> sourceMap = getResponse.getSourceAsMap();
String password = (String) sourceMap.get(User.Fields.PASSWORD.getPreferredName());
Boolean enabled = (Boolean) sourceMap.get(Fields.ENABLED.getPreferredName());
if (password == null || password.isEmpty()) {
listener.onFailure(new IllegalStateException("password hash must not be empty!"));
if (password == null) {
listener.onFailure(new IllegalStateException("password hash must not be null!"));
} else if (enabled == null) {
listener.onFailure(new IllegalStateException("enabled must not be null!"));
} else if (password.isEmpty()) {
listener.onResponse(new ReservedUserInfo(ReservedRealm.DEFAULT_PASSWORD_HASH, enabled, true));
} else {
listener.onResponse(new ReservedUserInfo(password.toCharArray(), enabled));
listener.onResponse(new ReservedUserInfo(password.toCharArray(), enabled, false));
}
} else {
listener.onResponse(null);
@ -671,7 +684,7 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
listener.onFailure(new IllegalStateException("enabled must not be null!"));
break;
} else {
userInfos.put(searchHit.getId(), new ReservedUserInfo(password.toCharArray(), enabled));
userInfos.put(searchHit.getId(), new ReservedUserInfo(password.toCharArray(), enabled, false));
}
}
listener.onResponse(userInfos);
@ -786,10 +799,12 @@ public class NativeUsersStore extends AbstractComponent implements ClusterStateL
final char[] passwordHash;
final boolean enabled;
final boolean hasDefaultPassword;
ReservedUserInfo(char[] passwordHash, boolean enabled) {
ReservedUserInfo(char[] passwordHash, boolean enabled, boolean hasDefaultPassword) {
this.passwordHash = passwordHash;
this.enabled = enabled;
this.hasDefaultPassword = hasDefaultPassword;
}
}
}

View File

@ -9,9 +9,11 @@ import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.XPackSettings;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.authc.RealmConfig;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo;
import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm;
@ -30,7 +32,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
/**
* A realm for predefined users. These users can only be modified in terms of changing their passwords; no other modifications are allowed.
@ -39,26 +40,34 @@ import java.util.function.Predicate;
public class ReservedRealm extends CachingUsernamePasswordRealm {
public static final String TYPE = "reserved";
static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(new SecuredString("changeme".toCharArray()));
private static final ReservedUserInfo DEFAULT_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, true);
private static final ReservedUserInfo DISABLED_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, false);
static final SecuredString DEFAULT_PASSWORD_TEXT = new SecuredString("changeme".toCharArray());
static final char[] DEFAULT_PASSWORD_HASH = Hasher.BCRYPT.hash(DEFAULT_PASSWORD_TEXT);
private static final ReservedUserInfo DEFAULT_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, true, true);
private static final ReservedUserInfo DISABLED_USER_INFO = new ReservedUserInfo(DEFAULT_PASSWORD_HASH, false, true);
public static final Setting<Boolean> ACCEPT_DEFAULT_PASSWORD_SETTING = Setting.boolSetting(
Security.setting("authc.accept_default_password"), true, Setting.Property.NodeScope, Setting.Property.Filtered);
private final NativeUsersStore nativeUsersStore;
private final AnonymousUser anonymousUser;
private final boolean realmEnabled;
private final boolean anonymousEnabled;
private final boolean enabled;
private final boolean defaultPasswordEnabled;
public ReservedRealm(Environment env, Settings settings, NativeUsersStore nativeUsersStore, AnonymousUser anonymousUser) {
super(TYPE, new RealmConfig(TYPE, Settings.EMPTY, settings, env));
this.nativeUsersStore = nativeUsersStore;
this.enabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
this.realmEnabled = XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings);
this.anonymousUser = anonymousUser;
this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings);
this.defaultPasswordEnabled = ACCEPT_DEFAULT_PASSWORD_SETTING.get(settings);
}
@Override
protected void doAuthenticate(UsernamePasswordToken token, ActionListener<User> listener) {
if (enabled == false) {
if (realmEnabled == false) {
listener.onResponse(null);
} else if (isReserved(token.principal(), config.globalSettings()) == false) {
listener.onResponse(null);
@ -67,7 +76,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
Runnable action;
if (userInfo != null) {
try {
if (Hasher.BCRYPT.verify(token.credentials(), userInfo.passwordHash)) {
if (verifyPassword(userInfo, token)) {
final User user = getUser(token.principal(), userInfo);
action = () -> listener.onResponse(user);
} else {
@ -89,9 +98,19 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
}
}
private boolean verifyPassword(ReservedUserInfo userInfo, UsernamePasswordToken token) {
if (Hasher.BCRYPT.verify(token.credentials(), userInfo.passwordHash)) {
if (userInfo.hasDefaultPassword && this.defaultPasswordEnabled == false) {
return false;
}
return true;
}
return false;
}
@Override
protected void doLookupUser(String username, ActionListener<User> listener) {
if (enabled == false) {
if (realmEnabled == false) {
if (anonymousEnabled && AnonymousUser.isAnonymousUsername(username, config.globalSettings())) {
listener.onResponse(anonymousUser);
}
@ -143,7 +162,7 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
public void users(ActionListener<Collection<User>> listener) {
if (nativeUsersStore.started() == false || enabled == false) {
if (nativeUsersStore.started() == false || realmEnabled == false) {
listener.onResponse(anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList());
} else {
nativeUsersStore.getAllReservedUserInfo(ActionListener.wrap((reservedUserInfos) -> {
@ -207,4 +226,8 @@ public class ReservedRealm extends CachingUsernamePasswordRealm {
return Version.V_5_0_0;
}
}
public static void addSettings(List<Setting<?>> settingsList) {
settingsList.add(ACCEPT_DEFAULT_PASSWORD_SETTING);
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.bootstrap;
import java.util.Locale;
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
public class DefaultPasswordBootstrapCheck implements BootstrapCheck {
private Settings settings;
public DefaultPasswordBootstrapCheck(Settings settings) {
this.settings = settings;
}
/**
* This check fails if the "accept default password" is set to <code>true</code>.
*/
@Override
public boolean check() {
return ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.get(settings) == true;
}
@Override
public String errorMessage() {
return String.format(Locale.ROOT, "The configuration setting '%s' is %s - it should be set to false",
ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey(),
ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.exists(settings) ? "set to true" : "not set"
);
}
}

View File

@ -5,23 +5,42 @@
*/
package org.elasticsearch.xpack.security.authc.esnative;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -30,6 +49,7 @@ import static org.mockito.Mockito.when;
public class NativeRealmMigratorTests extends ESTestCase {
private Consumer<ActionListener<Void>> ensureDisabledHandler;
private Map<String, NativeUsersStore.ReservedUserInfo> reservedUsers;
private NativeUsersStore nativeUsersStore;
private NativeRealmMigrator migrator;
private XPackLicenseState licenseState;
@ -44,7 +64,21 @@ public class NativeRealmMigratorTests extends ESTestCase {
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[2];
ensureDisabledHandler.accept(listener);
return null;
}).when(nativeUsersStore).ensureReservedUserIsDisabled(any(), eq(allowClearCache), any());
}).when(nativeUsersStore).ensureReservedUserIsDisabled(anyString(), eq(allowClearCache), any(ActionListener.class));
Mockito.doAnswer(invocation -> {
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[2];
listener.onResponse(null);
return null;
}).when(nativeUsersStore).changePassword(any(ChangePasswordRequest.class), anyBoolean(), any(ActionListener.class));
reservedUsers = Collections.emptyMap();
Mockito.doAnswer(invocation -> {
ActionListener<Map<String, NativeUsersStore.ReservedUserInfo>> listener =
(ActionListener<Map<String, NativeUsersStore.ReservedUserInfo>>) invocation.getArguments()[0];
listener.onResponse(reservedUsers);
return null;
}).when(nativeUsersStore).getAllReservedUserInfo(any(ActionListener.class));
final Settings settings = Settings.EMPTY;
@ -55,15 +89,28 @@ public class NativeRealmMigratorTests extends ESTestCase {
}
public void testNoChangeOnFreshInstall() throws Exception {
verifyNoOpUpgrade(null);
verifyUpgrade(null, false, false);
}
public void testNoChangeOnUpgradeOnOrAfterV5_2() throws Exception {
verifyNoOpUpgrade(randomFrom(Version.V_5_2_0_UNRELEASED, Version.V_6_0_0_alpha1_UNRELEASED));
public void testNoChangeOnUpgradeAfterV5_3() throws Exception {
verifyUpgrade(randomFrom(Version.V_6_0_0_alpha1_UNRELEASED), false, false);
}
public void testDisableLogstashOnUpgradeFromVersionPriorToV5_2() throws Exception {
verifyUpgradeDisablesLogstashSystemUser(randomFrom(Version.V_5_1_1_UNRELEASED, Version.V_5_0_2, Version.V_5_0_0));
public void testDisableLogstashAndConvertPasswordsOnUpgradeFromVersionPriorToV5_2() throws Exception {
this.reservedUsers = Collections.singletonMap(
KibanaUser.NAME,
new NativeUsersStore.ReservedUserInfo(Hasher.BCRYPT.hash(ReservedRealm.DEFAULT_PASSWORD_TEXT), false, false)
);
verifyUpgrade(randomFrom(Version.V_5_1_1_UNRELEASED, Version.V_5_0_2, Version.V_5_0_0), true, true);
}
public void testConvertPasswordsOnUpgradeFromVersion5_2() throws Exception {
this.reservedUsers = randomSubsetOf(randomIntBetween(0, 3), LogstashSystemUser.NAME, KibanaUser.NAME, ElasticUser.NAME)
.stream().collect(Collectors.toMap(Function.identity(),
name -> new NativeUsersStore.ReservedUserInfo(Hasher.BCRYPT.hash(ReservedRealm.DEFAULT_PASSWORD_TEXT),
randomBoolean(), false)
));
verifyUpgrade(Version.V_5_2_0_UNRELEASED, false, true);
}
public void testExceptionInUsersStoreIsPropagatedToListener() throws Exception {
@ -74,18 +121,31 @@ public class NativeRealmMigratorTests extends ESTestCase {
assertThat(caught.getCause(), is(thrown));
}
private void verifyNoOpUpgrade(Version fromVersion) throws ExecutionException, InterruptedException {
private void verifyUpgrade(Version fromVersion, boolean disableLogstashUser, boolean convertDefaultPasswords) throws Exception {
final PlainActionFuture<Boolean> future = doUpgrade(fromVersion);
boolean expectedResult = false;
if (disableLogstashUser) {
final boolean clearCache = licenseState.isAuthAllowed();
verify(nativeUsersStore).ensureReservedUserIsDisabled(eq(LogstashSystemUser.NAME), eq(clearCache), any());
expectedResult = true;
}
if (convertDefaultPasswords) {
verify(nativeUsersStore).getAllReservedUserInfo(any());
ArgumentCaptor<ChangePasswordRequest> captor = ArgumentCaptor.forClass(ChangePasswordRequest.class);
verify(nativeUsersStore, times(this.reservedUsers.size()))
.changePassword(captor.capture(), eq(true), any(ActionListener.class));
final List<ChangePasswordRequest> requests = captor.getAllValues();
this.reservedUsers.keySet().forEach(u -> {
ChangePasswordRequest request = requests.stream().filter(r -> r.username().equals(u)).findFirst().get();
assertThat(request.validate(), nullValue(ActionRequestValidationException.class));
assertThat(request.username(), equalTo(u));
assertThat(request.passwordHash().length, equalTo(0));
assertThat(request.getRefreshPolicy(), equalTo(WriteRequest.RefreshPolicy.IMMEDIATE));
});
expectedResult = true;
}
verifyNoMoreInteractions(nativeUsersStore);
assertThat(future.get(), is(Boolean.FALSE));
}
private void verifyUpgradeDisablesLogstashSystemUser(Version fromVersion) throws ExecutionException, InterruptedException {
final PlainActionFuture<Boolean> future = doUpgrade(fromVersion);
final boolean clearCache = licenseState.isAuthAllowed();
verify(nativeUsersStore).ensureReservedUserIsDisabled(eq(LogstashSystemUser.NAME), eq(clearCache), any());
verifyNoMoreInteractions(nativeUsersStore);
assertThat(future.get(), is(Boolean.TRUE));
assertThat(future.get(), is(expectedResult));
}
private PlainActionFuture<Boolean> doUpgrade(Version fromVersion) {
@ -93,4 +153,4 @@ public class NativeRealmMigratorTests extends ESTestCase {
migrator.performUpgrade(fromVersion, future);
return future;
}
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.authc.esnative;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityTemplateService;
import org.elasticsearch.xpack.security.test.SecurityTestUtils;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
import org.elasticsearch.xpack.security.user.User;
import org.junit.Before;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
public class NativeUsersStoreTests extends ESTestCase {
private static final String ENABLED_FIELD = User.Fields.ENABLED.getPreferredName();
private static final String PASSWORD_FIELD = User.Fields.PASSWORD.getPreferredName();
private static final String BLANK_PASSWORD = "";
private InternalClient internalClient;
private final List<Tuple<ActionRequest, ActionListener<? extends ActionResponse>>> requests = new CopyOnWriteArrayList<>();
@Before
public void setupMocks() {
internalClient = new InternalClient(Settings.EMPTY, null, null, null) {
@Override
protected <
Request extends ActionRequest,
Response extends ActionResponse,
RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>
> void doExecute(
Action<Request, Response, RequestBuilder> action,
Request request,
ActionListener<Response> listener) {
requests.add(new Tuple<>(request, listener));
}
};
}
public void testPasswordUpsertWhenSetEnabledOnReservedUser() throws Exception {
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME);
final PlainActionFuture<Void> future = new PlainActionFuture<>();
nativeUsersStore.setEnabled(user, true, WriteRequest.RefreshPolicy.IMMEDIATE, future);
final UpdateRequest update = actionRespond(UpdateRequest.class, null);
final Map<String, Object> docMap = update.doc().sourceAsMap();
assertThat(docMap.get(ENABLED_FIELD), equalTo(Boolean.TRUE));
assertThat(docMap.get(PASSWORD_FIELD), nullValue());
final Map<String, Object> upsertMap = update.upsertRequest().sourceAsMap();
assertThat(upsertMap.get(User.Fields.ENABLED.getPreferredName()), equalTo(Boolean.TRUE));
assertThat(upsertMap.get(User.Fields.PASSWORD.getPreferredName()), equalTo(BLANK_PASSWORD));
}
public void testBlankPasswordInIndexImpliesDefaultPassword() throws Exception {
final NativeUsersStore nativeUsersStore = startNativeUsersStore();
final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME);
final Map<String, Object> values = new HashMap<>();
values.put(ENABLED_FIELD, Boolean.TRUE);
values.put(PASSWORD_FIELD, BLANK_PASSWORD);
final GetResult result = new GetResult(
SecurityTemplateService.SECURITY_INDEX_NAME,
NativeUsersStore.RESERVED_USER_DOC_TYPE,
randomAsciiOfLength(12),
1L,
true,
jsonBuilder().map(values).bytes(),
Collections.emptyMap());
final PlainActionFuture<NativeUsersStore.ReservedUserInfo> future = new PlainActionFuture<>();
nativeUsersStore.getReservedUserInfo(user, future);
actionRespond(GetRequest.class, new GetResponse(result));
final NativeUsersStore.ReservedUserInfo userInfo = future.get();
assertThat(userInfo.hasDefaultPassword, equalTo(true));
assertThat(userInfo.enabled, equalTo(true));
assertThat(userInfo.passwordHash, equalTo(ReservedRealm.DEFAULT_PASSWORD_HASH));
}
private <ARequest extends ActionRequest, AResponse extends ActionResponse> ARequest actionRespond(Class<ARequest> requestClass,
AResponse response) {
Tuple<ARequest, ActionListener<?>> tuple = findRequest(requestClass);
((ActionListener<AResponse>) tuple.v2()).onResponse(response);
return tuple.v1();
}
private <ARequest extends ActionRequest> Tuple<ARequest, ActionListener<?>> findRequest(
Class<ARequest> requestClass) {
return this.requests.stream()
.filter(t -> requestClass.isInstance(t.v1()))
.map(t -> new Tuple<ARequest, ActionListener<?>>(requestClass.cast(t.v1()), t.v2()))
.findFirst().orElseThrow(() -> new RuntimeException("Cannot find request of type " + requestClass));
}
private NativeUsersStore startNativeUsersStore() {
final NativeUsersStore nativeUsersStore = new NativeUsersStore(Settings.EMPTY, internalClient);
assertTrue(nativeUsersStore + " should be ready to start",
nativeUsersStore.canStart(SecurityTestUtils.getClusterStateWithSecurityIndex(), true));
nativeUsersStore.start();
return nativeUsersStore;
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.esnative;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.action.user.ChangePasswordResponse;
@ -41,6 +42,25 @@ public class ReservedRealmIntegTests extends NativeRealmIntegTestCase {
}
}
/**
* Enabling a user forces a doc to be written to the security index, and "user doc with default password" has a special case code in
* the reserved realm.
*/
public void testAuthenticateAfterEnablingUser() {
final SecurityClient c = securityClient();
for (String username : Arrays.asList(ElasticUser.NAME, KibanaUser.NAME)) {
c.prepareSetEnabled(username, true).get();
ClusterHealthResponse response = client()
.filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, DEFAULT_PASSWORD)))
.admin()
.cluster()
.prepareHealth()
.get();
assertThat(response.getClusterName(), is(cluster().getClusterName()));
}
}
public void testChangingPassword() {
String username = randomFrom(ElasticUser.NAME, KibanaUser.NAME);
final char[] newPassword = "supersecretvalue".toCharArray();

View File

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.authc.esnative;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.NativeRealmIntegTestCase;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.elasticsearch.xpack.security.user.KibanaUser;
import static java.util.Collections.singletonMap;
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
/**
* Integration tests for the built in realm with default passwords disabled
*/
public class ReservedRealmNoDefaultPasswordIntegTests extends NativeRealmIntegTestCase {
private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray());
@Override
protected Settings nodeSettings(int nodeOrdinal) {
Settings.Builder builder = Settings.builder()
.put(super.nodeSettings(nodeOrdinal))
.put(ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey(), false);
return builder.build();
}
/**
* This ensures that if a user is explicitly enabled, thus creating an entry in the security index, but no password is ever set,
* then the user is treated as having a default password, and cannot login.
*/
public void testEnablingUserWithoutPasswordCannotLogin() throws Exception {
final SecurityClient c = securityClient();
c.prepareSetEnabled(KibanaUser.NAME, true).get();
ElasticsearchSecurityException elasticsearchSecurityException = expectThrows(ElasticsearchSecurityException.class, () -> client()
.filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(KibanaUser.NAME, DEFAULT_PASSWORD)))
.admin()
.cluster()
.prepareHealth()
.get());
assertThat(elasticsearchSecurityException.getMessage(), containsString("authenticate"));
final SecuredString newPassword = new SecuredString("not-the-default-password".toCharArray());
c.prepareChangePassword(KibanaUser.NAME, newPassword.copyChars()).get();
ClusterHealthResponse response = client()
.filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(KibanaUser.NAME, newPassword)))
.admin()
.cluster()
.prepareHealth()
.get();
assertThat(response.getClusterName(), is(cluster().getClusterName()));
}
}

View File

@ -32,6 +32,7 @@ import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
@ -55,6 +56,7 @@ import static org.mockito.Mockito.when;
public class ReservedRealmTests extends ESTestCase {
private static final SecuredString DEFAULT_PASSWORD = new SecuredString("changeme".toCharArray());
public static final String ACCEPT_DEFAULT_PASSWORDS = ReservedRealm.ACCEPT_DEFAULT_PASSWORD_SETTING.getKey();
private NativeUsersStore usersStore;
@Before
@ -90,7 +92,7 @@ public class ReservedRealmTests extends ESTestCase {
assertThat(future.get().enabled(), equalTo(false));
}
public void testDefaultPasswordAuthentication() throws Throwable {
public void testSuccessfulDefaultPasswordAuthentication() throws Throwable {
final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true));
final String principal = expected.principal();
final boolean securityIndexExists = randomBoolean();
@ -105,7 +107,6 @@ public class ReservedRealmTests extends ESTestCase {
final ReservedRealm reservedRealm =
new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY));
PlainActionFuture<User> listener = new PlainActionFuture<>();
reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, DEFAULT_PASSWORD), listener);
final User authenticated = listener.actionGet();
@ -121,6 +122,29 @@ public class ReservedRealmTests extends ESTestCase {
verifyNoMoreInteractions(usersStore);
}
public void testDisableDefaultPasswordAuthentication() throws Throwable {
final User expected = randomFrom(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true));
final Environment environment = mock(Environment.class);
final AnonymousUser anonymousUser = new AnonymousUser(Settings.EMPTY);
final Settings settings = Settings.builder().put(ACCEPT_DEFAULT_PASSWORDS, false).build();
final ReservedRealm reservedRealm = new ReservedRealm(environment, settings, usersStore, anonymousUser);
final ActionListener<User> listener = new ActionListener<User>() {
@Override
public void onResponse(User user) {
fail("Authentication should have failed because default-password is not allowed");
}
@Override
public void onFailure(Exception e) {
assertThat(e, instanceOf(ElasticsearchSecurityException.class));
assertThat(e.getMessage(), containsString("failed to authenticate"));
}
};
reservedRealm.doAuthenticate(new UsernamePasswordToken(expected.principal(), DEFAULT_PASSWORD), listener);
}
public void testAuthenticationDisabled() throws Throwable {
Settings settings = Settings.builder().put(XPackSettings.RESERVED_REALM_ENABLED_SETTING.getKey(), false).build();
final boolean securityIndexExists = randomBoolean();
@ -147,15 +171,15 @@ public class ReservedRealmTests extends ESTestCase {
}
private void verifySuccessfulAuthentication(boolean enabled) {
final ReservedRealm reservedRealm =
new ReservedRealm(mock(Environment.class), Settings.EMPTY, usersStore, new AnonymousUser(Settings.EMPTY));
final Settings settings = Settings.builder().put(ACCEPT_DEFAULT_PASSWORDS, randomBoolean()).build();
final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, new AnonymousUser(settings));
final User expectedUser = randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new LogstashSystemUser(enabled));
final String principal = expectedUser.principal();
final SecuredString newPassword = new SecuredString("foobar".toCharArray());
when(usersStore.securityIndexExists()).thenReturn(true);
doAnswer((i) -> {
ActionListener callback = (ActionListener) i.getArguments()[1];
callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), enabled));
callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), enabled, false));
return null;
}).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class));
@ -168,7 +192,7 @@ public class ReservedRealmTests extends ESTestCase {
// the realm assumes it owns the hashed password so it fills it with 0's
doAnswer((i) -> {
ActionListener callback = (ActionListener) i.getArguments()[1];
callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true));
callback.onResponse(new ReservedUserInfo(Hasher.BCRYPT.hash(newPassword), true, false));
return null;
}).when(usersStore).getReservedUserInfo(eq(principal), any(ActionListener.class));

View File

@ -10,12 +10,14 @@ import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.cluster.metadata.IndexTemplateMetaData;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
import org.elasticsearch.cluster.routing.RoutingTable;
import org.elasticsearch.cluster.routing.ShardRouting;
import org.elasticsearch.cluster.routing.UnassignedInfo;
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.settings.Settings;
@ -30,6 +32,7 @@ import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.UUID;
import static org.elasticsearch.cluster.routing.RecoverySource.StoreRecoverySource.EXISTING_STORE_INSTANCE;
@ -76,7 +79,11 @@ public class SecurityTestUtils {
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
.build();
MetaData metaData = MetaData.builder()
.put(IndexMetaData.builder(SecurityTemplateService.SECURITY_INDEX_NAME).settings(settings)).build();
.put(IndexMetaData.builder(SecurityTemplateService.SECURITY_INDEX_NAME).settings(settings))
.put(new IndexTemplateMetaData(SecurityTemplateService.SECURITY_TEMPLATE_NAME, 0, 0,
Collections.singletonList(SecurityTemplateService.SECURITY_INDEX_NAME), Settings.EMPTY, ImmutableOpenMap.of(),
ImmutableOpenMap.of(), ImmutableOpenMap.of()))
.build();
RoutingTable routingTable = buildSecurityIndexRoutingTable();
return ClusterState.builder(new ClusterName(NativeRolesStoreTests.class.getName()))

View File

@ -29,3 +29,14 @@
- do:
xpack.security.clear_cached_realms:
realms: "_all"
---
"verify users for default password migration in mixed cluster":
- do:
xpack.security.get_user:
username: "kibana,logstash_system"
- match: { kibana.enabled: false }
- match: { logstash_system.enabled: true }

View File

@ -57,3 +57,34 @@
index: ".security"
wait_for_active_shards: 2 # 1 primary and 1 replica since we have two nodes
timeout: 25s
---
"default password migration":
# Check that enabling a user in old cluster will not prevent the user from having a "default password" in the new cluster.
# See: org.elasticsearch.xpack.security.authc.esnative.NativeRealmMigrator.doConvertDefaultPasswords
- do:
xpack.security.disable_user:
username: "kibana"
- do:
xpack.security.get_user:
username: "kibana"
- match: { kibana.enabled: false }
- do:
xpack.security.change_password:
username: "logstash_system"
body: >
{
"password" : "changed-it"
}
- do:
xpack.security.enable_user:
username: "logstash_system"
- do:
xpack.security.get_user:
username: "logstash_system"
- match: { logstash_system.enabled: true }

View File

@ -21,3 +21,30 @@
- match: { native_role.cluster.0: "all" }
- match: { native_role.indices.0.names.0: "test_index" }
- match: { native_role.indices.0.privileges.0: "all" }
---
"Verify default password migration results in upgraded cluster":
- do:
headers:
Authorization: "Basic bmF0aXZlX3VzZXI6Y2hhbmdlbWU="
cluster.health:
wait_for_status: green
wait_for_nodes: 2
timeout: 25s
- match: { timed_out: false }
- do:
get:
index: ".security"
type: "reserved-user"
id: "kibana"
- match: { _source.password: "" }
- match: { _source.enabled: false }
- do:
get:
index: ".security"
type: "reserved-user"
id: "logstash_system"
- match: { _source.password: "/^\\$2a\\$10\\$[a-zA-Z0-9/.]{53}$/" }
- match: { _source.enabled: true }