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:
parent
2f1693c0fd
commit
f275a3f07b
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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.";
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue