Skip to content

Commit

Permalink
Update auto-generated credentials output (#79755)
Browse files Browse the repository at this point in the history
- Adjust formatting and wording according to @lockewritesdocs
feedback
- Generate and print enrollment token for nodes when the node is
not listening only on localhost
- Control generation/output on a single setting

Co-authored-by: Albert Zaharovits <albert.zaharovits@elastic.co>
  • Loading branch information
jkakavas and albertzaharovits committed Oct 26, 2021
1 parent 7d21e3c commit 24a7bd1
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 290 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,22 @@ private XPackSettings() {
public static final Setting<Boolean> FIPS_MODE_ENABLED =
Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope);

/** Setting for enabling enrollment process; set-up by the es start-up script */
/**
* Setting for enabling the enrollment process, ie the enroll APIs are enabled, and the initial cluster node generates and displays
* enrollment tokens (for Kibana and sometimes for ES nodes) when starting up for the first time.
* This is usually set by start-up scripts, which run before the node starts, which perform TLS and cluster formation specific
* configuration (persisted in the node's config dir).
* This can be toggled liberally by admins (it can be made a dynamic setting), in order to permit or not the enrollment of subsequent
* nodes. Nevertheless, we assumes that when {@code ENROLLMENT_ENABLED} is {@code true} the node MUST have been configured by said
* start-up scripts (eg we don't support enrollment with general TLS certificates).
*/
public static final Setting<Boolean> ENROLLMENT_ENABLED =
Setting.boolSetting("xpack.security.enrollment.enabled", false, Property.NodeScope);

/**
* Setting for enabling or disabling the TLS auto-configuration as well as credentials auto-generation for nodes, before starting for
* the first time, and in the absence of other conflicting configurations.
*/
public static final Setting<Boolean> SECURITY_AUTOCONFIGURATION_ENABLED =
Setting.boolSetting("xpack.security.autoconfiguration.enabled", true, Property.NodeScope);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.Strings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore;
import org.elasticsearch.xpack.security.enrollment.BaseEnrollmentTokenGenerator;
import org.elasticsearch.xpack.security.enrollment.InternalEnrollmentTokenGenerator;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;

import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;

import static org.elasticsearch.xpack.core.XPackSettings.ENROLLMENT_ENABLED;
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_ELASTIC_PASSWORD_HASH;
import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.BOOTSTRAP_ELASTIC_PASSWORD;
import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword;

public class InitialNodeSecurityAutoConfiguration {

private static final Logger LOGGER = LogManager.getLogger(InitialNodeSecurityAutoConfiguration.class);

private InitialNodeSecurityAutoConfiguration() {
throw new IllegalStateException("Class should not be instantiated");
}

/**
* Generates and displays a password for the elastic superuser, an enrollment token for kibana and an enrollment token for es
* nodes, the first time a node starts as the first node in a cluster, when a terminal is attached.
*/
public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStartup(
NativeUsersStore nativeUsersStore,
SecurityIndexManager securityIndexManager,
SSLService sslService,
Client client,
Environment environment
) {
// 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
// tokens being generated).
// This is not ideal because the {@code ENROLLMENT_ENABLED} setting is now interpreted as
// "did the pre-startup configuration completed", in order to generate/display information assuming
// and relying on that configuration being done.
// TODO maybe we can improve the "did pre-start-up config run" check
if (false == ENROLLMENT_ENABLED.get(environment.settings())) {
return;
}
final InternalEnrollmentTokenGenerator enrollmentTokenGenerator = new InternalEnrollmentTokenGenerator(
environment,
sslService,
client
);
final PrintStream out = BootstrapInfo.getOriginalStandardOut();
// Check if it has been closed, try to write something so that we trigger PrintStream#ensureOpen
out.println();
if (out.checkError()) {
LOGGER.info("Auto-configuration will not generate a password for the elastic built-in superuser, as we cannot " +
" determine if there is a terminal attached to the elasticsearch process. You can use the" +
" `bin/elasticsearch-reset-password` tool to set the password for the elastic user.");
return;
}
// if enrollment is enabled, we assume (and document this assumption) that the node is auto-configured in a specific way
// wrt to TLS and cluster formation
securityIndexManager.onStateRecovered(securityIndexState -> {
if (false == securityIndexState.indexExists()) {
// a starting node with {@code ENROLLMENT_ENABLED} set to true, and with no .security index,
// must be the initial node of a cluster (starting for the first time and forming a cluster by itself)
// Not always true, but in the cases where it's not (which involve deleting the .security index which
// 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<Map<String, String>> groupedActionListener =
new GroupedActionListener<>(ActionListener.wrap(results -> {
final Map<String, String> allResultsMap = new HashMap<>();
for (Map<String, String> 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 skyped
groupedActionListener.onResponse(Map.of("generated_elastic_user_password", ""));
}
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());
}
});
enrollmentTokenGenerator.maybeCreateNodeEnrollmentToken(encodedNodeToken -> {
if (encodedNodeToken != null) {
groupedActionListener.onResponse(Map.of("node_enrollment_token", encodedNodeToken));
} else {
groupedActionListener.onResponse(Map.of());
}
});
}
});
}

private static void outputInformationToConsole(String elasticPassword, String kibanaEnrollmentToken,
String nodeEnrollmentToken, String caCertFingerprint, PrintStream out) {
StringBuilder builder = new StringBuilder();
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("--------------------------------------------------------------------------------------------------------------");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
if (elasticPassword == null) {
builder.append("Unable to auto-generate the password for the elastic built-in superuser.");
} else if (Strings.isEmpty(elasticPassword)) {
builder.append("The generated password for the elastic built-in superuser has not been changed.");
} else {
builder.append("The generated password for the elastic built-in superuser is:");
builder.append(System.lineSeparator());
builder.append(elasticPassword);

}
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
if (null != kibanaEnrollmentToken) {
builder.append("The enrollment token for Kibana instances, valid for the next ");
builder.append(BaseEnrollmentTokenGenerator.ENROLL_API_KEY_EXPIRATION_MINUTES);
builder.append(" minutes:");
builder.append(System.lineSeparator());
builder.append(kibanaEnrollmentToken);
} else {
builder.append("Unable to generate an enrollment token for Kibana instances.");
}
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
if (nodeEnrollmentToken == null) {
builder.append("Unable to generate an enrollment token for Elasticsearch nodes.");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
} else if (false == Strings.isEmpty(nodeEnrollmentToken)) {
builder.append("The enrollment token for Elasticsearch instances, valid for the next ");
builder.append(BaseEnrollmentTokenGenerator.ENROLL_API_KEY_EXPIRATION_MINUTES);
builder.append(" minutes:");
builder.append(System.lineSeparator());
builder.append(nodeEnrollmentToken);
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
}
if (null != caCertFingerprint) {
builder.append("The hex-encoded SHA-256 fingerprint of the generated HTTPS CA DER-encoded certificate:");
builder.append(System.lineSeparator());
builder.append(caCertFingerprint);
builder.append(System.lineSeparator());
}
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("You can complete the following actions at any time:");
builder.append(System.lineSeparator());
builder.append("Reset the password of the elastic built-in superuser with 'bin/elasticsearch-reset-password -u elastic'.");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("Generate an enrollment token for Kibana instances with 'bin/elasticsearch-create-enrollment-token -s kibana'.");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("Generate an enrollment token for Elasticsearch nodes with 'bin/elasticsearch-create-enrollment-token -s node'.");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("--------------------------------------------------------------------------------------------------------------");
builder.append(System.lineSeparator());
builder.append(System.lineSeparator());
out.println(builder);
}
}

0 comments on commit 24a7bd1

Please sign in to comment.