diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java index ff6722895e4ed..52dfbc5907e6f 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java @@ -66,7 +66,7 @@ public class DistroTestPlugin implements Plugin { 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 diff --git a/build-tools-internal/src/main/resources/forbidden/es-all-signatures.txt b/build-tools-internal/src/main/resources/forbidden/es-all-signatures.txt index 7a5126457a42b..64c05ad953ab2 100644 --- a/build-tools-internal/src/main/resources/forbidden/es-all-signatures.txt +++ b/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#() diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java b/libs/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java index 7edb6bade44f0..2f66a858f4e72 100644 --- a/libs/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java +++ b/libs/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java @@ -13,6 +13,7 @@ */ 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 */ diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java index e982c7609bd2c..b00a263f37236 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/KeystoreManagementTests.java @@ -383,12 +383,6 @@ private void createKeystore(String password) throws Exception { 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 @@ private void createKeystore(String password) throws Exception { 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() { diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java index 516bdbaacfab1..c54965aaf21f2 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java @@ -197,6 +197,7 @@ private static void verifyDefaultInstallation(Installation es, Distribution dist "elasticsearch-certutil", "elasticsearch-croneval", "elasticsearch-saml-metadata", + "elasticsearch-security-config", "elasticsearch-setup-passwords", "elasticsearch-sql-cli", "elasticsearch-syskeygen", diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 5b9333cabb461..9b7677ddd81ef 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -496,6 +496,7 @@ private static void verifyDefaultInstallation(Installation es) { "elasticsearch-certutil", "elasticsearch-croneval", "elasticsearch-saml-metadata", + "elasticsearch-security-config", "elasticsearch-setup-passwords", "elasticsearch-sql-cli", "elasticsearch-syskeygen", diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java index e8e6f1061ac0f..06c95c3aab887 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java @@ -185,6 +185,7 @@ public class Executables { 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"); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java index fea1b1c120e55..c8a984172ef41 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java @@ -26,7 +26,6 @@ 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.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 void onFailure(Exception e) { 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; } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index 472afeaa4f738..00ccac0d4bf04 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -18,7 +18,6 @@ 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 @@ static SecureSettings loadSecureSettings(Environment initialEnv) throws Bootstra } 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 diff --git a/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java b/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java index c17bd88d40fab..e2ca18263487b 100644 --- a/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java +++ b/server/src/main/java/org/elasticsearch/cli/KeyStoreAwareCommand.java @@ -26,9 +26,6 @@ public KeyStoreAwareCommand(String description) { 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 @@ protected static SecureString readPassword(Terminal terminal, boolean withVerifi 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."); } diff --git a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java index 54d8328b0da31..83a959ce97733 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java +++ b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java @@ -21,6 +21,7 @@ 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.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 @@ */ 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 @@ private static class Entry { "~!@#$%^&*-_=+?").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 static void addBootstrapSeed(KeyStoreWrapper wrapper) { Arrays.fill(characters, (char)0); } + public static KeyStoreWrapper bootstrap(Path configDir, CheckedSupplier 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 @@ private void decryptLegacyEntries() throws GeneralSecurityException, IOException /** 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 synchronized void save(Path configDir, char[] password) throws Exception 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 static void validateSettingName(String setting) { /** * Set a string setting. */ - synchronized void setString(String setting, char[] value) { + public synchronized void setString(String setting, char[] value) { ensureOpen(); validateSettingName(setting); diff --git a/server/src/test/resources/org/elasticsearch/action/admin/elasticsearch-empty-v4.keystore b/server/src/test/resources/org/elasticsearch/action/admin/elasticsearch-empty-v4.keystore new file mode 100644 index 0000000000000..1ac8661d2b5dc Binary files /dev/null and b/server/src/test/resources/org/elasticsearch/action/admin/elasticsearch-empty-v4.keystore differ diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 7037807d15aa0..3f46c53d2cc7b 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -31,6 +31,7 @@ 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 @@ static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal pri * 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 static BigInteger getSerial() { 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 static GeneralName createCommonName(String cn) { 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="); + } } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigInitialNode.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigInitialNode.java new file mode 100644 index 0000000000000..c1820971bb78f --- /dev/null +++ b/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 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 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 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 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 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 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 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); + } + } +} diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-security-config b/x-pack/plugin/security/src/main/bin/elasticsearch-security-config new file mode 100755 index 0000000000000..b59994f01c07f --- /dev/null +++ b/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 "$@" diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-security-config.bat b/x-pack/plugin/security/src/main/bin/elasticsearch-security-config.bat new file mode 100644 index 0000000000000..189495c765da1 --- /dev/null +++ b/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%