|
@@ -8,7 +8,8 @@
|
|
|
package org.elasticsearch.xpack.security.cli;
|
|
|
|
|
|
import joptsimple.OptionSet;
|
|
|
-import joptsimple.OptionSpec;
|
|
|
+
|
|
|
+import org.apache.commons.io.FileUtils;
|
|
|
import org.apache.lucene.util.SetOnce;
|
|
|
import org.bouncycastle.asn1.x509.GeneralName;
|
|
|
import org.bouncycastle.asn1.x509.GeneralNames;
|
|
@@ -18,6 +19,7 @@ import org.elasticsearch.cli.ExitCodes;
|
|
|
import org.elasticsearch.cli.Terminal;
|
|
|
import org.elasticsearch.cli.UserException;
|
|
|
import org.elasticsearch.cluster.coordination.ClusterBootstrapService;
|
|
|
+import org.elasticsearch.cluster.node.DiscoveryNode;
|
|
|
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
|
|
|
import org.elasticsearch.common.UUIDs;
|
|
|
import org.elasticsearch.common.network.NetworkAddress;
|
|
@@ -25,14 +27,16 @@ import org.elasticsearch.common.network.NetworkService;
|
|
|
import org.elasticsearch.common.network.NetworkUtils;
|
|
|
import org.elasticsearch.common.settings.KeyStoreWrapper;
|
|
|
import org.elasticsearch.common.settings.SecureString;
|
|
|
+import org.elasticsearch.common.settings.Settings;
|
|
|
import org.elasticsearch.core.CheckedConsumer;
|
|
|
import org.elasticsearch.core.SuppressForbidden;
|
|
|
+import org.elasticsearch.discovery.DiscoveryModule;
|
|
|
+import org.elasticsearch.discovery.SettingsBasedSeedHostsProvider;
|
|
|
import org.elasticsearch.env.Environment;
|
|
|
import org.elasticsearch.http.HttpTransportSettings;
|
|
|
-import org.elasticsearch.node.NodeRoleSettings;
|
|
|
+import org.elasticsearch.node.Node;
|
|
|
import org.elasticsearch.xpack.core.XPackSettings;
|
|
|
|
|
|
-import javax.security.auth.x500.X500Principal;
|
|
|
import java.io.BufferedWriter;
|
|
|
import java.io.IOException;
|
|
|
import java.io.OutputStream;
|
|
@@ -58,8 +62,8 @@ import java.util.HashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Locale;
|
|
|
import java.util.Set;
|
|
|
-
|
|
|
-import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildDnFromDomain;
|
|
|
+import java.util.stream.Stream;
|
|
|
+import javax.security.auth.x500.X500Principal;
|
|
|
|
|
|
/**
|
|
|
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
|
|
@@ -67,10 +71,10 @@ import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildDnFromDomai
|
|
|
* is started. Subsequent nodes can be added to the cluster via the enrollment flow, but this is not used to
|
|
|
* configure such nodes or to display the necessary configuration (ie the enrollment tokens) for such.
|
|
|
*
|
|
|
- * This will not run if Security is explicitly configured or if the existing configuration otherwise clashes with the
|
|
|
- * intent of this (i.e. the node is configured so it cannot form a single node cluster).
|
|
|
+ * This will NOT run if Security is explicitly configured or if the existing configuration otherwise clashes with the
|
|
|
+ * intent of this (i.e. the node is configured so it might not form a single node cluster).
|
|
|
*/
|
|
|
-public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
+public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
|
|
|
public static final String AUTO_CONFIG_ALT_DN = "CN=Elasticsearch security auto-configuration HTTP CA";
|
|
|
// the transport keystore is also used as a truststore
|
|
@@ -87,10 +91,11 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
private static final int HTTP_KEY_SIZE = 4096;
|
|
|
private static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_initial_node_";
|
|
|
|
|
|
- private final OptionSpec<Void> strictOption = parser.accepts("strict", "Error if auto config cannot be performed for any reason");
|
|
|
-
|
|
|
public ConfigInitialNode() {
|
|
|
super("Generates all the necessary security configuration for the initial node of a new secure cluster");
|
|
|
+ // This "cli utility" must be invoked EXCLUSIVELY from the node startup script, where it is passed all the
|
|
|
+ // node startup options unfiltered. It cannot consume most of them, but it does need to inspect the `-E` ones.
|
|
|
+ parser.allowsUnrecognizedOptions();
|
|
|
}
|
|
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
@@ -101,73 +106,70 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
|
|
// Silently skipping security auto configuration because node considered as restarting.
|
|
|
for (Path dataPath : env.dataFiles()) {
|
|
|
- // TODO: Files.list leaks a file handle because the stream is not closed
|
|
|
- // this effectively doesn't matter since config is run in a separate, short lived, process
|
|
|
- // but it should be fixed...
|
|
|
- if (Files.isDirectory(dataPath) && Files.list(dataPath).findAny().isPresent()) {
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
+ if (Files.isDirectory(dataPath) && false == isDirEmpty(dataPath)) {
|
|
|
+ terminal.println(Terminal.Verbosity.VERBOSE,
|
|
|
"Skipping security auto configuration because it appears that the node is not starting up for the first time.");
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
+ terminal.println(Terminal.Verbosity.VERBOSE,
|
|
|
"The node might already be part of a cluster and this auto setup utility is designed to configure Security for new " +
|
|
|
"clusters only.");
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
- } else {
|
|
|
- return; // silent error because we wish the node to start as usual (skip auto config) during a restart
|
|
|
- }
|
|
|
+ // we wish the node to start as usual during a restart
|
|
|
+ // but still the exit code should indicate that this has not been run
|
|
|
+ throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
}
|
|
|
- // preflight checks for the files that are going to be changed
|
|
|
- // Skipping security auto configuration if configuration files cannot be mutated (ie are read-only)
|
|
|
+
|
|
|
+ // pre-flight checks for the files that are going to be changed
|
|
|
final Path ymlPath = env.configFile().resolve("elasticsearch.yml");
|
|
|
final Path keystorePath = KeyStoreWrapper.keystorePath(env.configFile());
|
|
|
- try {
|
|
|
- // it is odd for the `elasticsearch.yml` file to be missing or not be a regular (the node won't start)
|
|
|
- // but auto configuration should not be concerned with fixing it (by creating the file) and let the node startup fail
|
|
|
- if (false == Files.exists(ymlPath) || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS)) {
|
|
|
- terminal.println(unexpectedNoopVerbosityLevel(), String.format(Locale.ROOT, "Skipping security auto configuration because" +
|
|
|
- " the configuration file [%s] is missing or is not a regular file", ymlPath));
|
|
|
- throw new UserException(ExitCodes.CONFIG, null);
|
|
|
- }
|
|
|
- // If the node's yml configuration is not readable, most probably auto-configuration isn't run under the suitable user
|
|
|
- if (false == Files.isReadable(ymlPath)) {
|
|
|
- terminal.println(unexpectedNoopVerbosityLevel(), String.format(Locale.ROOT, "Skipping security auto configuration because" +
|
|
|
- " the configuration file [%s] is not readable", ymlPath));
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
- }
|
|
|
- // Inform that auto-configuration will not run if keystore cannot be read.
|
|
|
- if (Files.exists(keystorePath) && (false == Files.isRegularFile(keystorePath, LinkOption.NOFOLLOW_LINKS) ||
|
|
|
- false == Files.isReadable(keystorePath))) {
|
|
|
- terminal.println(unexpectedNoopVerbosityLevel(), String.format(Locale.ROOT, "Skipping security auto configuration because" +
|
|
|
- " the node keystore file [%s] is not a readable regular file", keystorePath));
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
- }
|
|
|
- } catch (UserException e) {
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw e;
|
|
|
- } else {
|
|
|
- return; // silent error because we wish the node to start as usual (skip auto config) if the configuration is read-only
|
|
|
- }
|
|
|
+ // it is odd for the `elasticsearch.yml` file to be missing or not be a regular (the node won't start)
|
|
|
+ // but auto configuration should not be concerned with fixing it (by creating the file) and let the node startup fail
|
|
|
+ if (false == Files.exists(ymlPath) || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS)) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.NORMAL,
|
|
|
+ String.format(
|
|
|
+ Locale.ROOT,
|
|
|
+ "Skipping security auto configuration because the configuration file [%s] is missing or is not a regular file",
|
|
|
+ ymlPath
|
|
|
+ )
|
|
|
+ );
|
|
|
+ throw new UserException(ExitCodes.CONFIG, null);
|
|
|
+ }
|
|
|
+ // If the node's yml configuration is not readable, most probably auto-configuration isn't run under the suitable user
|
|
|
+ if (false == Files.isReadable(ymlPath)) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.NORMAL,
|
|
|
+ String.format(
|
|
|
+ Locale.ROOT,
|
|
|
+ "Skipping security auto configuration because the current user does not have permission to read "
|
|
|
+ + " configuration file [%s]",
|
|
|
+ ymlPath
|
|
|
+ )
|
|
|
+ );
|
|
|
+ throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ }
|
|
|
+ // Inform that auto-configuration will not run if keystore cannot be read.
|
|
|
+ if (Files.exists(keystorePath)
|
|
|
+ && (false == Files.isRegularFile(keystorePath, LinkOption.NOFOLLOW_LINKS) || false == Files.isReadable(keystorePath))) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.NORMAL,
|
|
|
+ String.format(
|
|
|
+ Locale.ROOT,
|
|
|
+ "Skipping security auto configuration because the node keystore file [%s] is not a readable regular file",
|
|
|
+ keystorePath
|
|
|
+ )
|
|
|
+ );
|
|
|
+ throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
|
|
|
// only perform auto-configuration if the existing configuration is not conflicting (eg Security already enabled)
|
|
|
// if it is, silently skip auto configuration
|
|
|
- try {
|
|
|
- checkExistingConfiguration(env, terminal);
|
|
|
- } catch (UserException e) {
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw e;
|
|
|
- } else {
|
|
|
- return; // silent error because we wish the node to start as usual (skip auto config) if certain configurations are set
|
|
|
- }
|
|
|
- }
|
|
|
+ checkExistingConfiguration(env.settings(), terminal);
|
|
|
|
|
|
final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC);
|
|
|
- final String instantAutoConfigName = TLS_CONFIG_DIR_NAME_PREFIX + autoConfigDate.toInstant().getEpochSecond();;
|
|
|
+ final String instantAutoConfigName = TLS_CONFIG_DIR_NAME_PREFIX + autoConfigDate.toInstant().getEpochSecond();
|
|
|
final Path instantAutoConfigDir = env.configFile().resolve(instantAutoConfigName);
|
|
|
try {
|
|
|
// it is useful to pre-create the sub-config dir in order to check that the config dir is writable and that file owners match
|
|
|
- // THIS AUTO CONFIGURATION COMMAND WILL NOT CHANGE THE OWNERS OF CONFIG FILES
|
|
|
Files.createDirectory(instantAutoConfigDir);
|
|
|
// set permissions to 750, don't rely on umask, we assume auto configuration preserves ownership so we don't have to
|
|
|
// grant "group" or "other" permissions
|
|
@@ -175,112 +177,155 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
if (view != null) {
|
|
|
view.setPermissions(PosixFilePermissions.fromString("rwxr-x---"));
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
+ } catch (Throwable t) {
|
|
|
try {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
// the config dir is probably read-only, either because this auto-configuration runs as a different user from the install user,
|
|
|
// or if the admin explicitly makes configuration immutable (read-only), both of which are reasons to skip auto-configuration
|
|
|
// this will show a message to the console (the first time the node starts) and auto-configuration is effectively bypassed
|
|
|
// the message will not be subsequently shown (because auto-configuration doesn't run for node restarts)
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw new UserException(ExitCodes.CANT_CREATE, "Could not create auto configuration directory", e);
|
|
|
- } else {
|
|
|
- return; // silent error because we wish the node to start as usual (skip auto config) if config dir is not writable
|
|
|
- }
|
|
|
+ throw new UserException(ExitCodes.CANT_CREATE, "Could not create auto configuration directory", t);
|
|
|
}
|
|
|
|
|
|
- // Ensure that the files created by the auto-config command MUST have the same owner as the config dir itself,
|
|
|
- // as well as that the replaced files don't change ownership.
|
|
|
- // This is because the files created by this command have hard-coded "no" permissions for "group" and "other"
|
|
|
- UserPrincipal newFileOwner = Files.getOwner(instantAutoConfigDir, LinkOption.NOFOLLOW_LINKS);
|
|
|
- if ((false == newFileOwner.equals(Files.getOwner(env.configFile(), LinkOption.NOFOLLOW_LINKS))) ||
|
|
|
- (false == newFileOwner.equals(Files.getOwner(ymlPath, LinkOption.NOFOLLOW_LINKS))) ||
|
|
|
- (Files.exists(keystorePath) && false == newFileOwner.equals(Files.getOwner(keystorePath, LinkOption.NOFOLLOW_LINKS)))) {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw new UserException(ExitCodes.CONFIG, "Aborting auto configuration because it would change config file owners");
|
|
|
- } else {
|
|
|
- return; // if a different user runs ES compared to the user that installed it, auto configuration will not run
|
|
|
- }
|
|
|
+ // Check that the created auto-config dir has the same owner as the config dir.
|
|
|
+ // This is a sort of sanity check.
|
|
|
+ // If the node process works OK given the owner of the config dir, it should also tolerate the auto-created config dir,
|
|
|
+ // provided that they both have the same owner and permissions.
|
|
|
+ final UserPrincipal newFileOwner = Files.getOwner(instantAutoConfigDir, LinkOption.NOFOLLOW_LINKS);
|
|
|
+ if (false == newFileOwner.equals(Files.getOwner(env.configFile(), LinkOption.NOFOLLOW_LINKS))) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ // the following is only printed once, if the node starts successfully
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.CONFIG,
|
|
|
+ "Aborting auto configuration because of config dir ownership mismatch. Config dir is owned by "
|
|
|
+ + Files.getOwner(env.configFile(), LinkOption.NOFOLLOW_LINKS).getName()
|
|
|
+ + " but auto-configuration directory would be owned by "
|
|
|
+ + newFileOwner.getName()
|
|
|
+ );
|
|
|
}
|
|
|
+ final KeyPair transportKeyPair;
|
|
|
+ final X509Certificate transportCert;
|
|
|
+ final KeyPair httpCAKeyPair;
|
|
|
+ final X509Certificate httpCACert;
|
|
|
+ final KeyPair httpKeyPair;
|
|
|
+ final X509Certificate httpCert;
|
|
|
+ try {
|
|
|
+ // the transport key-pair is the same across the cluster and is trusted without hostname verification (it is self-signed),
|
|
|
+ final X500Principal certificatePrincipal = new X500Principal("CN=" + System.getenv("HOSTNAME"));
|
|
|
+ final X500Principal caPrincipal = new X500Principal(AUTO_CONFIG_ALT_DN);
|
|
|
+ // this does DNS resolve and could block
|
|
|
+ final GeneralNames subjectAltNames = getSubjectAltNames();
|
|
|
|
|
|
- // the transport key-pair is the same across the cluster and is trusted without hostname verification (it is self-signed),
|
|
|
- final X500Principal certificatePrincipal = new X500Principal(buildDnFromDomain(System.getenv("HOSTNAME")));
|
|
|
- final X500Principal caPrincipal = new X500Principal(AUTO_CONFIG_ALT_DN);
|
|
|
- // this does DNS resolve and could block
|
|
|
- final GeneralNames subjectAltNames = getSubjectAltNames();
|
|
|
+ transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
|
|
|
+ // self-signed which is not a CA
|
|
|
+ transportCert = CertGenUtils.generateSignedCertificate(
|
|
|
+ certificatePrincipal,
|
|
|
+ subjectAltNames,
|
|
|
+ transportKeyPair,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ false,
|
|
|
+ TRANSPORT_CERTIFICATE_DAYS,
|
|
|
+ "SHA256withRSA"
|
|
|
+ );
|
|
|
+ httpCAKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
|
|
|
+ // self-signed CA
|
|
|
+ httpCACert = CertGenUtils.generateSignedCertificate(
|
|
|
+ caPrincipal,
|
|
|
+ null,
|
|
|
+ httpCAKeyPair,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ true,
|
|
|
+ HTTP_CA_CERTIFICATE_DAYS,
|
|
|
+ "SHA256withRSA"
|
|
|
+ );
|
|
|
+ httpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE);
|
|
|
+ // non-CA
|
|
|
+ httpCert = CertGenUtils.generateSignedCertificate(
|
|
|
+ certificatePrincipal,
|
|
|
+ subjectAltNames,
|
|
|
+ httpKeyPair,
|
|
|
+ httpCACert,
|
|
|
+ httpCAKeyPair.getPrivate(),
|
|
|
+ false,
|
|
|
+ HTTP_CERTIFICATE_DAYS,
|
|
|
+ "SHA256withRSA"
|
|
|
+ );
|
|
|
|
|
|
- KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
|
|
|
- // self-signed which is not a CA
|
|
|
- X509Certificate transportCert = CertGenUtils.generateSignedCertificate(certificatePrincipal,
|
|
|
- subjectAltNames, transportKeyPair, null, null, false, TRANSPORT_CERTIFICATE_DAYS, "SHA256withRSA");
|
|
|
- KeyPair httpCAKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
|
|
|
- // self-signed CA
|
|
|
- X509Certificate httpCACert = CertGenUtils.generateSignedCertificate(caPrincipal,
|
|
|
- null , httpCAKeyPair, null, null, true, HTTP_CA_CERTIFICATE_DAYS, "SHA256withRSA");
|
|
|
- KeyPair httpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE);
|
|
|
- // non-CA
|
|
|
- X509Certificate httpCert = CertGenUtils.generateSignedCertificate(certificatePrincipal,
|
|
|
- subjectAltNames, httpKeyPair, httpCACert, httpCAKeyPair.getPrivate(), false, HTTP_CERTIFICATE_DAYS, "SHA256withRSA");
|
|
|
+ // the HTTP CA PEM file is provided "just in case". The node doesn't use it, but clients (configured manually, outside of the
|
|
|
+ // enrollment process) might indeed need it, and it is currently impossible to retrieve it
|
|
|
|
|
|
- // the HTTP CA PEM file is provided "just in case", the node configuration doesn't use it
|
|
|
- // but clients (configured manually, outside of the enrollment process) might indeed need it and
|
|
|
- // it is impossible to use the keystore because it is password protected because it contains the key
|
|
|
- try {
|
|
|
fullyWriteFile(instantAutoConfigDir, HTTP_AUTOGENERATED_CA_NAME + ".crt", false, stream -> {
|
|
|
- try (JcaPEMWriter pemWriter =
|
|
|
- new JcaPEMWriter(new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8)))) {
|
|
|
+ try (
|
|
|
+ JcaPEMWriter pemWriter = new JcaPEMWriter(new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8)))
|
|
|
+ ) {
|
|
|
pemWriter.writeObject(httpCACert);
|
|
|
}
|
|
|
});
|
|
|
- } catch (Exception e) {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
- throw e; // this is an error which mustn't be ignored during node startup
|
|
|
+ } catch (Throwable t) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ // this is an error which mustn't be ignored during node startup
|
|
|
+ // the exit code for unhandled Exceptions is "1"
|
|
|
+ throw t;
|
|
|
}
|
|
|
|
|
|
// save original keystore before updating (replacing)
|
|
|
- final Path keystoreBackupPath =
|
|
|
- env.configFile().resolve(KeyStoreWrapper.KEYSTORE_FILENAME + "." + autoConfigDate.toInstant().getEpochSecond() + ".orig");
|
|
|
+ final Path keystoreBackupPath = env.configFile()
|
|
|
+ .resolve(KeyStoreWrapper.KEYSTORE_FILENAME + "." + autoConfigDate.toInstant().getEpochSecond() + ".orig");
|
|
|
if (Files.exists(keystorePath)) {
|
|
|
try {
|
|
|
Files.copy(keystorePath, keystoreBackupPath, StandardCopyOption.COPY_ATTRIBUTES);
|
|
|
- } catch (Exception e) {
|
|
|
+ } catch (Throwable t) {
|
|
|
try {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
- throw e;
|
|
|
+ throw t;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
final SetOnce<SecureString> nodeKeystorePassword = new SetOnce<>();
|
|
|
try (KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> {
|
|
|
- nodeKeystorePassword.set(new SecureString(terminal.readSecret(nodeKeystorePasswordPrompt(),
|
|
|
- KeyStoreWrapper.MAX_PASSPHRASE_LENGTH)));
|
|
|
+ nodeKeystorePassword.set(new SecureString(terminal.readSecret("", KeyStoreWrapper.MAX_PASSPHRASE_LENGTH)));
|
|
|
return nodeKeystorePassword.get().clone();
|
|
|
})) {
|
|
|
// do not overwrite keystore entries
|
|
|
// instead expect the user to manually remove them herself
|
|
|
- if (nodeKeystore.getSettingNames().contains("xpack.security.transport.ssl.keystore.secure_password") ||
|
|
|
- nodeKeystore.getSettingNames().contains("xpack.security.transport.ssl.truststore.secure_password") ||
|
|
|
- nodeKeystore.getSettingNames().contains("xpack.security.http.ssl.keystore.secure_password")) {
|
|
|
- throw new UserException(ExitCodes.CONFIG, "Aborting auto configuration because the node keystore contains password " +
|
|
|
- "settings already"); // it is OK to silently ignore these because the node won't start
|
|
|
+ if (nodeKeystore.getSettingNames().contains("xpack.security.transport.ssl.keystore.secure_password")
|
|
|
+ || nodeKeystore.getSettingNames().contains("xpack.security.transport.ssl.truststore.secure_password")
|
|
|
+ || nodeKeystore.getSettingNames().contains("xpack.security.http.ssl.keystore.secure_password")) {
|
|
|
+ // this error condition is akin to condition of existing configuration in the yml file
|
|
|
+ // this is not a fresh install and the admin has something planned for Security
|
|
|
+ // Even though this is probably invalid configuration, do NOT fix it, let the node fail to start in its usual way.
|
|
|
+ // Still display a message, because this can be tricky to figure out (why auto-conf did not run) if by mistake.
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.CONFIG,
|
|
|
+ "Aborting auto configuration because the node keystore contains password " + "settings already"
|
|
|
+ );
|
|
|
}
|
|
|
try (SecureString transportKeystorePassword = newKeystorePassword()) {
|
|
|
KeyStore transportKeystore = KeyStore.getInstance("PKCS12");
|
|
|
transportKeystore.load(null);
|
|
|
// the PKCS12 keystore and the contained private key use the same password
|
|
|
- transportKeystore.setKeyEntry(TRANSPORT_AUTOGENERATED_KEY_ALIAS, transportKeyPair.getPrivate(),
|
|
|
- transportKeystorePassword.getChars(), new Certificate[]{transportCert});
|
|
|
+ transportKeystore.setKeyEntry(
|
|
|
+ TRANSPORT_AUTOGENERATED_KEY_ALIAS,
|
|
|
+ transportKeyPair.getPrivate(),
|
|
|
+ transportKeystorePassword.getChars(),
|
|
|
+ new Certificate[] { transportCert }
|
|
|
+ );
|
|
|
// the transport keystore is used as a trustore too, hence it must contain a certificate entry
|
|
|
transportKeystore.setCertificateEntry(TRANSPORT_AUTOGENERATED_CERT_ALIAS, transportCert);
|
|
|
- fullyWriteFile(instantAutoConfigDir, TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12", false,
|
|
|
- stream -> transportKeystore.store(stream, transportKeystorePassword.getChars()));
|
|
|
+ fullyWriteFile(
|
|
|
+ instantAutoConfigDir,
|
|
|
+ TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12",
|
|
|
+ false,
|
|
|
+ stream -> transportKeystore.store(stream, transportKeystorePassword.getChars())
|
|
|
+ );
|
|
|
nodeKeystore.setString("xpack.security.transport.ssl.keystore.secure_password", transportKeystorePassword.getChars());
|
|
|
// we use the same PKCS12 file for the keystore and the truststore
|
|
|
nodeKeystore.setString("xpack.security.transport.ssl.truststore.secure_password", transportKeystorePassword.getChars());
|
|
@@ -290,41 +335,45 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
httpKeystore.load(null);
|
|
|
// the keystore contains both the node's and the CA's private keys
|
|
|
// both keys are encrypted using the same password as the PKCS12 keystore they're contained in
|
|
|
- httpKeystore.setKeyEntry(HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca", httpCAKeyPair.getPrivate(),
|
|
|
- httpKeystorePassword.getChars(), new Certificate[]{httpCACert});
|
|
|
- httpKeystore.setKeyEntry(HTTP_AUTOGENERATED_KEYSTORE_NAME, httpKeyPair.getPrivate(),
|
|
|
- httpKeystorePassword.getChars(), new Certificate[]{httpCert, httpCACert});
|
|
|
- fullyWriteFile(instantAutoConfigDir, HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12", false,
|
|
|
- stream -> httpKeystore.store(stream, httpKeystorePassword.getChars()));
|
|
|
+ httpKeystore.setKeyEntry(
|
|
|
+ HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca",
|
|
|
+ httpCAKeyPair.getPrivate(),
|
|
|
+ httpKeystorePassword.getChars(),
|
|
|
+ new Certificate[] { httpCACert }
|
|
|
+ );
|
|
|
+ httpKeystore.setKeyEntry(
|
|
|
+ HTTP_AUTOGENERATED_KEYSTORE_NAME,
|
|
|
+ httpKeyPair.getPrivate(),
|
|
|
+ httpKeystorePassword.getChars(),
|
|
|
+ new Certificate[] { httpCert, httpCACert }
|
|
|
+ );
|
|
|
+ fullyWriteFile(
|
|
|
+ instantAutoConfigDir,
|
|
|
+ HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12",
|
|
|
+ false,
|
|
|
+ stream -> httpKeystore.store(stream, httpKeystorePassword.getChars())
|
|
|
+ );
|
|
|
nodeKeystore.setString("xpack.security.http.ssl.keystore.secure_password", httpKeystorePassword.getChars());
|
|
|
}
|
|
|
// finally overwrites the node keystore (if the keystores have been successfully written)
|
|
|
nodeKeystore.save(env.configFile(), nodeKeystorePassword.get() == null ? new char[0] : nodeKeystorePassword.get().getChars());
|
|
|
- } catch (Exception e) {
|
|
|
+ } catch (Throwable t) {
|
|
|
// restore keystore to revert possible keystore bootstrap
|
|
|
try {
|
|
|
if (Files.exists(keystoreBackupPath)) {
|
|
|
- Files.move(keystoreBackupPath, keystorePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE,
|
|
|
- StandardCopyOption.COPY_ATTRIBUTES);
|
|
|
+ Files.move(keystoreBackupPath, keystorePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
|
|
} else {
|
|
|
Files.deleteIfExists(keystorePath);
|
|
|
}
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
try {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
- }
|
|
|
- if (false == (e instanceof UserException)) {
|
|
|
- throw e; // unexpected exections should prevent the node from starting
|
|
|
- }
|
|
|
- if (options.has(strictOption)) {
|
|
|
- throw e;
|
|
|
- } else {
|
|
|
- return; // ignoring if the keystore contains password values already, so that the node startup deals with it (fails)
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
+ throw t;
|
|
|
} finally {
|
|
|
if (nodeKeystorePassword.get() != null) {
|
|
|
nodeKeystorePassword.get().close();
|
|
@@ -365,7 +414,9 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
bw.write(XPackSettings.SECURITY_ENABLED.getKey() + ": true");
|
|
|
bw.newLine();
|
|
|
bw.newLine();
|
|
|
- if (false == env.settings().hasValue(XPackSettings.ENROLLMENT_ENABLED.getKey())) {
|
|
|
+ // Set enrollment mode to true unless user explicitly set it to false themselves
|
|
|
+ if (false == (env.settings().hasValue(XPackSettings.ENROLLMENT_ENABLED.getKey())
|
|
|
+ && false == XPackSettings.ENROLLMENT_ENABLED.get(env.settings()))) {
|
|
|
bw.write(XPackSettings.ENROLLMENT_ENABLED.getKey() + ": true");
|
|
|
bw.newLine();
|
|
|
bw.newLine();
|
|
@@ -377,73 +428,101 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
bw.newLine();
|
|
|
bw.write("xpack.security.transport.ssl.verification_mode: certificate");
|
|
|
bw.newLine();
|
|
|
- bw.write("xpack.security.transport.ssl.keystore.path: " + instantAutoConfigDir
|
|
|
- .resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12"));
|
|
|
+ bw.write(
|
|
|
+ "xpack.security.transport.ssl.keystore.path: "
|
|
|
+ + instantAutoConfigDir.resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12")
|
|
|
+ );
|
|
|
bw.newLine();
|
|
|
// we use the keystore as a truststore in order to minimize the number of auto-generated resources,
|
|
|
// and also because a single file is more idiomatic to the scheme of a shared secret between the cluster nodes
|
|
|
// no one should only need the TLS cert without the associated key for the transport layer
|
|
|
- bw.write("xpack.security.transport.ssl.truststore.path: " + instantAutoConfigDir
|
|
|
- .resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12"));
|
|
|
+ bw.write(
|
|
|
+ "xpack.security.transport.ssl.truststore.path: "
|
|
|
+ + instantAutoConfigDir.resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12")
|
|
|
+ );
|
|
|
bw.newLine();
|
|
|
|
|
|
bw.newLine();
|
|
|
bw.write("xpack.security.http.ssl.enabled: true");
|
|
|
bw.newLine();
|
|
|
- bw.write("xpack.security.http.ssl.keystore.path: " + instantAutoConfigDir.resolve(HTTP_AUTOGENERATED_KEYSTORE_NAME +
|
|
|
- ".p12"));
|
|
|
+ bw.write(
|
|
|
+ "xpack.security.http.ssl.keystore.path: " + instantAutoConfigDir.resolve(HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12")
|
|
|
+ );
|
|
|
bw.newLine();
|
|
|
|
|
|
+ // we have configured TLS on the transport layer with newly generated certs and keys,
|
|
|
+ // hence this node cannot form a multi-node cluster
|
|
|
+ // if we don't set the following the node might trip the discovery bootstrap check
|
|
|
+ if (false == DiscoveryModule.isSingleNodeDiscovery(env.settings())
|
|
|
+ && false == ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.exists(env.settings())) {
|
|
|
+ bw.newLine();
|
|
|
+ bw.write("# The initial node with security auto-configured must form a cluster on its own,");
|
|
|
+ bw.newLine();
|
|
|
+ bw.write("# and all the subsequent nodes should be added via the node enrollment flow");
|
|
|
+ bw.newLine();
|
|
|
+ bw.write(ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.getKey() + ": [\"${HOSTNAME}\"]");
|
|
|
+ bw.newLine();
|
|
|
+ }
|
|
|
+
|
|
|
// if any address settings have been set, assume the admin has thought it through wrt to addresses,
|
|
|
// and don't try to be smart and mess with that
|
|
|
- if (false == (env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_HOST.getKey()) ||
|
|
|
- env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_BIND_HOST.getKey()) ||
|
|
|
- env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST.getKey()) ||
|
|
|
- env.settings().hasValue(NetworkService.GLOBAL_NETWORK_HOST_SETTING.getKey()) ||
|
|
|
- env.settings().hasValue(NetworkService.GLOBAL_NETWORK_BIND_HOST_SETTING.getKey()) ||
|
|
|
- env.settings().hasValue(NetworkService.GLOBAL_NETWORK_PUBLISH_HOST_SETTING.getKey()))) {
|
|
|
+ if (false == (env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_HOST.getKey())
|
|
|
+ || env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_BIND_HOST.getKey())
|
|
|
+ || env.settings().hasValue(HttpTransportSettings.SETTING_HTTP_PUBLISH_HOST.getKey())
|
|
|
+ || env.settings().hasValue(NetworkService.GLOBAL_NETWORK_HOST_SETTING.getKey())
|
|
|
+ || env.settings().hasValue(NetworkService.GLOBAL_NETWORK_BIND_HOST_SETTING.getKey())
|
|
|
+ || env.settings().hasValue(NetworkService.GLOBAL_NETWORK_PUBLISH_HOST_SETTING.getKey()))) {
|
|
|
bw.newLine();
|
|
|
- bw.write("# With security now configured, which includes user authentication over HTTPs, " +
|
|
|
- "it's reasonable to serve requests on the local network too");
|
|
|
+ bw.write(
|
|
|
+ "# With security now configured, which includes user authentication over HTTPs, "
|
|
|
+ + "it's reasonable to serve requests on the local network too"
|
|
|
+ );
|
|
|
bw.newLine();
|
|
|
bw.write(HttpTransportSettings.SETTING_HTTP_HOST.getKey() + ": [_local_, _site_]");
|
|
|
bw.newLine();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
- } catch (Exception e) {
|
|
|
+ } catch (Throwable t) {
|
|
|
try {
|
|
|
if (Files.exists(keystoreBackupPath)) {
|
|
|
- Files.move(keystoreBackupPath, keystorePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE,
|
|
|
- StandardCopyOption.COPY_ATTRIBUTES);
|
|
|
+ Files.move(
|
|
|
+ keystoreBackupPath,
|
|
|
+ keystorePath,
|
|
|
+ StandardCopyOption.REPLACE_EXISTING,
|
|
|
+ StandardCopyOption.ATOMIC_MOVE,
|
|
|
+ StandardCopyOption.COPY_ATTRIBUTES
|
|
|
+ );
|
|
|
} else {
|
|
|
Files.deleteIfExists(keystorePath);
|
|
|
}
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
try {
|
|
|
- Files.deleteIfExists(instantAutoConfigDir);
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
} catch (Exception ex) {
|
|
|
- e.addSuppressed(ex);
|
|
|
+ t.addSuppressed(ex);
|
|
|
}
|
|
|
- throw e;
|
|
|
+ throw t;
|
|
|
}
|
|
|
+ // only delete the backed up file if all went well
|
|
|
Files.deleteIfExists(keystoreBackupPath);
|
|
|
}
|
|
|
|
|
|
- @SuppressForbidden(reason = "InetAddress#getCanonicalHostName used to populate auto generated HTTPS cert")
|
|
|
+ @SuppressForbidden(reason = "Uses File API because the commons io library does, which is useful for file manipulation")
|
|
|
+ private void deleteDirectory(Path directory) throws IOException {
|
|
|
+ FileUtils.deleteDirectory(directory.toFile());
|
|
|
+ }
|
|
|
+
|
|
|
private GeneralNames getSubjectAltNames() throws IOException {
|
|
|
Set<GeneralName> generalNameSet = new HashSet<>();
|
|
|
for (InetAddress ip : NetworkUtils.getAllAddresses()) {
|
|
|
String ipString = NetworkAddress.format(ip);
|
|
|
generalNameSet.add(new GeneralName(GeneralName.iPAddress, ipString));
|
|
|
- String reverseFQDN = ip.getCanonicalHostName();
|
|
|
- if (false == ipString.equals(reverseFQDN)) {
|
|
|
- // reverse FQDN successful
|
|
|
- generalNameSet.add(new GeneralName(GeneralName.dNSName, reverseFQDN));
|
|
|
- }
|
|
|
}
|
|
|
+ generalNameSet.add(new GeneralName(GeneralName.dNSName, "localhost"));
|
|
|
+ generalNameSet.add(new GeneralName(GeneralName.dNSName, System.getenv("HOSTNAME")));
|
|
|
return new GeneralNames(generalNameSet.toArray(new GeneralName[0]));
|
|
|
}
|
|
|
|
|
@@ -452,88 +531,102 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
return UUIDs.randomBase64UUIDSecureString();
|
|
|
}
|
|
|
|
|
|
- // Detect if the existing yml configuration is incompatible with auto-configuration,
|
|
|
- // in which case auto-configuration is SILENTLY skipped.
|
|
|
- // This assumes the user knows what she's doing when configuring the node.
|
|
|
- void checkExistingConfiguration(Environment environment, Terminal terminal) throws UserException {
|
|
|
- // Silently skipping security auto configuration, because Security is already configured.
|
|
|
- if (environment.settings().hasValue(XPackSettings.SECURITY_ENABLED.getKey())) {
|
|
|
- // do not try to validate, correct or fill in any incomplete security configuration,
|
|
|
- // instead rely on the regular node startup to do this validation
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because it appears that security is already configured.");
|
|
|
+ /*
|
|
|
+ * Detect if the existing yml configuration is incompatible with auto-configuration,
|
|
|
+ * in which case auto-configuration is SILENTLY skipped.
|
|
|
+ * This assumes the user knows what they are doing when configuring the node.
|
|
|
+ */
|
|
|
+ void checkExistingConfiguration(Settings settings, Terminal terminal) throws UserException {
|
|
|
+ // Allow the user to explicitly set that they don't want auto-configuration for security, regardless of our heuristics
|
|
|
+ if (XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED.get(settings) == false) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because [" + XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED.getKey() + "] is false"
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skipping security auto configuration if enrollment is disabled.
|
|
|
- // But tolerate enrollment explicitly enabled, as it could be useful to enable it by a command line option
|
|
|
- // only the first time that the node is started.
|
|
|
- if (environment.settings().hasValue(XPackSettings.ENROLLMENT_ENABLED.getKey()) && false ==
|
|
|
- XPackSettings.ENROLLMENT_ENABLED.get(environment.settings())) {
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because enrollment is explicitly disabled.");
|
|
|
+ // Silently skip security auto configuration when Security is already configured.
|
|
|
+ // Security is enabled implicitly, but if the admin chooses to enable it explicitly then
|
|
|
+ // skip the TLS auto-configuration, as this is a sign that the admin is opting for the default behavior
|
|
|
+ if (XPackSettings.SECURITY_ENABLED.exists(settings)) {
|
|
|
+ // do not try to validate, correct or fill in any incomplete security configuration,
|
|
|
+ // instead rely on the regular node startup to do this validation
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because it appears that security is already configured."
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skipping security auto configuration because the node is configured for cluster formation.
|
|
|
- // Auto-configuration assumes that this is done in order to configure a multi-node cluster,
|
|
|
- // and Security auto-configuration doesn't work when bootstrapping a multi node clusters
|
|
|
- if (environment.settings().hasValue(ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.getKey())) {
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because this node is explicitly configured to form a new cluster.");
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "The node cannot be auto configured to participate in forming a new multi-node secure cluster.");
|
|
|
+ // Security auto configuration must not run if the node is configured for multi-node cluster formation (bootstrap or join).
|
|
|
+ // This is because transport TLS with newly generated certs will hinder cluster formation because the other nodes cannot trust yet.
|
|
|
+ if (false == isInitialClusterNode(settings)) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because this node is configured to bootstrap or to join a "
|
|
|
+ + "multi-node cluster, which is not supported."
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skipping security auto configuration because node cannot become master.
|
|
|
- final List<DiscoveryNodeRole> nodeRoles = NodeRoleSettings.NODE_ROLES_SETTING.get(environment.settings());
|
|
|
- boolean canBecomeMaster = nodeRoles.contains(DiscoveryNodeRole.MASTER_ROLE) &&
|
|
|
- false == nodeRoles.contains(DiscoveryNodeRole.VOTING_ONLY_NODE_ROLE);
|
|
|
+ // Silently skip security auto configuration because node cannot become master.
|
|
|
+ boolean canBecomeMaster = DiscoveryNode.isMasterNode(settings)
|
|
|
+ && false == DiscoveryNode.hasRole(settings, DiscoveryNodeRole.VOTING_ONLY_NODE_ROLE);
|
|
|
if (false == canBecomeMaster) {
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because the node is configured such that it cannot become master.");
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because the node is configured such that it cannot become master."
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skipping security auto configuration, because the node cannot contain the Security index data
|
|
|
- boolean canHoldSecurityIndex = nodeRoles.stream().anyMatch(DiscoveryNodeRole::canContainData);
|
|
|
+ // Silently skip security auto configuration, because the node cannot contain the Security index data
|
|
|
+ boolean canHoldSecurityIndex = DiscoveryNode.canContainData(settings);
|
|
|
if (false == canHoldSecurityIndex) {
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because the node is configured such that it cannot contain data.");
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because the node is configured such that it cannot contain data."
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
// Silently skipping security auto configuration because TLS is already configured
|
|
|
- if (false == environment.settings().getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX).isEmpty() ||
|
|
|
- false == environment.settings().getByPrefix(XPackSettings.HTTP_SSL_PREFIX).isEmpty()) {
|
|
|
- // zero validation for the TLS settings as well, let the node bootup do its thing
|
|
|
- terminal.println(expectedNoopVerbosityLevel(),
|
|
|
- "Skipping security auto configuration because it appears that TLS is already configured.");
|
|
|
+ if (false == settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX).isEmpty()
|
|
|
+ || false == settings.getByPrefix(XPackSettings.HTTP_SSL_PREFIX).isEmpty()) {
|
|
|
+ // zero validation for the TLS settings as well, let the node boot and do its thing
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because it appears that TLS is already configured."
|
|
|
+ );
|
|
|
throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // auto-configuration runs even if the realms are configured in any way (assuming defining realms is permitted without touching
|
|
|
- // the xpack.security.enabled setting, otherwise auto-config doesn't run, see previous condition)
|
|
|
+ // auto-configuration runs even if the realms are configured in any way,
|
|
|
+ // including defining file based users (defining realms is permitted without touching
|
|
|
+ // the xpack.security.enabled setting)
|
|
|
// but the file realm is required for some of the auto-configuration parts (setting/resetting the elastic user)
|
|
|
// if disabled, it must be manually enabled back and, preferably, at the head of the realm chain
|
|
|
}
|
|
|
|
|
|
- String nodeKeystorePasswordPrompt() {
|
|
|
- return "Enter password for the elasticsearch keystore : ";
|
|
|
- }
|
|
|
-
|
|
|
- Terminal.Verbosity expectedNoopVerbosityLevel() {
|
|
|
- return Terminal.Verbosity.NORMAL;
|
|
|
+ // Unfortunately, we cannot tell, for every configuration, if it is going to result in a multi node cluster, as it depends
|
|
|
+ // on the addresses that this node, and the others, will bind to when starting (and this runs on a single node before it
|
|
|
+ // starts).
|
|
|
+ // Here we take a conservative approach: if any of the discovery or initial master nodes setting are set to a non-empty
|
|
|
+ // value, we assume the admin intended a multi-node cluster configuration. There is only one exception: if the initial master
|
|
|
+ // nodes setting contains just the current node name.
|
|
|
+ private boolean isInitialClusterNode(Settings settings) {
|
|
|
+ return DiscoveryModule.isSingleNodeDiscovery(settings)
|
|
|
+ || (ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.get(settings).isEmpty()
|
|
|
+ && SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING.get(settings).isEmpty()
|
|
|
+ && DiscoveryModule.DISCOVERY_SEED_PROVIDERS_SETTING.get(settings).isEmpty())
|
|
|
+ || ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.get(settings).equals(List.of(Node.NODE_NAME_SETTING.get(settings)));
|
|
|
}
|
|
|
|
|
|
- Terminal.Verbosity unexpectedNoopVerbosityLevel() {
|
|
|
- return Terminal.Verbosity.NORMAL;
|
|
|
- }
|
|
|
-
|
|
|
- private static void fullyWriteFile(Path basePath, String fileName, boolean replace,
|
|
|
- CheckedConsumer<OutputStream, Exception> writer) throws Exception {
|
|
|
- boolean success = false;
|
|
|
+ private static void fullyWriteFile(Path basePath, String fileName, boolean replace, CheckedConsumer<OutputStream, Exception> writer)
|
|
|
+ throws Exception {
|
|
|
Path filePath = basePath.resolve(fileName);
|
|
|
if (false == replace && Files.exists(filePath)) {
|
|
|
- throw new UserException(ExitCodes.IO_ERROR, String.format(Locale.ROOT, "Output file [%s] already exists and " +
|
|
|
- "will not be replaced", filePath));
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.IO_ERROR,
|
|
|
+ String.format(Locale.ROOT, "Output file [%s] already exists and " + "will not be replaced", filePath)
|
|
|
+ );
|
|
|
}
|
|
|
- // the default permission
|
|
|
+ // the default permission, if not replacing; if replacing use the permission of the to be replaced file
|
|
|
Set<PosixFilePermission> permission = PosixFilePermissions.fromString("rw-rw----");
|
|
|
// if replacing, use the permission of the replaced file
|
|
|
if (Files.exists(filePath)) {
|
|
@@ -549,26 +642,20 @@ public class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
if (view != null) {
|
|
|
view.setPermissions(permission);
|
|
|
}
|
|
|
- success = true;
|
|
|
- } finally {
|
|
|
- if (success) {
|
|
|
- if (replace) {
|
|
|
- if (Files.exists(filePath, LinkOption.NOFOLLOW_LINKS) &&
|
|
|
- false == Files.getOwner(tmpPath, LinkOption.NOFOLLOW_LINKS).equals(Files.getOwner(filePath,
|
|
|
- LinkOption.NOFOLLOW_LINKS))) {
|
|
|
- Files.deleteIfExists(tmpPath);
|
|
|
- String message = String.format(
|
|
|
- Locale.ROOT,
|
|
|
- "will not overwrite file at [%s], because this incurs changing the file owner",
|
|
|
- filePath);
|
|
|
- throw new UserException(ExitCodes.CONFIG, message);
|
|
|
- }
|
|
|
- Files.move(tmpPath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
|
|
- } else {
|
|
|
- Files.move(tmpPath, filePath, StandardCopyOption.ATOMIC_MOVE);
|
|
|
- }
|
|
|
+ if (replace) {
|
|
|
+ Files.move(tmpPath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
|
|
+ } else {
|
|
|
+ Files.move(tmpPath, filePath, StandardCopyOption.ATOMIC_MOVE);
|
|
|
}
|
|
|
+ } finally {
|
|
|
Files.deleteIfExists(tmpPath);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private static boolean isDirEmpty(Path path) throws IOException {
|
|
|
+ // Files.list MUST always be used in a try-with-resource construct in order to release the dir file handler
|
|
|
+ try (Stream<Path> dirContentsStream = Files.list(path)) {
|
|
|
+ return false == dirContentsStream.findAny().isPresent();
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|