From f60e490a2818074138c857539cc417eb347c7b79 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 13 Jan 2022 20:51:13 +0200 Subject: [PATCH 1/4] Done --- .../InitialNodeSecurityAutoConfiguration.java | 169 ++++++++++-------- .../xpack/security/Security.java | 13 +- 2 files changed, 110 insertions(+), 72 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 903ab07aa13b4..3947bdf1b6e7c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -15,8 +15,10 @@ import org.elasticsearch.bootstrap.BootstrapInfo; import org.elasticsearch.client.internal.Client; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.env.Environment; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.enrollment.BaseEnrollmentTokenGenerator; @@ -27,6 +29,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.CountDownLatch; import static org.elasticsearch.xpack.core.XPackSettings.ENROLLMENT_ENABLED; import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH; @@ -51,7 +54,9 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart SecurityIndexManager securityIndexManager, SSLService sslService, Client client, - Environment environment + Environment environment, + CountDownLatch nodeStartedSignal, + ThreadPool threadPool ) { // Assume the following auto-configuration must NOT run if enrollment is disabled when the node starts, // so no credentials or HTTPS CA fingerprint will be displayed in this case (in addition to no enrollment @@ -88,81 +93,103 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart // is now a system index), it's not a catastrophic position to be in either, because it only entails // that new tokens and possibly credentials are generated anew // TODO maybe we can improve the check that this is indeed the initial node - String fingerprint; - try { - fingerprint = enrollmentTokenGenerator.getHttpsCaFingerprint(); - LOGGER.info( - "HTTPS has been configured with automatically generated certificates, " - + "and the CA's hex-encoded SHA-256 fingerprint is [" - + fingerprint - + "]" - ); - } catch (Exception e) { - fingerprint = null; - LOGGER.error("Failed to compute the HTTPS CA fingerprint, probably the certs are not auto-generated", e); - } - final String httpsCaFingerprint = fingerprint; - GroupedActionListener> groupedActionListener = new GroupedActionListener<>( - ActionListener.wrap(results -> { - final Map allResultsMap = new HashMap<>(); - for (Map result : results) { - allResultsMap.putAll(result); - } - final String elasticPassword = allResultsMap.get("generated_elastic_user_password"); - final String kibanaEnrollmentToken = allResultsMap.get("kibana_enrollment_token"); - final String nodeEnrollmentToken = allResultsMap.get("node_enrollment_token"); - outputInformationToConsole(elasticPassword, kibanaEnrollmentToken, nodeEnrollmentToken, httpsCaFingerprint, out); - }, e -> { LOGGER.error("Unexpected exception during security auto-configuration", e); }), - 3 - ); - // we only generate the elastic user password if the node has been auto-configured in a specific way, such that the first - // time a node starts it will form a cluster by itself and can hold the .security index (which we assume it is when - // {@code ENROLLMENT_ENABLED} is true), that the node process's output is a terminal and that the password is not - // specified already via the two secure settings - if (false == BOOTSTRAP_ELASTIC_PASSWORD.exists(environment.settings()) - && false == AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(environment.settings())) { - final char[] elasticPassword = generatePassword(20); - nativeUsersStore.createElasticUser(elasticPassword, ActionListener.wrap(aVoid -> { - LOGGER.debug("elastic credentials generated successfully"); - groupedActionListener.onResponse(Map.of("generated_elastic_user_password", new String(elasticPassword))); - }, e -> { - LOGGER.error("Failed to generate credentials for the elastic built-in superuser", e); - // null password in case of error - groupedActionListener.onResponse(Map.of()); - })); - } else { - if (false == BOOTSTRAP_ELASTIC_PASSWORD.exists(environment.settings())) { - LOGGER.info( - "Auto-configuration will not generate a password for the elastic built-in superuser, " - + "you should use the password specified in the node's secure setting [" - + BOOTSTRAP_ELASTIC_PASSWORD.getKey() - + "] in order to authenticate as elastic" - ); + threadPool.generic().execute(new AbstractRunnable() { + + @Override + public void onFailure(Exception e) { + LOGGER.error("Unexpected exception when auto configuring the initial node for Security", e); } - // empty password in case password generation is skipped - groupedActionListener.onResponse(Map.of("generated_elastic_user_password", "")); - } - final Iterator backoff = BACKOFF_POLICY.iterator(); - enrollmentTokenGenerator.createKibanaEnrollmentToken(kibanaToken -> { - if (kibanaToken != null) { + + @Override + protected void doRun() throws Exception { + // the HTTP address is guaranteed to be bound only after the node started + nodeStartedSignal.await(); + // a lot of stuff runs when a node just started, and the autoconfiguration is not time-critical + // and nothing else depends on it; be a good sport and wait a couple + Thread.sleep(9000); + String fingerprint; try { - LOGGER.debug("Successfully generated the kibana enrollment token"); - groupedActionListener.onResponse(Map.of("kibana_enrollment_token", kibanaToken.getEncoded())); + fingerprint = enrollmentTokenGenerator.getHttpsCaFingerprint(); + LOGGER.info( + "HTTPS has been configured with automatically generated certificates, " + + "and the CA's hex-encoded SHA-256 fingerprint is [" + + fingerprint + + "]" + ); } catch (Exception e) { - LOGGER.error("Failed to encode kibana enrollment token", e); - groupedActionListener.onResponse(Map.of()); + fingerprint = null; + LOGGER.error("Failed to compute the HTTPS CA fingerprint, probably the certs are not auto-generated", e); } - } else { - groupedActionListener.onResponse(Map.of()); - } - }, backoff); - enrollmentTokenGenerator.maybeCreateNodeEnrollmentToken(encodedNodeToken -> { - if (encodedNodeToken != null) { - groupedActionListener.onResponse(Map.of("node_enrollment_token", encodedNodeToken)); - } else { - groupedActionListener.onResponse(Map.of()); + final String httpsCaFingerprint = fingerprint; + GroupedActionListener> groupedActionListener = new GroupedActionListener<>( + ActionListener.wrap(results -> { + final Map allResultsMap = new HashMap<>(); + for (Map result : results) { + allResultsMap.putAll(result); + } + final String elasticPassword = allResultsMap.get("generated_elastic_user_password"); + final String kibanaEnrollmentToken = allResultsMap.get("kibana_enrollment_token"); + final String nodeEnrollmentToken = allResultsMap.get("node_enrollment_token"); + outputInformationToConsole( + elasticPassword, + kibanaEnrollmentToken, + nodeEnrollmentToken, + httpsCaFingerprint, + out + ); + }, e -> { LOGGER.error("Unexpected exception during security auto-configuration", e); }), + 3 + ); + // we only generate the elastic user password if the node has been auto-configured in a specific way, such that the + // first time a node starts it will form a cluster by itself and can hold the .security index (which we assume + // it is when {@code ENROLLMENT_ENABLED} is true), that the node process's output is a terminal and that the + // password is not specified already via the two secure settings + if (false == BOOTSTRAP_ELASTIC_PASSWORD.exists(environment.settings()) + && false == AUTOCONFIG_ELASTIC_PASSWORD_HASH.exists(environment.settings())) { + final char[] elasticPassword = generatePassword(20); + nativeUsersStore.createElasticUser(elasticPassword, ActionListener.wrap(aVoid -> { + LOGGER.debug("elastic credentials generated successfully"); + groupedActionListener.onResponse(Map.of("generated_elastic_user_password", new String(elasticPassword))); + }, e -> { + LOGGER.error("Failed to generate credentials for the elastic built-in superuser", e); + // null password in case of error + groupedActionListener.onResponse(Map.of()); + })); + } else { + if (false == BOOTSTRAP_ELASTIC_PASSWORD.exists(environment.settings())) { + LOGGER.info( + "Auto-configuration will not generate a password for the elastic built-in superuser, " + + "you should use the password specified in the node's secure setting [" + + BOOTSTRAP_ELASTIC_PASSWORD.getKey() + + "] in order to authenticate as elastic" + ); + } + // empty password in case password generation is skipped + groupedActionListener.onResponse(Map.of("generated_elastic_user_password", "")); + } + final Iterator backoff = BACKOFF_POLICY.iterator(); + enrollmentTokenGenerator.createKibanaEnrollmentToken(kibanaToken -> { + if (kibanaToken != null) { + try { + LOGGER.debug("Successfully generated the kibana enrollment token"); + groupedActionListener.onResponse(Map.of("kibana_enrollment_token", kibanaToken.getEncoded())); + } catch (Exception e) { + LOGGER.error("Failed to encode kibana enrollment token", e); + groupedActionListener.onResponse(Map.of()); + } + } else { + groupedActionListener.onResponse(Map.of()); + } + }, backoff); + enrollmentTokenGenerator.maybeCreateNodeEnrollmentToken(encodedNodeToken -> { + if (encodedNodeToken != null) { + groupedActionListener.onResponse(Map.of("node_enrollment_token", encodedNodeToken)); + } else { + groupedActionListener.onResponse(Map.of()); + } + }, backoff); } - }, backoff); + }); } }); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index fbdcf2d10e8c4..202c24b6a8925 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -325,6 +325,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -441,6 +442,8 @@ public class Security extends Plugin private final boolean enabled; private final SecuritySystemIndices systemIndices; + private final CountDownLatch nodeStartedSignal; + /* what a PITA that we need an extra indirection to initialize this. Yet, once we got rid of guice we can thing about how * to fix this or make it simpler. Today we need several service that are created in createComponents but we need to register * an instance of TransportInterceptor way earlier before createComponents is called. */ @@ -472,6 +475,7 @@ public Security(Settings settings, final Path configPath) { // TODO this is wrong, we should only use the environment that is provided to createComponents this.enabled = XPackSettings.SECURITY_ENABLED.get(settings); this.systemIndices = new SecuritySystemIndices(); + this.nodeStartedSignal = new CountDownLatch(1); if (enabled) { runStartupChecks(settings); Automatons.updateConfiguration(settings); @@ -762,7 +766,9 @@ Collection createComponents( systemIndices.getMainIndexManager(), getSslService(), client, - environment + environment, + nodeStartedSignal, + threadPool ); // to keep things simple, just invalidate all cached entries on license change. this happens so rarely that the impact should be @@ -1282,6 +1288,11 @@ public Map getProcessors(Processor.Parameters paramet ); } + @Override + public void onNodeStarted() { + this.nodeStartedSignal.countDown(); + } + /** * Realm settings were changed in 7.0. This method validates that the settings in use on this node match the new style of setting. * In 6.x a realm config would be From e0a33f329640fa63da3b7031af94ca03e2fa8122 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 14 Jan 2022 12:26:56 +0200 Subject: [PATCH 2/4] Schedule --- .../xpack/security/InitialNodeSecurityAutoConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 3947bdf1b6e7c..807e411809f5b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -93,7 +93,7 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart // is now a system index), it's not a catastrophic position to be in either, because it only entails // that new tokens and possibly credentials are generated anew // TODO maybe we can improve the check that this is indeed the initial node - threadPool.generic().execute(new AbstractRunnable() { + threadPool.schedule(new AbstractRunnable() { @Override public void onFailure(Exception e) { @@ -189,7 +189,7 @@ protected void doRun() throws Exception { } }, backoff); } - }); + }, TimeValue.timeValueSeconds(9), ThreadPool.Names.GENERIC); } }); } From 459d6a912c487fcdca51c6ceab0402f7e65f356f Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 14 Jan 2022 16:59:36 +0200 Subject: [PATCH 3/4] Meh --- .../xpack/security/InitialNodeSecurityAutoConfiguration.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 807e411809f5b..2971e23984b84 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -93,6 +93,8 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart // is now a system index), it's not a catastrophic position to be in either, because it only entails // that new tokens and possibly credentials are generated anew // TODO maybe we can improve the check that this is indeed the initial node + // a lot of stuff runs when a node just started, and the autoconfiguration is not time-critical + // and nothing else depends on it; be a good sport and wait a couple threadPool.schedule(new AbstractRunnable() { @Override @@ -104,9 +106,6 @@ public void onFailure(Exception e) { protected void doRun() throws Exception { // the HTTP address is guaranteed to be bound only after the node started nodeStartedSignal.await(); - // a lot of stuff runs when a node just started, and the autoconfiguration is not time-critical - // and nothing else depends on it; be a good sport and wait a couple - Thread.sleep(9000); String fingerprint; try { fingerprint = enrollmentTokenGenerator.getHttpsCaFingerprint(); From 49028443637be3ae1c1f22960304c8a71f381979 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 14 Jan 2022 17:20:08 +0200 Subject: [PATCH 4/4] Listener --- .../InitialNodeSecurityAutoConfiguration.java | 16 +++++++++------- .../elasticsearch/xpack/security/Security.java | 11 +++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 2971e23984b84..5a2284ed21830 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -29,7 +29,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import java.util.concurrent.CountDownLatch; import static org.elasticsearch.xpack.core.XPackSettings.ENROLLMENT_ENABLED; import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH; @@ -55,7 +54,7 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart SSLService sslService, Client client, Environment environment, - CountDownLatch nodeStartedSignal, + OnNodeStartedListener onNodeStartedListener, ThreadPool threadPool ) { // Assume the following auto-configuration must NOT run if enrollment is disabled when the node starts, @@ -95,7 +94,7 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart // TODO maybe we can improve the check that this is indeed the initial node // a lot of stuff runs when a node just started, and the autoconfiguration is not time-critical // and nothing else depends on it; be a good sport and wait a couple - threadPool.schedule(new AbstractRunnable() { + onNodeStartedListener.run(() -> threadPool.schedule(new AbstractRunnable() { @Override public void onFailure(Exception e) { @@ -103,9 +102,8 @@ public void onFailure(Exception e) { } @Override - protected void doRun() throws Exception { + protected void doRun() { // the HTTP address is guaranteed to be bound only after the node started - nodeStartedSignal.await(); String fingerprint; try { fingerprint = enrollmentTokenGenerator.getHttpsCaFingerprint(); @@ -136,7 +134,7 @@ protected void doRun() throws Exception { httpsCaFingerprint, out ); - }, e -> { LOGGER.error("Unexpected exception during security auto-configuration", e); }), + }, e -> LOGGER.error("Unexpected exception during security auto-configuration", e)), 3 ); // we only generate the elastic user password if the node has been auto-configured in a specific way, such that the @@ -188,7 +186,7 @@ protected void doRun() throws Exception { } }, backoff); } - }, TimeValue.timeValueSeconds(9), ThreadPool.Names.GENERIC); + }, TimeValue.timeValueSeconds(9), ThreadPool.Names.GENERIC)); } }); } @@ -279,4 +277,8 @@ private static void outputInformationToConsole( builder.append(System.lineSeparator()); out.println(builder); } + + interface OnNodeStartedListener { + void run(Runnable runnable); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 202c24b6a8925..d565b08a6d6a6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -40,6 +40,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Nullable; @@ -325,7 +326,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -441,8 +441,7 @@ public class Security extends Plugin private final Settings settings; private final boolean enabled; private final SecuritySystemIndices systemIndices; - - private final CountDownLatch nodeStartedSignal; + private final ListenableFuture nodeStartedListenable; /* what a PITA that we need an extra indirection to initialize this. Yet, once we got rid of guice we can thing about how * to fix this or make it simpler. Today we need several service that are created in createComponents but we need to register @@ -475,7 +474,7 @@ public Security(Settings settings, final Path configPath) { // TODO this is wrong, we should only use the environment that is provided to createComponents this.enabled = XPackSettings.SECURITY_ENABLED.get(settings); this.systemIndices = new SecuritySystemIndices(); - this.nodeStartedSignal = new CountDownLatch(1); + this.nodeStartedListenable = new ListenableFuture<>(); if (enabled) { runStartupChecks(settings); Automatons.updateConfiguration(settings); @@ -767,7 +766,7 @@ Collection createComponents( getSslService(), client, environment, - nodeStartedSignal, + (runnable -> nodeStartedListenable.addListener(ActionListener.wrap(runnable))), threadPool ); @@ -1290,7 +1289,7 @@ public Map getProcessors(Processor.Parameters paramet @Override public void onNodeStarted() { - this.nodeStartedSignal.countDown(); + this.nodeStartedListenable.onResponse(null); } /**