From f275a3f07ba0cba170f17b97ffce13854242c1b3 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Wed, 28 Jun 2017 12:48:49 -0500 Subject: [PATCH] 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@78f53fd2322d88151465e1ee9168ba70a9ca6076 --- .../xpack/security/Security.java | 11 +-- .../security/authc/ContainerSettings.java | 65 +++++++++++++++++ .../authc/esnative/NativeUsersStore.java | 13 +++- .../ContainerPasswordBootstrapCheck.java | 41 +++++++++++ .../DefaultPasswordBootstrapCheck.java | 37 ---------- .../authc/esnative/NativeUsersStoreTests.java | 69 ++++++++++++++++++- .../ContainerPasswordBootstrapCheckTests.java | 36 ++++++++++ 7 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/authc/ContainerSettings.java create mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheck.java delete mode 100644 plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java create mode 100644 plugin/src/test/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheckTests.java diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java index d79758c6bbd..8b91d61aa68 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -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 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(); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ContainerSettings.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ContainerSettings.java new file mode 100644 index 00000000000..a9dfe2c9aed --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ContainerSettings.java @@ -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); + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index da50010d203..3d9ec2022e7 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -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 { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheck.java b/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheck.java new file mode 100644 index 00000000000..4661a4932ea --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheck.java @@ -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."; + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java b/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java deleted file mode 100644 index 9b1a2770d36..00000000000 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/bootstrap/DefaultPasswordBootstrapCheck.java +++ /dev/null @@ -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 true. - */ - @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" - ); - } -} diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java index 46bb8f01e5a..8c26642e045 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java @@ -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 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 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 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 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 actionRespond(Class requestClass, AResponse response) { Tuple> 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); } } \ No newline at end of file diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheckTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheckTests.java new file mode 100644 index 00000000000..68ab16089fe --- /dev/null +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/bootstrap/ContainerPasswordBootstrapCheckTests.java @@ -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()); + } +}