Browse Source

Configure security for the initial node cli (#74868)

This introduces a new cmd line tool that generates the security configuration
for a new node in a new cluster (as opposed to joining an existing cluster).
The security configuration consists of TLS key and certificates, which
are stored in a directory inside the config path, as well as settings appended
to the elasticsearch.yml referencing the aforementioned certs.
Albert Zaharovits 4 years ago
parent
commit
b9bc7a6f3f

+ 1 - 1
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java

@@ -66,7 +66,7 @@ import static org.elasticsearch.gradle.internal.vagrant.VagrantMachine.convertWi
 public class DistroTestPlugin implements Plugin<Project> {
     private static final String SYSTEM_JDK_VERSION = "11.0.2+9";
     private static final String SYSTEM_JDK_VENDOR = "openjdk";
-    private static final String GRADLE_JDK_VERSION = "15.0.2+7";
+    private static final String GRADLE_JDK_VERSION = "16.0.1+9";
     private static final String GRADLE_JDK_VENDOR = "adoptopenjdk";
 
     // all distributions used by distro tests. this is temporary until tests are per distribution

+ 0 - 1
build-tools-internal/src/main/resources/forbidden/es-all-signatures.txt

@@ -10,7 +10,6 @@ java.nio.file.Path#of(java.lang.String, java.lang.String[]) @ Use org.elasticsea
 java.nio.file.FileSystems#getDefault() @ use org.elasticsearch.core.PathUtils.getDefaultFileSystem() instead.
 
 java.nio.file.Files#getFileStore(java.nio.file.Path) @ Use org.elasticsearch.env.Environment.getFileStore() instead, impacted by JDK-8034057
-java.nio.file.Files#isWritable(java.nio.file.Path) @ Use org.elasticsearch.env.Environment.isWritable() instead, impacted by JDK-8034057
 
 @defaultMessage Use org.elasticsearch.common.Randomness#get for reproducible sources of randomness
 java.util.Random#<init>()

+ 1 - 0
libs/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java

@@ -13,6 +13,7 @@ package org.elasticsearch.cli;
  */
 public class ExitCodes {
     public static final int OK = 0;
+    public static final int NOOP = 63;           /* nothing to do */
     public static final int USAGE = 64;          /* command line usage error */
     public static final int DATA_ERROR = 65;     /* data format error */
     public static final int NO_INPUT = 66;       /* cannot open input */

+ 6 - 6
qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java

@@ -383,12 +383,6 @@ public class KeystoreManagementTests extends PackagingTestCase {
         final Installation.Executables bin = installation.executables();
         bin.keystoreTool.run("create");
 
-        // this is a hack around the fact that we can't run a command in the same session as the same user but not as administrator.
-        // the keystore ends up being owned by the Administrators group, so we manually set it to be owned by the vagrant user here.
-        // from the server's perspective the permissions aren't really different, this is just to reflect what we'd expect in the tests.
-        // when we run these commands as a role user we won't have to do this
-        Platforms.onWindows(() -> sh.chown(keystore));
-
         if (distribution().isDocker()) {
             try {
                 waitForPathToExist(keystore);
@@ -400,6 +394,12 @@ public class KeystoreManagementTests extends PackagingTestCase {
         if (password != null) {
             setKeystorePassword(password);
         }
+
+        // this is a hack around the fact that we can't run a command in the same session as the same user but not as administrator.
+        // the keystore ends up being owned by the Administrators group, so we manually set it to be owned by the vagrant user here.
+        // from the server's perspective the permissions aren't really different, this is just to reflect what we'd expect in the tests.
+        // when we run these commands as a role user we won't have to do this
+        Platforms.onWindows(() -> sh.chown(keystore));
     }
 
     private void rmKeystoreIfExists() {

+ 1 - 0
qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java

@@ -197,6 +197,7 @@ public class Archives {
             "elasticsearch-certutil",
             "elasticsearch-croneval",
             "elasticsearch-saml-metadata",
+            "elasticsearch-security-config",
             "elasticsearch-setup-passwords",
             "elasticsearch-sql-cli",
             "elasticsearch-syskeygen",

+ 1 - 0
qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java

@@ -496,6 +496,7 @@ public class Docker {
             "elasticsearch-certutil",
             "elasticsearch-croneval",
             "elasticsearch-saml-metadata",
+            "elasticsearch-security-config",
             "elasticsearch-setup-passwords",
             "elasticsearch-sql-cli",
             "elasticsearch-syskeygen",

+ 1 - 0
qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java

@@ -185,6 +185,7 @@ public class Installation {
         public final Executable cronevalTool = new Executable("elasticsearch-croneval");
         public final Executable shardTool = new Executable("elasticsearch-shard");
         public final Executable nodeTool = new Executable("elasticsearch-node");
+        public final Executable securityConfigTool = new Executable("elasticsearch-security-config");
         public final Executable setupPasswordsTool = new Executable("elasticsearch-setup-passwords");
         public final Executable sqlCli = new Executable("elasticsearch-sql-cli");
         public final Executable syskeygenTool = new Executable("elasticsearch-syskeygen");

+ 3 - 14
server/src/internalClusterTest/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java

@@ -26,7 +26,6 @@ import org.junit.BeforeClass;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
-import java.security.AccessControlException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -35,11 +34,11 @@ import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.containsString;
 
 @ESIntegTestCase.ClusterScope(minNumDataNodes = 2)
 public class ReloadSecureSettingsIT extends ESIntegTestCase {
@@ -399,17 +398,7 @@ public class ReloadSecureSettingsIT extends ESIntegTestCase {
 
     private SecureSettings writeEmptyKeystore(Environment environment, char[] password) throws Exception {
         final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create();
-        try {
-            keyStoreWrapper.save(environment.configFile(), password);
-        } catch (final AccessControlException e) {
-            if (e.getPermission() instanceof RuntimePermission && e.getPermission().getName().equals("accessUserInformation")) {
-                // this is expected: the save method is extra diligent and wants to make sure
-                // the keystore is readable, not relying on umask and whatnot. It's ok, we don't
-                // care about this in tests.
-            } else {
-                throw e;
-            }
-        }
+        keyStoreWrapper.save(environment.configFile(), password, false);
         return keyStoreWrapper;
     }
 

+ 1 - 28
server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

@@ -18,7 +18,6 @@ import org.apache.lucene.util.Constants;
 import org.apache.lucene.util.StringHelper;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.Version;
-import org.elasticsearch.cli.KeyStoreAwareCommand;
 import org.elasticsearch.cli.Terminal;
 import org.elasticsearch.cli.UserException;
 import org.elasticsearch.common.PidFile;
@@ -238,37 +237,11 @@ final class Bootstrap {
     }
 
     static SecureSettings loadSecureSettings(Environment initialEnv, InputStream stdin) throws BootstrapException {
-        final KeyStoreWrapper keystore;
         try {
-            keystore = KeyStoreWrapper.load(initialEnv.configFile());
-        } catch (IOException e) {
-            throw new BootstrapException(e);
-        }
-
-        SecureString password;
-        try {
-            if (keystore != null && keystore.hasPassword()) {
-                password = readPassphrase(stdin, KeyStoreAwareCommand.MAX_PASSPHRASE_LENGTH);
-            } else {
-                password = new SecureString(new char[0]);
-            }
-        } catch (IOException e) {
-            throw new BootstrapException(e);
-        }
-
-        try (password) {
-            if (keystore == null) {
-                final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create();
-                keyStoreWrapper.save(initialEnv.configFile(), new char[0]);
-                return keyStoreWrapper;
-            } else {
-                keystore.decrypt(password.getChars());
-                KeyStoreWrapper.upgrade(keystore, initialEnv.configFile(), password.getChars());
-            }
+            return KeyStoreWrapper.bootstrap(initialEnv.configFile(), () -> readPassphrase(stdin, KeyStoreWrapper.MAX_PASSPHRASE_LENGTH));
         } catch (Exception e) {
             throw new BootstrapException(e);
         }
-        return keystore;
     }
 
     // visible for tests

+ 2 - 5
server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java

@@ -26,9 +26,6 @@ public abstract class KeyStoreAwareCommand extends EnvironmentAwareCommand {
         super(description);
     }
 
-    /** Arbitrarily chosen maximum passphrase length */
-    public static final int MAX_PASSPHRASE_LENGTH = 128;
-
     /**
      * Reads the keystore password from the {@link Terminal}, prompting for verification where applicable and returns it as a
      * {@link SecureString}.
@@ -42,9 +39,9 @@ public abstract class KeyStoreAwareCommand extends EnvironmentAwareCommand {
         final char[] passwordArray;
         if (withVerification) {
             passwordArray = terminal.readSecret("Enter new password for the elasticsearch keystore (empty for no password): ",
-                MAX_PASSPHRASE_LENGTH);
+                KeyStoreWrapper.MAX_PASSPHRASE_LENGTH);
             char[] passwordVerification = terminal.readSecret("Enter same password again: ",
-                MAX_PASSPHRASE_LENGTH);
+                KeyStoreWrapper.MAX_PASSPHRASE_LENGTH);
             if (Arrays.equals(passwordArray, passwordVerification) == false) {
                 throw new UserException(ExitCodes.DATA_ERROR, "Passwords are not equal, exiting.");
             }

+ 79 - 8
server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

@@ -21,6 +21,7 @@ import org.apache.lucene.store.NIOFSDirectory;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.cli.ExitCodes;
 import org.elasticsearch.cli.UserException;
+import org.elasticsearch.common.CheckedSupplier;
 import org.elasticsearch.common.Randomness;
 import org.elasticsearch.common.hash.MessageDigests;
 
@@ -45,6 +46,7 @@ import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.AccessDeniedException;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.PosixFileAttributeView;
@@ -72,6 +74,9 @@ import java.util.regex.Pattern;
  */
 public class KeyStoreWrapper implements SecureSettings {
 
+    /** Arbitrarily chosen maximum passphrase length */
+    public static final int MAX_PASSPHRASE_LENGTH = 128;
+
     /** An identifier for the type of data that may be stored in a keystore entry. */
     private enum EntryType {
         STRING,
@@ -101,7 +106,7 @@ public class KeyStoreWrapper implements SecureSettings {
         "~!@#$%^&*-_=+?").toCharArray();
 
     /** The name of the keystore file to read and write. */
-    private static final String KEYSTORE_FILENAME = "elasticsearch.keystore";
+    public static final String KEYSTORE_FILENAME = "elasticsearch.keystore";
 
     /** The version of the metadata written before the keystore data. */
     static final int FORMAT_VERSION = 4;
@@ -194,6 +199,29 @@ public class KeyStoreWrapper implements SecureSettings {
         Arrays.fill(characters, (char)0);
     }
 
+    public static KeyStoreWrapper bootstrap(Path configDir, CheckedSupplier<SecureString, Exception> passwordSupplier) throws Exception {
+        KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
+
+        SecureString password;
+        if (keystore != null && keystore.hasPassword()) {
+            password = passwordSupplier.get();
+        } else {
+            password = new SecureString(new char[0]);
+        }
+
+        try (password) {
+            if (keystore == null) {
+                final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create();
+                keyStoreWrapper.save(configDir, new char[0]);
+                return keyStoreWrapper;
+            } else {
+                keystore.decrypt(password.getChars());
+                KeyStoreWrapper.upgrade(keystore, configDir, password.getChars());
+            }
+        }
+        return keystore;
+    }
+
     /**
      * Loads information about the Elasticsearch keystore from the provided config directory.
      *
@@ -477,11 +505,16 @@ public class KeyStoreWrapper implements SecureSettings {
 
     /** Write the keystore to the given config directory. */
     public synchronized void save(Path configDir, char[] password) throws Exception {
+        save(configDir, password, true);
+    }
+
+    public synchronized void save(Path configDir, char[] password, boolean preservePermissions) throws Exception {
         ensureOpen();
 
         Directory directory = new NIOFSDirectory(configDir);
         // write to tmp file first, then overwrite
         String tmpFile = KEYSTORE_FILENAME + ".tmp";
+        Path keystoreTempFile = configDir.resolve(tmpFile);
         try (IndexOutput output = directory.createOutput(tmpFile, IOContext.DEFAULT)) {
             CodecUtil.writeHeader(output, KEYSTORE_FILENAME, FORMAT_VERSION);
             output.writeByte(password.length == 0 ? (byte)0 : (byte)1);
@@ -515,17 +548,55 @@ public class KeyStoreWrapper implements SecureSettings {
             final String message = String.format(
                 Locale.ROOT,
                 "unable to create temporary keystore at [%s], write permissions required for [%s] or run [elasticsearch-keystore upgrade]",
-                configDir.resolve(tmpFile),
+                keystoreTempFile,
                 configDir);
             throw new UserException(ExitCodes.CONFIG, message, e);
+        } catch (final Exception e) {
+            try {
+                Files.deleteIfExists(keystoreTempFile);
+            } catch (Exception ex) {
+                e.addSuppressed(e);
+            }
+            throw e;
         }
 
         Path keystoreFile = keystorePath(configDir);
-        Files.move(configDir.resolve(tmpFile), keystoreFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
-        PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreFile, PosixFileAttributeView.class);
-        if (attrs != null) {
-            // don't rely on umask: ensure the keystore has minimal permissions
-            attrs.setPermissions(PosixFilePermissions.fromString("rw-rw----"));
+        if (preservePermissions) {
+            try {
+                // check that replace doesn't change the owner
+                if (Files.exists(keystoreFile, LinkOption.NOFOLLOW_LINKS) &&
+                        false == Files.getOwner(keystoreTempFile, LinkOption.NOFOLLOW_LINKS).equals(Files.getOwner(keystoreFile,
+                                LinkOption.NOFOLLOW_LINKS))) {
+                    String message = String.format(
+                            Locale.ROOT,
+                            "will not overwrite keystore at [%s], because this incurs changing the file owner",
+                            keystoreFile);
+                    throw new UserException(ExitCodes.CONFIG, message);
+                }
+                PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreTempFile, PosixFileAttributeView.class);
+                if (attrs != null) {
+                    // don't rely on umask: ensure the keystore has minimal permissions
+                    attrs.setPermissions(PosixFilePermissions.fromString("rw-rw----"));
+                }
+            } catch (Exception e) {
+                try {
+                    Files.deleteIfExists(keystoreTempFile);
+                } catch (Exception ex) {
+                    e.addSuppressed(ex);
+                }
+                throw e;
+            }
+        }
+
+        try {
+            Files.move(keystoreTempFile, keystoreFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+        } catch (Exception e) {
+            try {
+                Files.deleteIfExists(keystoreTempFile);
+            } catch (Exception ex) {
+                e.addSuppressed(ex);
+            }
+            throw e;
         }
     }
 
@@ -584,7 +655,7 @@ public class KeyStoreWrapper implements SecureSettings {
     /**
      * Set a string setting.
      */
-    synchronized void setString(String setting, char[] value) {
+    public synchronized void setString(String setting, char[] value) {
         ensureOpen();
         validateSettingName(setting);
 

BIN
server/src/test/resources/org/elasticsearch/action/admin/elasticsearch-empty-v4.keystore


+ 12 - 2
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java

@@ -31,6 +31,7 @@ import org.bouncycastle.operator.OperatorCreationException;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
+import org.elasticsearch.common.Randomness;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.common.network.NetworkUtils;
@@ -256,7 +257,7 @@ public class CertGenUtils {
      * Gets a random serial for a certificate that is generated from a {@link SecureRandom}
      */
     public static BigInteger getSerial() {
-        SecureRandom random = new SecureRandom();
+        SecureRandom random = Randomness.createSecure();
         BigInteger serial = new BigInteger(SERIAL_BIT_LENGTH, random);
         assert serial.compareTo(BigInteger.valueOf(0L)) >= 0;
         return serial;
@@ -268,7 +269,7 @@ public class CertGenUtils {
     public static KeyPair generateKeyPair(int keysize) throws NoSuchAlgorithmException {
         // generate a private key
         KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
-        keyPairGenerator.initialize(keysize);
+        keyPairGenerator.initialize(keysize, Randomness.createSecure());
         return keyPairGenerator.generateKeyPair();
     }
 
@@ -314,4 +315,13 @@ public class CertGenUtils {
         final ASN1Encodable[] sequence = {new ASN1ObjectIdentifier(CN_OID), new DERTaggedObject(true, 0, new DERUTF8String(cn))};
         return new GeneralName(GeneralName.otherName, new DERSequence(sequence));
     }
+
+    /**
+     * See RFC 2247 Using Domains in LDAP/X.500 Distinguished Names
+     * @param domain active directory domain name
+     * @return LDAP DN, distinguished name, of the root of the domain
+     */
+    public static String buildDnFromDomain(String domain) {
+        return "DC=" + domain.replace(".", ",DC=");
+    }
 }

+ 565 - 0
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigInitialNode.java

@@ -0,0 +1,565 @@
+/*
+ * 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.cli;
+
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.apache.lucene.util.SetOnce;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.elasticsearch.cli.EnvironmentAwareCommand;
+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.DiscoveryNodeRole;
+import org.elasticsearch.common.UUIDs;
+import org.elasticsearch.common.network.NetworkAddress;
+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.core.CheckedConsumer;
+import org.elasticsearch.core.SuppressForbidden;
+import org.elasticsearch.env.Environment;
+import org.elasticsearch.http.HttpTransportSettings;
+import org.elasticsearch.node.NodeRoleSettings;
+import org.elasticsearch.xpack.core.XPackSettings;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+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;
+
+/**
+ * Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
+ * Security enabled. Used to configure only the initial node of a cluster, and only before the first time that the node
+ * 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).
+ */
+public class ConfigInitialNode extends EnvironmentAwareCommand {
+
+    // the transport keystore is also used as a truststore
+    private static final String TRANSPORT_AUTOGENERATED_KEYSTORE_NAME = "transport_keystore_all_nodes";
+    private static final int TRANSPORT_CERTIFICATE_DAYS = 99 * 365;
+    private static final int TRANSPORT_KEY_SIZE = 4096;
+    private static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node";
+    private static final String HTTP_AUTOGENERATED_CA_NAME = "http_ca";
+    private static final int HTTP_CA_CERTIFICATE_DAYS = 3 * 365;
+    private static final int HTTP_CA_KEY_SIZE = 4096;
+    private static final int HTTP_CERTIFICATE_DAYS = 2 * 365;
+    private static final int HTTP_KEY_SIZE = 4096;
+
+    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");
+    }
+
+    public static void main(String[] args) throws Exception {
+        exit(new ConfigInitialNode().main(args, Terminal.DEFAULT));
+    }
+
+    @Override
+    protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
+        // Silently skipping security auto configuration because node considered as restarting.
+        if (Files.isDirectory(env.dataFile()) && Files.list(env.dataFile()).findAny().isPresent()) {
+            terminal.println(expectedNoopVerbosityLevel(),
+                    "Skipping security auto configuration because it appears that the node is not starting up for the first time.");
+            terminal.println(expectedNoopVerbosityLevel(),
+                    "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
+            }
+        }
+        // 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)
+        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
+            }
+        }
+
+        // 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
+            }
+        }
+
+        final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC);
+        final String instantAutoConfigName = "auto_config_on_" + 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
+            PosixFileAttributeView view = Files.getFileAttributeView(instantAutoConfigDir, PosixFileAttributeView.class);
+            if (view != null) {
+                view.setPermissions(PosixFilePermissions.fromString("rwxr-x---"));
+            }
+        } catch (Exception e) {
+            try {
+                Files.deleteIfExists(instantAutoConfigDir);
+            } catch (Exception ex) {
+                e.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
+            }
+        }
+
+        // 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
+            }
+        }
+
+        // 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 GeneralNames subjectAltNames = getSubjectAltNames();
+
+        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, null);
+        KeyPair httpCAKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
+        // self-signed CA
+        X509Certificate httpCACert = CertGenUtils.generateSignedCertificate(certificatePrincipal,
+                subjectAltNames, httpCAKeyPair, null, null, true, HTTP_CA_CERTIFICATE_DAYS, null);
+        KeyPair httpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE);
+        // non-CA
+        X509Certificate httpCert = CertGenUtils.generateSignedCertificate(certificatePrincipal,
+                subjectAltNames, httpKeyPair, httpCACert, httpCAKeyPair.getPrivate(), false, HTTP_CERTIFICATE_DAYS, null);
+
+        // 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)))) {
+                    pemWriter.writeObject(httpCACert);
+                }
+            });
+        } catch (Exception e) {
+            Files.deleteIfExists(instantAutoConfigDir);
+            throw e; // this is an error which mustn't be ignored during node startup
+        }
+
+        // save original keystore before updating (replacing)
+        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) {
+                try {
+                    Files.deleteIfExists(instantAutoConfigDir);
+                } catch (Exception ex) {
+                    e.addSuppressed(ex);
+                }
+                throw e;
+            }
+        }
+
+        final SetOnce<SecureString> nodeKeystorePassword = new SetOnce<>();
+        try (KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> {
+            nodeKeystorePassword.set(new SecureString(terminal.readSecret(nodeKeystorePasswordPrompt(),
+                    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
+            }
+            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_KEYSTORE_NAME, transportKeyPair.getPrivate(),
+                        transportKeystorePassword.getChars(), new Certificate[]{transportCert});
+                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());
+            }
+            try (SecureString httpKeystorePassword = newKeystorePassword()) {
+                KeyStore httpKeystore = KeyStore.getInstance("PKCS12");
+                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()));
+                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) {
+            // 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);
+                } else {
+                    Files.deleteIfExists(keystorePath);
+                }
+            } catch (Exception ex) {
+                e.addSuppressed(ex);
+            }
+            try {
+                Files.deleteIfExists(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)
+            }
+        } finally {
+            if (nodeKeystorePassword.get() != null) {
+                nodeKeystorePassword.get().close();
+            }
+        }
+
+        try {
+            List<String> existingConfigLines = Files.readAllLines(ymlPath, StandardCharsets.UTF_8);
+            fullyWriteFile(env.configFile(), "elasticsearch.yml", true, stream -> {
+                try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
+                    // start with the existing config lines
+                    for (String line : existingConfigLines) {
+                        bw.write(line);
+                        bw.newLine();
+                    }
+                    bw.newLine();
+                    bw.newLine();
+                    bw.write("###################################################################################");
+                    bw.newLine();
+                    bw.write("# The following settings, and associated TLS certificates and keys configuration, #");
+                    bw.newLine();
+                    bw.write("# have been automatically generated in order to configure Security.               #");
+                    bw.newLine();
+                    bw.write("# These have been generated the first time that the new node was started, without #");
+                    bw.newLine();
+                    bw.write("# joining or enrolling to an existing cluster and only if Security had not been   #");
+                    bw.newLine();
+                    bw.write("# explicitly configured beforehand.                                               #");
+                    bw.newLine();
+                    bw.write(String.format(Locale.ROOT, "# %-79s #", ""));
+                    bw.newLine();
+                    bw.write(String.format(Locale.ROOT, "# %-79s #", autoConfigDate));
+                    // TODO add link to docs
+                    bw.newLine();
+                    bw.write("###################################################################################");
+                    bw.newLine();
+                    bw.newLine();
+                    bw.write(XPackSettings.SECURITY_ENABLED.getKey() + ": true");
+                    bw.newLine();
+                    bw.newLine();
+                    if (false == env.settings().hasValue(XPackSettings.ENROLLMENT_ENABLED.getKey())) {
+                        bw.write(XPackSettings.ENROLLMENT_ENABLED.getKey() + ": true");
+                        bw.newLine();
+                        bw.newLine();
+                    }
+
+                    bw.write("xpack.security.transport.ssl.enabled: true");
+                    bw.newLine();
+                    bw.write("# All the nodes use the same key and certificate on the inter-node connection");
+                    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.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.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.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()))) {
+                        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.newLine();
+                        bw.write(HttpTransportSettings.SETTING_HTTP_HOST.getKey() + ": [_local_, _site_]");
+                        bw.newLine();
+                    }
+                }
+            });
+        } catch (Exception e) {
+            try {
+                if (Files.exists(keystoreBackupPath)) {
+                    Files.move(keystoreBackupPath, keystorePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE,
+                            StandardCopyOption.COPY_ATTRIBUTES);
+                } else {
+                    Files.deleteIfExists(keystorePath);
+                }
+            } catch (Exception ex) {
+                e.addSuppressed(ex);
+            }
+            try {
+                Files.deleteIfExists(instantAutoConfigDir);
+            } catch (Exception ex) {
+                e.addSuppressed(ex);
+            }
+            throw e;
+        }
+        Files.deleteIfExists(keystoreBackupPath);
+    }
+
+    @SuppressForbidden(reason = "InetAddress#getCanonicalHostName used to populate auto generated HTTPS cert")
+    private GeneralNames getSubjectAltNames() throws IOException {
+        Set<GeneralName> generalNameSet = new HashSet<>();
+        // use only ipv4 addresses
+        // ipv6 can also technically be used, but they are many and they are long
+        for (InetAddress ip : NetworkUtils.getAllIPV4Addresses()) {
+            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));
+            }
+        }
+        // this is the unequivocal, non-standard, mark for a cert generated by this auto-config process
+        generalNameSet.add(new GeneralName(GeneralName.otherName, CertGenUtils.createCommonName(ConfigInitialNode.class.getName())));
+        return new GeneralNames(generalNameSet.toArray(new GeneralName[0]));
+    }
+
+    // for tests
+    SecureString newKeystorePassword() {
+        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.");
+            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.");
+            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.");
+            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);
+        if (false == canBecomeMaster) {
+            terminal.println(expectedNoopVerbosityLevel(),
+                    "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);
+        if (false == canHoldSecurityIndex) {
+            terminal.println(expectedNoopVerbosityLevel(),
+                    "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.");
+            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)
+        // 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;
+    }
+
+    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;
+        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));
+        }
+        // the default permission
+        Set<PosixFilePermission> permission = PosixFilePermissions.fromString("rw-rw----");
+        // if replacing, use the permission of the replaced file
+        if (Files.exists(filePath)) {
+            PosixFileAttributeView view = Files.getFileAttributeView(filePath, PosixFileAttributeView.class);
+            if (view != null) {
+                permission = view.readAttributes().permissions();
+            }
+        }
+        Path tmpPath = basePath.resolve(fileName + "." + UUIDs.randomBase64UUID() + ".tmp");
+        try (OutputStream outputStream = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW)) {
+            writer.accept(outputStream);
+            PosixFileAttributeView view = Files.getFileAttributeView(tmpPath, PosixFileAttributeView.class);
+            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);
+                }
+            }
+            Files.deleteIfExists(tmpPath);
+        }
+    }
+}

+ 12 - 0
x-pack/plugin/security/src/main/bin/elasticsearch-security-config

@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# 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.
+
+ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.ConfigInitialNode \
+  ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \
+  ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \
+  "$(dirname "$0")/elasticsearch-cli" \
+  -strict "$@"

+ 21 - 0
x-pack/plugin/security/src/main/bin/elasticsearch-security-config.bat

@@ -0,0 +1,21 @@
+@echo off
+
+rem Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+rem or more contributor license agreements. Licensed under the Elastic License
+rem 2.0; you may not use this file except in compliance with the Elastic License
+rem 2.0.
+
+setlocal enabledelayedexpansion
+setlocal enableextensions
+
+set ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.ConfigInitialNode
+set ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env
+set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli
+call "%~dp0elasticsearch-cli.bat -strict" ^
+  %%* ^
+  || goto exit
+
+endlocal
+endlocal
+:exit
+exit /b %ERRORLEVEL%