Support bootstrap password when in container (elastic/x-pack-elasticsearch#1832)

This is related to elastic/x-pack-elasticsearch#1217. This commit reads two environment variables on
startup: BOOTSTRAP_PWD and ELASTIC_CONTAINER. If BOOTSTRAP_PWD is
present, ELASTIC_CONTAINER must be set to true. Otherwise a new
bootstrap check will fail.

If ELASTIC_CONTAINER is set to true, the elastic user can be
authenticated with the BOOTSTRAP_PWD variable when its password
has not been explicitly set.

Original commit: elastic/x-pack-elasticsearch@78f53fd232
This commit is contained in:
Tim Brooks 2017-06-28 12:48:49 -05:00 committed by GitHub
parent 2f1693c0fd
commit f275a3f07b
7 changed files with 228 additions and 44 deletions

View File

@ -104,6 +104,7 @@ import org.elasticsearch.xpack.security.audit.index.IndexNameResolver;
import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail;
import org.elasticsearch.xpack.security.authc.AuthenticationFailureHandler;
import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.authc.ContainerSettings;
import org.elasticsearch.xpack.security.authc.DefaultAuthenticationFailureHandler;
import org.elasticsearch.xpack.security.authc.InternalRealms;
import org.elasticsearch.xpack.security.authc.Realm;
@ -127,7 +128,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.bootstrap.ContainerPasswordBootstrapCheck;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
@ -321,8 +322,10 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
final TokenService tokenService = new TokenService(settings, Clock.systemUTC(), client, securityLifecycleService);
components.add(tokenService);
final ContainerSettings containerSettings = ContainerSettings.parseAndCreate();
// realms construction
final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityLifecycleService);
final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityLifecycleService, containerSettings);
final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityLifecycleService);
final AnonymousUser anonymousUser = new AnonymousUser(settings);
final ReservedRealm reservedRealm = new ReservedRealm(env, settings, nativeUsersStore,
@ -499,11 +502,11 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
public List<BootstrapCheck> getBootstrapChecks() {
if (enabled) {
return Arrays.asList(
new DefaultPasswordBootstrapCheck(settings),
new SSLBootstrapCheck(sslService, settings, env),
new TokenPassphraseBootstrapCheck(settings),
new TokenSSLBootstrapCheck(settings),
new PkiRealmBootstrapCheck(settings, sslService)
new PkiRealmBootstrapCheck(settings, sslService),
new ContainerPasswordBootstrapCheck()
);
} else {
return Collections.emptyList();

View File

@ -0,0 +1,65 @@
/*
* 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;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.xpack.security.authc.support.Hasher;
/**
* Parses and stores environment settings relevant to running Elasticsearch in a container.
*/
public final class ContainerSettings {
public static final String BOOTSTRAP_PASSWORD_ENV_VAR = "BOOTSTRAP_PWD";
public static final String CONTAINER_ENV_VAR = "ELASTIC_CONTAINER";
private final boolean inContainer;
private final char[] passwordHash;
public ContainerSettings(boolean inContainer, char[] passwordHash) {
this.inContainer = inContainer;
this.passwordHash = passwordHash;
}
/**
* Returns a boolean indicating if Elasticsearch is deployed in a container (such as Docker). The way
* we determine if Elasticsearch is deployed in a container is by reading the ELASTIC_CONTAINER env
* variable. This should be set to true if Elasticsearch is in a container.
*
* @return if elasticsearch is running in a container
*/
public boolean inContainer() {
return inContainer;
}
/**
* Returns the hash for the bootstrap password. This is the password passed as an environmental variable
* for use when elasticsearch is deployed in a container.
*
* @return the password hash
*/
public char[] getPasswordHash() {
return passwordHash;
}
public static ContainerSettings parseAndCreate() {
String inContainerString = System.getenv(CONTAINER_ENV_VAR);
boolean inContainer = inContainerString != null && Booleans.parseBoolean(inContainerString);
char[] passwordHash;
String passwordString = System.getenv(BOOTSTRAP_PASSWORD_ENV_VAR);
if (passwordString != null) {
SecureString password = new SecureString(passwordString.toCharArray());
passwordHash = Hasher.BCRYPT.hash(password);
password.close();
} else {
passwordHash = null;
}
return new ContainerSettings(inContainer, passwordHash);
}
}

View File

@ -42,8 +42,10 @@ import org.elasticsearch.xpack.security.action.realm.ClearRealmCacheResponse;
import org.elasticsearch.xpack.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.security.action.user.DeleteUserRequest;
import org.elasticsearch.xpack.security.action.user.PutUserRequest;
import org.elasticsearch.xpack.security.authc.ContainerSettings;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.SystemUser;
import org.elasticsearch.xpack.security.user.User;
import org.elasticsearch.xpack.security.user.User.Fields;
@ -75,12 +77,15 @@ public class NativeUsersStore extends AbstractComponent {
private final boolean isTribeNode;
private volatile SecurityLifecycleService securityLifecycleService;
private final ContainerSettings containerSettings;
public NativeUsersStore(Settings settings, InternalClient client, SecurityLifecycleService securityLifecycleService) {
public NativeUsersStore(Settings settings, InternalClient client, SecurityLifecycleService securityLifecycleService,
ContainerSettings containerSettings) {
super(settings);
this.client = client;
this.isTribeNode = XPackPlugin.isTribeNode(settings);
this.securityLifecycleService = securityLifecycleService;
this.containerSettings = containerSettings;
}
/**
@ -540,6 +545,8 @@ public class NativeUsersStore extends AbstractComponent {
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() && containerSettings.inContainer() && username.equals(ElasticUser.NAME)) {
listener.onResponse(new ReservedUserInfo(containerSettings.getPasswordHash(), enabled, false));
} else if (password.isEmpty()) {
listener.onResponse(new ReservedUserInfo(ReservedRealm.DEFAULT_PASSWORD_HASH, enabled, true));
} else {
@ -595,6 +602,10 @@ public class NativeUsersStore extends AbstractComponent {
} else if (enabled == null) {
listener.onFailure(new IllegalStateException("enabled must not be null!"));
return;
} else if (password.isEmpty() && containerSettings.inContainer() &&
ElasticUser.NAME.equals(searchHit.getId())) {
char[] passwordHash = containerSettings.getPasswordHash();
userInfos.put(searchHit.getId(), new ReservedUserInfo(passwordHash, enabled, false));
} else if (password.isEmpty()) {
userInfos.put(username, new ReservedUserInfo(ReservedRealm.DEFAULT_PASSWORD_HASH, enabled, true));
} else {

View File

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.bootstrap;
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.xpack.security.authc.ContainerSettings;
/**
* A bootstrap check validating container environment variables. The bootstrap password option
* cannot be present if the container environment variable is not set to true.
*/
public final class ContainerPasswordBootstrapCheck implements BootstrapCheck {
private final ContainerSettings containerSettings;
public ContainerPasswordBootstrapCheck() {
this(ContainerSettings.parseAndCreate());
}
public ContainerPasswordBootstrapCheck(ContainerSettings containerSettings) {
this.containerSettings = containerSettings;
}
@Override
public boolean check() {
if (containerSettings.getPasswordHash() != null && containerSettings.inContainer() == false) {
return true;
}
return false;
}
@Override
public String errorMessage() {
return "Cannot use bootstrap password env variable [" + ContainerSettings.BOOTSTRAP_PASSWORD_ENV_VAR + "] if " +
"Elasticsearch is not being deployed in a container.";
}
}

View File

@ -1,37 +0,0 @@
/*
* 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

@ -27,6 +27,8 @@ import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.InternalClient;
import org.elasticsearch.xpack.security.SecurityLifecycleService;
import org.elasticsearch.xpack.security.authc.ContainerSettings;
import org.elasticsearch.xpack.security.user.BeatsSystemUser;
import org.elasticsearch.xpack.security.user.ElasticUser;
import org.elasticsearch.xpack.security.user.KibanaUser;
import org.elasticsearch.xpack.security.user.LogstashSystemUser;
@ -114,6 +116,66 @@ public class NativeUsersStoreTests extends ESTestCase {
assertThat(userInfo.passwordHash, equalTo(ReservedRealm.DEFAULT_PASSWORD_HASH));
}
public void testInContainerTrueReturnsEmptyPasswordForNonElasticReservedUsers() throws Exception {
char[] passwordHash = randomAlphaOfLength(10).toCharArray();
final NativeUsersStore nativeUsersStore = startNativeUsersStore(new ContainerSettings(true, passwordHash));
final String user = randomFrom(BeatsSystemUser.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(
SecurityLifecycleService.SECURITY_INDEX_NAME,
NativeUsersStore.INDEX_TYPE,
randomAlphaOfLength(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));
}
public void testInContainerTrueReturnsBootstrapPasswordForElastic() throws Exception {
char[] passwordHash = randomAlphaOfLength(10).toCharArray();
final NativeUsersStore nativeUsersStore = startNativeUsersStore(new ContainerSettings(true, passwordHash));
final Map<String, Object> values = new HashMap<>();
values.put(ENABLED_FIELD, Boolean.TRUE);
values.put(PASSWORD_FIELD, BLANK_PASSWORD);
final GetResult result = new GetResult(
SecurityLifecycleService.SECURITY_INDEX_NAME,
NativeUsersStore.INDEX_TYPE,
randomAlphaOfLength(12),
1L,
true,
jsonBuilder().map(values).bytes(),
Collections.emptyMap());
final PlainActionFuture<NativeUsersStore.ReservedUserInfo> future = new PlainActionFuture<>();
nativeUsersStore.getReservedUserInfo(ElasticUser.NAME, future);
actionRespond(GetRequest.class, new GetResponse(result));
final NativeUsersStore.ReservedUserInfo userInfo = future.get();
assertThat(userInfo.hasDefaultPassword, equalTo(false));
assertThat(userInfo.enabled, equalTo(true));
assertThat(userInfo.passwordHash, equalTo(passwordHash));
}
private <ARequest extends ActionRequest, AResponse extends ActionResponse> ARequest actionRespond(Class<ARequest> requestClass,
AResponse response) {
Tuple<ARequest, ActionListener<?>> tuple = findRequest(requestClass);
@ -130,6 +192,10 @@ public class NativeUsersStoreTests extends ESTestCase {
}
private NativeUsersStore startNativeUsersStore() {
return startNativeUsersStore(new ContainerSettings(false, null));
}
private NativeUsersStore startNativeUsersStore(ContainerSettings containerSettings) {
SecurityLifecycleService securityLifecycleService = mock(SecurityLifecycleService.class);
when(securityLifecycleService.isSecurityIndexAvailable()).thenReturn(true);
when(securityLifecycleService.isSecurityIndexExisting()).thenReturn(true);
@ -143,8 +209,7 @@ public class NativeUsersStoreTests extends ESTestCase {
listener.onResponse(null);
return null;
}).when(securityLifecycleService).createIndexIfNeededThenExecute(any(ActionListener.class), any(Runnable.class));
final NativeUsersStore nativeUsersStore = new NativeUsersStore(Settings.EMPTY, internalClient, securityLifecycleService);
return nativeUsersStore;
return new NativeUsersStore(Settings.EMPTY, internalClient, securityLifecycleService, containerSettings);
}
}

View File

@ -0,0 +1,36 @@
/*
* 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 org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.security.authc.ContainerSettings;
import org.junit.Before;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ContainerPasswordBootstrapCheckTests extends ESTestCase {
private ContainerPasswordBootstrapCheck bootstrapCheck;
public void testCheckPassesIfNoPassword() {
bootstrapCheck = new ContainerPasswordBootstrapCheck(new ContainerSettings(false, null));
assertFalse(bootstrapCheck.check());
}
public void testCheckPassesIfPasswordAndInContainer() {
bootstrapCheck = new ContainerPasswordBootstrapCheck(new ContainerSettings(true, "password".toCharArray()));
assertFalse(bootstrapCheck.check());
}
public void testCheckFailsIfPasswordAndNotContainer() {
bootstrapCheck = new ContainerPasswordBootstrapCheck(new ContainerSettings(false, "password".toCharArray()));
assertTrue(bootstrapCheck.check());
}
}