From 3ee60acc8e342dd080ebb8470d8ef6e0a08c93b7 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 3 Sep 2021 14:46:44 +0300 Subject: [PATCH 01/12] wip, no tests --- .../elasticsearch/common/ssl/PemUtils.java | 7 + x-pack/plugin/security/cli/build.gradle | 1 + .../security/cli/ConfigAdditionalNodes.java | 565 ++++++++++++++++++ .../xpack/security/cli/ConfigInitialNode.java | 1 + .../cli/ConfigAdditionalNodesTests.java | 86 +++ .../src/main/bin/elasticsearch-enroll-node | 12 + .../security/enrollment/EnrollmentToken.java | 55 ++ .../security/tool/CommandLineHttpClient.java | 117 +++- .../enrollment/EnrollmentTokenTests.java | 25 +- 9 files changed, 850 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java create mode 100644 x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java create mode 100644 x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java index eaccf729cc13b..cb4505e271ff1 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java @@ -213,6 +213,13 @@ private static PrivateKey parsePKCS8(BufferedReader bReader) throws IOException, return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); } + public static PrivateKey parsePKCS8PemString(String pem) throws IOException, GeneralSecurityException{ + byte[] keyBytes = Base64.getDecoder().decode(pem); + String keyAlgo = getKeyAlgorithmIdentifier(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + } + /** * Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an EC private key encoded in * OpenSSL traditional format. diff --git a/x-pack/plugin/security/cli/build.gradle b/x-pack/plugin/security/cli/build.gradle index 738dcaf338de4..c1bb37b6f3115 100644 --- a/x-pack/plugin/security/cli/build.gradle +++ b/x-pack/plugin/security/cli/build.gradle @@ -8,6 +8,7 @@ archivesBaseName = 'elasticsearch-security-cli' dependencies { compileOnly project(":server") compileOnly project(path: xpackModule('core')) + compileOnly project(path: xpackModule('security')) api "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" api "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}" testImplementation("com.google.jimfs:jimfs:${versions.jimfs}") { diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java new file mode 100644 index 0000000000000..9aeaf084ed322 --- /dev/null +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.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.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.KeyStoreAwareCommand; +import org.elasticsearch.cli.SuppressForbidden; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.Strings; +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.Tuple; +import org.elasticsearch.env.Environment; +import org.elasticsearch.http.HttpTransportSettings; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.security.tool.HttpResponse; + +import javax.security.auth.x500.X500Principal; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.URL; +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.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.ssl.PemUtils.parsePKCS8PemString; +import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING; +import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildDnFromDomain; +import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; + +/** + * Configures a node to join an existing cluster with security features enabled. + */ +public class ConfigAdditionalNodes extends KeyStoreAwareCommand { + + private final OptionSpec enrollmentTokenParam = parser.accepts("enrollment-token", "The enrollment token to use") + .withRequiredArg().required(); + private final BiFunction clientFunction; + + private static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_node_"; + private static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node"; + private static final String TRANSPORT_AUTOGENERATED_KEYSTORE_NAME = "transport_keystore_all_nodes"; + private static final String TRANSPORT_AUTOGENERATED_KEY_ALIAS = "transport_all_nodes_key"; + private static final String TRANSPORT_AUTOGENERATED_CERT_ALIAS = "transport_all_nodes_cert"; + private static final int HTTP_CERTIFICATE_DAYS = 2 * 365; + private static final int HTTP_KEY_SIZE = 4096; + + public ConfigAdditionalNodes(BiFunction clientFunction) { + super("Configures security so that this node can join an existing cluster"); + this.clientFunction = clientFunction; + } + + public ConfigAdditionalNodes() { + this(CommandLineHttpClient::new); + } + + public static void main(String[] args) throws Exception { + exit(new ConfigAdditionalNodes().main(args, Terminal.DEFAULT)); + } + + @Override + protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { + + final Path ymlPath = env.configFile().resolve("elasticsearch.yml"); + final Path keystorePath = KeyStoreWrapper.keystorePath(env.configFile()); + if (false == Files.exists(ymlPath) || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS)) { + terminal.println( + Terminal.Verbosity.NORMAL, + String.format( + Locale.ROOT, + "Skipping security auto configuration because the configuration file [%s] is missing or is not a regular file", + ymlPath + ) + ); + throw new UserException(ExitCodes.CONFIG, null); + } + + if (false == Files.isReadable(ymlPath)) { + terminal.println( + Terminal.Verbosity.NORMAL, + String.format( + Locale.ROOT, + "Skipping security auto configuration because the configuration file [%s] is not readable", + ymlPath + ) + ); + throw new UserException(ExitCodes.CONFIG, null); + } + + if (Files.exists(keystorePath) + && (false == Files.isRegularFile(keystorePath, LinkOption.NOFOLLOW_LINKS) || false == Files.isReadable(keystorePath))) { + terminal.println( + Terminal.Verbosity.NORMAL, + String.format( + Locale.ROOT, + "Skipping security auto configuration because the node keystore file [%s] is not a readable regular file", + keystorePath + ) + ); + throw new UserException(ExitCodes.CONFIG, null); + } + + final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC); + final String instantAutoConfigName = TLS_CONFIG_DIR_NAME_PREFIX + autoConfigDate.toInstant().getEpochSecond(); + final Path instantAutoConfigDir = env.configFile().resolve(instantAutoConfigName); + try { + // it is useful to pre-create the sub-config dir in order to check that the config dir is writable and that file owners match + 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); + } + throw new UserException(ExitCodes.CANT_CREATE, "Could not create auto configuration directory", e); + } + + final UserPrincipal newFileOwner = Files.getOwner(instantAutoConfigDir, LinkOption.NOFOLLOW_LINKS); + if (false == newFileOwner.equals(Files.getOwner(env.configFile(), LinkOption.NOFOLLOW_LINKS))) { + Files.deleteIfExists(instantAutoConfigDir); + throw new UserException(ExitCodes.CONFIG, "Aborting auto configuration because of config dir ownership mismatch"); + } + + final EnrollmentToken enrollmentToken; + try { + enrollmentToken = EnrollmentToken.decodeFromString(enrollmentTokenParam.value(options)); + } catch (IOException e) { + throw new UserException(ExitCodes.IO_ERROR, "Invalid enrollment token"); + } + + final CommandLineHttpClient client = clientFunction.apply(env, enrollmentToken.getFingerprint()); + + // We don't wait for cluster health here. If the user has a token, it means that at least the first node has started + // successfully so we expect the cluster to be healthy already. If not, this is a sign of a problem and we should bail. + final URL enrollNodeUrl = createURL(new URL("https://" + enrollmentToken.getBoundAddress().get(0)), "/_security/enroll/node", ""); + final HttpResponse enrollResponse = client.execute( + "GET", + enrollNodeUrl, + new SecureString(enrollmentToken.getApiKey().toCharArray()), + () -> null, + CommandLineHttpClient::responseBuilder + ); + if (enrollResponse.getHttpStatus() != 200) { + throw new UserException( + ExitCodes.UNAVAILABLE, + "Unexpected HTTP status [" + enrollResponse.getHttpStatus() + "] calling the enroll node API (" + enrollNodeUrl + ")" + ); + } + final Map responseMap = enrollResponse.getResponseBody(); + if (responseMap == null) { + throw new UserException(ExitCodes.DATA_ERROR, "Empty response when calling the enroll node API (" + enrollNodeUrl + ")"); + } + final String httpCaKeyPem = (String) responseMap.get("http_ca_key"); + final String httpCaCertPem = (String) responseMap.get("http_ca_cert"); + final String transportKeyPem = (String) responseMap.get("transport_key"); + final String transportCertPem = (String) responseMap.get("transport_cert"); + @SuppressWarnings("unchecked") + final List transportAddresses = (List) responseMap.get("nodes_addresses"); + if (Strings.isNullOrEmpty(httpCaCertPem) + || Strings.isNullOrEmpty(httpCaKeyPem) + || Strings.isNullOrEmpty(transportKeyPem) + || Strings.isNullOrEmpty(transportCertPem) + || null == transportAddresses) { + throw new UserException(ExitCodes.DATA_ERROR, "Invalid response when calling the enroll node API (" + enrollNodeUrl + ")"); + } + + final Tuple httpCa = parseKeyCertFromPem(httpCaKeyPem, httpCaCertPem); + final PrivateKey httpCaKey = httpCa.v1(); + final X509Certificate httpCaCert = httpCa.v2(); + final Tuple transport = parseKeyCertFromPem(transportKeyPem, transportCertPem); + final PrivateKey transportKey = transport.v1(); + final X509Certificate transportCert = transport.v2(); + + final X500Principal certificatePrincipal = new X500Principal(buildDnFromDomain(System.getenv("HOSTNAME"))); + // this does DNS resolve and could block + final GeneralNames subjectAltNames = getSubjectAltNames(); + + final KeyPair nodeHttpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE); + final X509Certificate nodeHttpCert = CertGenUtils.generateSignedCertificate( + certificatePrincipal, + subjectAltNames, + nodeHttpKeyPair, + httpCaCert, + httpCaKey, + false, + HTTP_CERTIFICATE_DAYS, + null + ); + + // 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("", KeyStoreWrapper.MAX_PASSPHRASE_LENGTH))); + return nodeKeystorePassword.get().clone(); + })) { + // do not overwrite keystore entries + // instead expect the user to manually remove them themselves + 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 and settings already" + ); + } + try (SecureString httpKeystorePassword = newKeystorePassword()) { + final KeyStore httpKeystore = KeyStore.getInstance("PKCS12"); + httpKeystore.load(null); + httpKeystore.setKeyEntry( + HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca", + httpCaKey, + httpKeystorePassword.getChars(), + new Certificate[] { httpCaCert } + ); + httpKeystore.setKeyEntry( + HTTP_AUTOGENERATED_KEYSTORE_NAME, + nodeHttpKeyPair.getPrivate(), + httpKeystorePassword.getChars(), + new Certificate[] { nodeHttpCert, 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()); + } + + try (SecureString transportKeystorePassword = newKeystorePassword()) { + KeyStore transportKeystore = KeyStore.getInstance("PKCS12"); + transportKeystore.load(null); + // the PKCS12 keystore and the contained private key use the same password + transportKeystore.setKeyEntry( + TRANSPORT_AUTOGENERATED_KEY_ALIAS, + transportKey, + transportKeystorePassword.getChars(), + new Certificate[] { transportCert } + ); + // the transport keystore is used as a truststore too, hence it must contain a certificate entry + transportKeystore.setCertificateEntry(TRANSPORT_AUTOGENERATED_CERT_ALIAS, transportCert); + fullyWriteFile( + instantAutoConfigDir, + TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12", + false, + stream -> transportKeystore.store(stream, transportKeystorePassword.getChars()) + ); + 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()); + } + // finally overwrites the node keystore (if the keystore 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); + } + throw e; + } finally { + if (nodeKeystorePassword.get() != null) { + nodeKeystorePassword.get().close(); + } + } + + // We have everything, let's write to the config + 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, when #"); + bw.newLine(); + bw.write("# enrolling to an existing cluster #"); + 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(); + bw.write("# We set seed.hosts so that the node can actually discover the existing nodes in the cluster"); + bw.newLine(); + bw.write( + DISCOVERY_SEED_HOSTS_SETTING.getKey() + + ": [" + + transportAddresses.stream().map(p -> '"' + p + '"').collect(Collectors.joining(", ")) + + "]" + ); + 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; + } + // only delete the backed up file if all went well + Files.deleteIfExists(keystoreBackupPath); + + } + + 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); + } + } + + SecureString newKeystorePassword() { + return UUIDs.randomBase64UUIDSecureString(); + } + + @SuppressForbidden(reason = "DNS resolve 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)); + } + } + return new GeneralNames(generalNameSet.toArray(new GeneralName[0])); + } + + private Tuple parseKeyCertFromPem(String pemFormattedKey, String pemFormattedCert) throws UserException { + final PrivateKey key; + final X509Certificate cert; + try { + final List certs = CertParsingUtils.readCertificates( + Base64.getDecoder().wrap(new ByteArrayInputStream(pemFormattedCert.getBytes(StandardCharsets.UTF_8))) + ); + if (certs.size() != 1) { + throw new IllegalStateException("Enroll node API returned multiple certificates"); + } + cert = (X509Certificate) certs.get(0); + key = parsePKCS8PemString(pemFormattedKey); + return new Tuple<>(key, cert); + } catch (Exception e) { + throw new UserException( + ExitCodes.DATA_ERROR, + "Failed to parse Private Key and Certificate from the response of the Enroll Node API" + ); + } + } + +} 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 index 951426117e693..3c3f8fd11c21f 100644 --- 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 @@ -266,6 +266,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th // 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}); + transportKeystore.setCertificateEntry("catransport", 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()); diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java new file mode 100644 index 0000000000000..01f83090804b8 --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java @@ -0,0 +1,86 @@ +/* + * 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 com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; + +import org.elasticsearch.common.CheckedSupplier; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.PathUtilsForTesting; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConfigAdditionalNodesTests extends ESTestCase { + + static FileSystem jimfs; + private Path confDir; + private CommandLineHttpClient client; + + @BeforeClass + public static void setupJimfs() { + Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix").build(); + jimfs = Jimfs.newFileSystem(conf); + PathUtilsForTesting.installMock(jimfs); + } + + @Before + public void setup() throws Exception { + Path homeDir = jimfs.getPath("eshome"); + IOUtils.rm(homeDir); + confDir = homeDir.resolve("config"); + Files.createDirectories(confDir); + + HttpResponse nodeEnrollResponse = new HttpResponse( + HttpURLConnection.HTTP_OK, + Map.of("status", randomFrom("yellow", "green")) + ); + this.client = mock(CommandLineHttpClient.class); + when( + client.execute( + anyString(), + any(URL.class), + anyString(), + any(SecureString.class), + any(CheckedSupplier.class), + any(CheckedFunction.class) + ) + ).thenReturn(nodeEnrollResponse); + + } + + @AfterClass + public static void closeJimfs() throws IOException { + if (jimfs != null) { + jimfs.close(); + jimfs = null; + } + } + + +} diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node new file mode 100644 index 0000000000000..cce0b32dec638 --- /dev/null +++ b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node @@ -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.ConfigAdditionalNodes \ + ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \ + ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \ + "$(dirname "$0")/elasticsearch-cli" \ + "$@" diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java index c62fcaeb01b69..50cbb74a65b93 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java @@ -8,14 +8,22 @@ package org.elasticsearch.xpack.security.enrollment; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ParseField; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Objects; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + public class EnrollmentToken { private final String apiKey; private final String fingerprint; @@ -27,6 +35,21 @@ public class EnrollmentToken { public String getVersion() { return version; } public List getBoundAddress() { return boundAddress; } + private static final ParseField API_KEY = new ParseField("key"); + private static final ParseField FINGERPRINT = new ParseField("fgr"); + private static final ParseField VERSION = new ParseField("ver"); + private static final ParseField ADDRESS = new ParseField("adr"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("enrollment_token", false, + a -> new EnrollmentToken((String) a[0], (String) a[1], (String) a[2], (List) a[3])); + + static { + PARSER.declareString(constructorArg(), API_KEY); + PARSER.declareString(constructorArg(), FINGERPRINT); + PARSER.declareString(constructorArg(), VERSION); + PARSER.declareStringArray(constructorArg(), ADDRESS); + } /** * Create an EnrollmentToken * @@ -61,4 +84,36 @@ public String getEncoded() throws Exception { final String jsonString = getRaw(); return Base64.getUrlEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); } + + /** + * Decodes and parses an enrollment token from it's serialized form (created with {@link EnrollmentToken#getEncoded()} + * @param encoded The Base64 encoded JSON representation of the enrollment token + * @return the parsed EnrollmentToken + * @throws IOException when failing to decode the serialized token + */ + public static EnrollmentToken decodeFromString(String encoded) throws IOException { + if (Strings.isNullOrEmpty(encoded)) { + throw new IOException("Cannot decode enrollment token from an empty string"); + } + final XContentParser jsonParser = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + Base64.getDecoder().decode(encoded) + ); + return EnrollmentToken.PARSER.parse(jsonParser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EnrollmentToken that = (EnrollmentToken) o; + return apiKey.equals(that.apiKey) && fingerprint.equals(that.fingerprint) && version.equals(that.version) && boundAddress.equals( + that.boundAddress); + } + + @Override + public int hashCode() { + return Objects.hash(apiKey, fingerprint, version, boundAddress); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java index 0a9416f507de8..a571d2fea6d00 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java @@ -6,7 +6,9 @@ */ package org.elasticsearch.xpack.security.tool; +import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.io.Streams; +import org.elasticsearch.core.CharArrays; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.Strings; @@ -26,6 +28,9 @@ import org.elasticsearch.xpack.security.tool.HttpResponse.HttpResponseBuilder; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -34,9 +39,16 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.AccessController; -import java.security.PrivilegedAction; +import java.security.MessageDigest; +import java.security.PrivilegedExceptionAction; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; @@ -60,9 +72,16 @@ public class CommandLineHttpClient { private static final int READ_TIMEOUT = 35 * 1000; private final Environment env; + private final String pinnedCaCertFingerprint; public CommandLineHttpClient(Environment env) { this.env = env; + this.pinnedCaCertFingerprint = null; + } + + public CommandLineHttpClient(Environment env, String pinnedCaCertFingerprint) { + this.env = env; + this.pinnedCaCertFingerprint = pinnedCaCertFingerprint; } /** @@ -79,22 +98,55 @@ public CommandLineHttpClient(Environment env) { * handler of the response Input Stream. * @return HTTP protocol response code. */ - @SuppressForbidden(reason = "We call connect in doPrivileged and provide SocketPermission") public HttpResponse execute(String method, URL url, String user, SecureString password, CheckedSupplier requestBodySupplier, CheckedFunction responseHandler) throws Exception { + + final String authorizationHeader = UsernamePasswordToken.basicAuthHeaderValue(user, password); + return execute(method, url, authorizationHeader, requestBodySupplier, responseHandler); + } + + /** + * General purpose HTTP(S) call with JSON Content-Type and Authorization Header. + * SSL settings are read from the settings file, if any. + * + * @param apiKey + * API key value to be used in the Authorization header + * @param requestBodySupplier + * supplier for the JSON string body of the request. + * @param responseHandler + * handler of the response Input Stream. + * @return HTTP protocol response code. + */ + public HttpResponse execute(String method, URL url, SecureString apiKey, + CheckedSupplier requestBodySupplier, + CheckedFunction responseHandler) throws Exception { + final String authorizationHeaderValue = apiKeyHeaderValue(apiKey); + return execute(method, url, authorizationHeaderValue, requestBodySupplier, responseHandler); + } + + @SuppressForbidden(reason = "We call connect in doPrivileged and provide SocketPermission") + private HttpResponse execute(String method, URL url, String authorizationHeader, + CheckedSupplier requestBodySupplier, + CheckedFunction responseHandler) throws Exception { final HttpURLConnection conn; // If using SSL, need a custom service because it's likely a self-signed certificate if ("https".equalsIgnoreCase(url.getProtocol())) { final SSLService sslService = new SSLService(env); final HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection(); - AccessController.doPrivileged((PrivilegedAction) () -> { - final SslConfiguration sslConfiguration = sslService.getHttpTransportSSLConfiguration(); - // Requires permission java.lang.RuntimePermission "setFactory"; - httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslConfiguration)); - final boolean isHostnameVerificationEnabled = sslConfiguration.getVerificationMode().isHostnameVerificationEnabled(); - if (isHostnameVerificationEnabled == false) { - httpsConn.setHostnameVerifier((hostname, session) -> true); + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + if (pinnedCaCertFingerprint != null) { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { fingerprintTrustingTrustManager(pinnedCaCertFingerprint) }, null); + httpsConn.setSSLSocketFactory(sslContext.getSocketFactory()); + } else { + final SslConfiguration sslConfiguration = sslService.getHttpTransportSSLConfiguration(); + // Requires permission java.lang.RuntimePermission "setFactory"; + httpsConn.setSSLSocketFactory(sslService.sslSocketFactory(sslConfiguration)); + final boolean isHostnameVerificationEnabled = sslConfiguration.getVerificationMode().isHostnameVerificationEnabled(); + if (isHostnameVerificationEnabled == false) { + httpsConn.setHostnameVerifier((hostname, session) -> true); + } } return null; }); @@ -105,8 +157,7 @@ public HttpResponse execute(String method, URL url, String user, SecureString pa conn.setRequestMethod(method); conn.setReadTimeout(READ_TIMEOUT); // Add basic-auth header - String token = UsernamePasswordToken.basicAuthHeaderValue(user, password); - conn.setRequestProperty("Authorization", token); + conn.setRequestProperty("Authorization", authorizationHeader); conn.setRequestProperty("Content-Type", XContentType.JSON.mediaType()); String bodyString = requestBodySupplier.get(); conn.setDoOutput(bodyString != null); // set true if we are sending a body @@ -253,4 +304,48 @@ public static HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) t public static URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException { return new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query); } + + public static String apiKeyHeaderValue(SecureString apiKey) { + CharBuffer chars = CharBuffer.allocate(apiKey.length()); + byte[] charBytes = null; + try { + chars.put(apiKey.getChars()); + charBytes = CharArrays.toUtf8Bytes(chars.array()); + + //TODO we still have passwords in Strings in headers. Maybe we can look into using a CharSequence? + String apiKeyToken = Base64.getEncoder().encodeToString(charBytes); + return "ApiKey " + apiKeyToken; + } finally { + Arrays.fill(chars.array(), (char) 0); + if (charBytes != null) { + Arrays.fill(charBytes, (byte) 0); + } + } + } + + /** + * Returns a TrustManager to be used in a client SSLContext, which trusts all certificates that are signed + * by a specific CA certificate ( identified by its SHA256 fingerprint, {@code pinnedCaCertFingerPrint} ) + */ + private TrustManager fingerprintTrustingTrustManager(String pinnedCaCertFingerprint) { + final TrustManager trustManager = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + final Certificate caCertFromChain = chain[chain.length-1]; + MessageDigest sha256 = MessageDigests.sha256(); + sha256.update(caCertFromChain.getEncoded()); + if (MessageDigests.toHexString(sha256.digest()).equals(pinnedCaCertFingerprint) == false ) { + throw new CertificateException(); + } + } + + @Override public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + return trustManager; + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java index df3773e99bef8..e0b74c1e7e8a4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java @@ -18,17 +18,12 @@ import java.util.Map; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.equalTo; + public class EnrollmentTokenTests extends ESTestCase { - EnrollmentToken createEnrollmentToken() { - final String apiKey = randomAlphaOfLength(16); - final String fingerprint = randomAlphaOfLength(64); - final String version = randomAlphaOfLength(5); - final List boundAddresses = Arrays.asList(generateRandomStringArray(4, randomIntBetween(2, 32), false)); - return new EnrollmentToken(apiKey, fingerprint, version, boundAddresses); - } public void testEnrollmentToken() throws Exception { - EnrollmentToken enrollmentToken = createEnrollmentToken(); + final EnrollmentToken enrollmentToken = createEnrollmentToken(); final String apiKey = enrollmentToken.getApiKey(); final String fingerprint = enrollmentToken.getFingerprint(); final String version = enrollmentToken.getVersion(); @@ -48,4 +43,18 @@ public void testEnrollmentToken() throws Exception { assertEquals(enrollmentMap.get("adr"), "[" + boundAddresses.stream().collect(Collectors.joining(", ")) + "]"); assertEquals(new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8), jsonString); } + + public void testDeserialization() throws Exception { + final EnrollmentToken enrollmentToken = createEnrollmentToken(); + final EnrollmentToken deserialized = EnrollmentToken.decodeFromString(enrollmentToken.getEncoded()); + assertThat(enrollmentToken, equalTo(deserialized)); + } + + private EnrollmentToken createEnrollmentToken() { + final String apiKey = randomAlphaOfLength(16); + final String fingerprint = randomAlphaOfLength(64); + final String version = randomAlphaOfLength(5); + final List boundAddresses = Arrays.asList(generateRandomStringArray(4, randomIntBetween(2, 32), false)); + return new EnrollmentToken(apiKey, fingerprint, version, boundAddresses); + } } From 3942411233611271bc77f3a6604792f0fb1f16f1 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 3 Sep 2021 17:55:09 +0300 Subject: [PATCH 02/12] Error handling and try all addresses in the enrollment token --- .../security/cli/ConfigAdditionalNodes.java | 157 +++++++++++------- 1 file changed, 101 insertions(+), 56 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java index 9aeaf084ed322..915576f5a38a5 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java @@ -81,7 +81,8 @@ public class ConfigAdditionalNodes extends KeyStoreAwareCommand { private final OptionSpec enrollmentTokenParam = parser.accepts("enrollment-token", "The enrollment token to use") - .withRequiredArg().required(); + .withRequiredArg() + .required(); private final BiFunction clientFunction; private static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_node_"; @@ -108,43 +109,34 @@ public static void main(String[] args) throws Exception { @Override protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { - final Path ymlPath = env.configFile().resolve("elasticsearch.yml"); - final Path keystorePath = KeyStoreWrapper.keystorePath(env.configFile()); - if (false == Files.exists(ymlPath) || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS)) { - terminal.println( - Terminal.Verbosity.NORMAL, - String.format( - Locale.ROOT, - "Skipping security auto configuration because the configuration file [%s] is missing or is not a regular file", - ymlPath - ) + if (Files.isDirectory(env.dataFile()) && Files.list(env.dataFile()).findAny().isPresent()) { + throw new UserException( + ExitCodes.CONFIG, + "Aborting enrolling to cluster. It appears that this is not the first time this node starts." ); - throw new UserException(ExitCodes.CONFIG, null); } - if (false == Files.isReadable(ymlPath)) { - terminal.println( - Terminal.Verbosity.NORMAL, + final Path ymlPath = env.configFile().resolve("elasticsearch.yml"); + final Path keystorePath = KeyStoreWrapper.keystorePath(env.configFile()); + if (false == Files.exists(ymlPath) + || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS) + || false == Files.isReadable(ymlPath)) { + throw new UserException( + ExitCodes.CONFIG, String.format( Locale.ROOT, - "Skipping security auto configuration because the configuration file [%s] is not readable", + "Aborting enrolling to cluster. The configuration file [%s] is not a readable regular file", ymlPath ) ); - throw new UserException(ExitCodes.CONFIG, null); } if (Files.exists(keystorePath) && (false == Files.isRegularFile(keystorePath, LinkOption.NOFOLLOW_LINKS) || false == Files.isReadable(keystorePath))) { - terminal.println( - Terminal.Verbosity.NORMAL, - String.format( - Locale.ROOT, - "Skipping security auto configuration because the node keystore file [%s] is not a readable regular file", - keystorePath - ) + throw new UserException( + ExitCodes.CONFIG, + String.format(Locale.ROOT, "Aborting enrolling to cluster. The keystore [%s] is not a readable regular file", ymlPath) ); - throw new UserException(ExitCodes.CONFIG, null); } final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC); @@ -165,43 +157,63 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } catch (Exception ex) { e.addSuppressed(ex); } - throw new UserException(ExitCodes.CANT_CREATE, "Could not create auto configuration directory", e); + throw new UserException( + ExitCodes.CANT_CREATE, + "Aborting enrolling to cluster. Could not create auto configuration directory", + e + ); } final UserPrincipal newFileOwner = Files.getOwner(instantAutoConfigDir, LinkOption.NOFOLLOW_LINKS); if (false == newFileOwner.equals(Files.getOwner(env.configFile(), LinkOption.NOFOLLOW_LINKS))) { Files.deleteIfExists(instantAutoConfigDir); - throw new UserException(ExitCodes.CONFIG, "Aborting auto configuration because of config dir ownership mismatch"); + throw new UserException(ExitCodes.CONFIG, "Aborting enrolling to cluster. config dir ownership mismatch"); } final EnrollmentToken enrollmentToken; try { enrollmentToken = EnrollmentToken.decodeFromString(enrollmentTokenParam.value(options)); } catch (IOException e) { - throw new UserException(ExitCodes.IO_ERROR, "Invalid enrollment token"); + try { + Files.deleteIfExists(instantAutoConfigDir); + } catch (Exception ex) { + e.addSuppressed(ex); + } + throw new UserException(ExitCodes.USAGE, "Aborting enrolling to cluster. Invalid enrollment token", e); } final CommandLineHttpClient client = clientFunction.apply(env, enrollmentToken.getFingerprint()); // We don't wait for cluster health here. If the user has a token, it means that at least the first node has started // successfully so we expect the cluster to be healthy already. If not, this is a sign of a problem and we should bail. - final URL enrollNodeUrl = createURL(new URL("https://" + enrollmentToken.getBoundAddress().get(0)), "/_security/enroll/node", ""); - final HttpResponse enrollResponse = client.execute( - "GET", - enrollNodeUrl, - new SecureString(enrollmentToken.getApiKey().toCharArray()), - () -> null, - CommandLineHttpClient::responseBuilder - ); - if (enrollResponse.getHttpStatus() != 200) { + HttpResponse enrollResponse = null; + URL enrollNodeUrl = null; + for (String address: enrollmentToken.getBoundAddress()) { + enrollNodeUrl = createURL(new URL("https://" + address), "/_security/enroll/node", ""); + enrollResponse = client.execute("GET", + enrollNodeUrl, + new SecureString(enrollmentToken.getApiKey().toCharArray()), + () -> null, + CommandLineHttpClient::responseBuilder); + if (enrollResponse.getHttpStatus() == 200 ){ + break; + } + } + if (null == enrollResponse) { throw new UserException( ExitCodes.UNAVAILABLE, - "Unexpected HTTP status [" + enrollResponse.getHttpStatus() + "] calling the enroll node API (" + enrollNodeUrl + ")" + "Aborting enrolling to cluster. " + + "Could not communicate with the initial node in any of the addresses from the enrollment token. All of " + + enrollmentToken.getBoundAddress() + + "where attempted." ); } final Map responseMap = enrollResponse.getResponseBody(); if (responseMap == null) { - throw new UserException(ExitCodes.DATA_ERROR, "Empty response when calling the enroll node API (" + enrollNodeUrl + ")"); + throw new UserException( + ExitCodes.DATA_ERROR, + "Aborting enrolling to cluster. Empty response when calling the enroll node API (" + enrollNodeUrl + ")" + ); } final String httpCaKeyPem = (String) responseMap.get("http_ca_key"); final String httpCaCertPem = (String) responseMap.get("http_ca_cert"); @@ -214,7 +226,11 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th || Strings.isNullOrEmpty(transportKeyPem) || Strings.isNullOrEmpty(transportCertPem) || null == transportAddresses) { - throw new UserException(ExitCodes.DATA_ERROR, "Invalid response when calling the enroll node API (" + enrollNodeUrl + ")"); + Files.deleteIfExists(instantAutoConfigDir); + throw new UserException( + ExitCodes.DATA_ERROR, + "Aborting enrolling to cluster. Invalid response when calling the enroll node API (" + enrollNodeUrl + ")" + ); } final Tuple httpCa = parseKeyCertFromPem(httpCaKeyPem, httpCaCertPem); @@ -228,17 +244,33 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th // this does DNS resolve and could block final GeneralNames subjectAltNames = getSubjectAltNames(); - final KeyPair nodeHttpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE); - final X509Certificate nodeHttpCert = CertGenUtils.generateSignedCertificate( - certificatePrincipal, - subjectAltNames, - nodeHttpKeyPair, - httpCaCert, - httpCaKey, - false, - HTTP_CERTIFICATE_DAYS, - null - ); + final KeyPair nodeHttpKeyPair; + final X509Certificate nodeHttpCert; + + try { + nodeHttpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE); + nodeHttpCert = CertGenUtils.generateSignedCertificate( + certificatePrincipal, + subjectAltNames, + nodeHttpKeyPair, + httpCaCert, + httpCaKey, + false, + HTTP_CERTIFICATE_DAYS, + null + ); + } catch (Exception e) { + try { + Files.deleteIfExists(instantAutoConfigDir); + } catch (Exception ex) { + e.addSuppressed(ex); + } + throw new UserException( + ExitCodes.IO_ERROR, + "Aborting enrolling to cluster. Failed to generate necessary key and certificate material", + e + ); + } // save original keystore before updating (replacing) final Path keystoreBackupPath = env.configFile() @@ -252,7 +284,11 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } catch (Exception ex) { e.addSuppressed(ex); } - throw e; + throw new UserException( + ExitCodes.IO_ERROR, + "Aborting enrolling to cluster. Could not create backup of existing keystore file", + e + ); } } @@ -341,7 +377,11 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } catch (Exception ex) { e.addSuppressed(ex); } - throw e; + throw new UserException( + ExitCodes.IO_ERROR, + "Aborting enrolling to cluster. Could not store necessary key and certificates.", + e + ); } finally { if (nodeKeystorePassword.get() != null) { nodeKeystorePassword.get().close(); @@ -463,7 +503,11 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } catch (Exception ex) { e.addSuppressed(ex); } - throw e; + throw new UserException( + ExitCodes.IO_ERROR, + "Aborting enrolling to cluster. Could not persist configuration in elasticsearch.yml", + e + ); } // only delete the backed up file if all went well Files.deleteIfExists(keystoreBackupPath); @@ -477,7 +521,7 @@ private static void fullyWriteFile(Path basePath, String fileName, boolean repla 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) + String.format(Locale.ROOT, "Output file [%s] already exists and will not be replaced", filePath) ); } // the default permission @@ -557,7 +601,8 @@ private Tuple parseKeyCertFromPem(String pemFormatt } catch (Exception e) { throw new UserException( ExitCodes.DATA_ERROR, - "Failed to parse Private Key and Certificate from the response of the Enroll Node API" + "Aborting enrolling to cluster. Failed to parse Private Key and Certificate from the response of the Enroll Node API", + e ); } } From b800f7958dc3c21f4d4d7ffa59262045873c81b3 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Sun, 5 Sep 2021 01:17:37 +0300 Subject: [PATCH 03/12] Add functionality to enroll to cluster This introduces a new named argument (--enrollment-token) that can be passed to the elasticsearch executables when starting a node. It takes an Enrollment Token as a value and using the information in it, it Elasticsearch will attempt to - Communicate with an existing node of the cluster - Receive necessary key/certificate material - Persist said material and configuration before the node is actually started. This can only happen if this is the first time the current node starts and if it doesn't have any explicit security related configuration defined. --- distribution/src/bin/elasticsearch | 16 + distribution/src/bin/elasticsearch.bat | 13 + .../core/security}/CommandLineHttpClient.java | 4 +- .../xpack/core/security}/EnrollmentToken.java | 2 +- .../xpack/core/security}/HttpResponse.java | 2 +- .../plugin-metadata/plugin-security.policy | 2 + .../core/security}/EnrollmentTokenTests.java | 3 +- x-pack/plugin/security/cli/build.gradle | 1 - .../xpack/security/cli/CertificateTool.java | 1 - ...nalNodes.java => EnrollNodeToCluster.java} | 78 +++-- .../cli/ConfigAdditionalNodesTests.java | 86 ----- .../cli/EnrollNodeToClusterTests.java | 314 ++++++++++++++++++ .../xpack/security/cli/http_ca.crt | 33 ++ .../xpack/security/cli/http_ca.key | 52 +++ .../xpack/security/cli/transport.crt | 33 ++ .../xpack/security/cli/transport.key | 52 +++ .../tool/ResetElasticPasswordTool.java | 6 +- .../esnative/tool/SetupPasswordTool.java | 6 +- .../enrollment/EnrollmentTokenGenerator.java | 5 +- ...swordAndEnrollmentTokenForInitialNode.java | 8 +- .../tool/CreateEnrollmentTokenTool.java | 2 +- .../tool/BaseRunAsSuperuserCommand.java | 2 + .../tool/CommandLineHttpClientTests.java | 31 +- .../esnative/tool/SetupPasswordToolTests.java | 6 +- .../EnrollmentTokenGeneratorTests.java | 4 +- ...AndEnrollmentTokenForInitialNodeTests.java | 6 +- .../xpack/security/authc/esnative/tool/ca.crt | 20 ++ .../xpack/security/authc/esnative/tool/ca.key | 27 ++ .../security/authc/esnative/tool/http.crt | 42 +++ .../security/authc/esnative/tool/http.key | 27 ++ .../tool/ResetElasticPasswordToolTests.java | 4 +- .../tool/BaseRunAsSuperuserCommandTests.java | 4 +- .../tool/CreateEnrollmentTokenToolTests.java | 6 +- 33 files changed, 752 insertions(+), 146 deletions(-) rename x-pack/plugin/{security/src/main/java/org/elasticsearch/xpack/security/tool => core/src/main/java/org/elasticsearch/xpack/core/security}/CommandLineHttpClient.java (99%) rename x-pack/plugin/{security/src/main/java/org/elasticsearch/xpack/security/enrollment => core/src/main/java/org/elasticsearch/xpack/core/security}/EnrollmentToken.java (98%) rename x-pack/plugin/{security/src/main/java/org/elasticsearch/xpack/security/tool => core/src/main/java/org/elasticsearch/xpack/core/security}/HttpResponse.java (97%) rename x-pack/plugin/{security/src/test/java/org/elasticsearch/xpack/security/enrollment => core/src/test/java/org/elasticsearch/xpack/core/security}/EnrollmentTokenTests.java (96%) rename x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/{ConfigAdditionalNodes.java => EnrollNodeToCluster.java} (89%) delete mode 100644 x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java create mode 100644 x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/EnrollNodeToClusterTests.java create mode 100644 x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.crt create mode 100644 x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.key create mode 100644 x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.crt create mode 100644 x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.key create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.key create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.key diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index c5805ea2ebd64..01fc5cac512d1 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -17,6 +17,7 @@ source "`dirname "$0"`"/elasticsearch-env CHECK_KEYSTORE=true DAEMONIZE=false +ENROLL_TO_CLUSTER=false for option in "$@"; do case "$option" in -h|--help|-V|--version) @@ -27,6 +28,13 @@ for option in "$@"; do ;; esac done +while [ $# -gt 0 ]; do + if [[ $1 == "--enrollment-token" ]]; then + ENROLL_TO_CLUSTER=true + ENROLLMENT_TOKEN="$2" + fi + shift +done if [ -z "$ES_TMPDIR" ]; then ES_TMPDIR=`"$JAVA" "$XSHARE" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory` @@ -45,6 +53,14 @@ then fi fi +if [[ $ENROLL_TO_CLUSTER = true ]]; then + ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster \ + ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \ + ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \ + "`dirname "$0"`"/elasticsearch-cli \ + --enrollment-token "$ENROLLMENT_TOKEN" +fi + # The JVM options parser produces the final JVM options to start Elasticsearch. # It does this by incorporating JVM options in the following way: # - first, system JVM options are applied (these are hardcoded options in the diff --git a/distribution/src/bin/elasticsearch.bat b/distribution/src/bin/elasticsearch.bat index 7d4d58010ba33..b5c4efcfec09c 100644 --- a/distribution/src/bin/elasticsearch.bat +++ b/distribution/src/bin/elasticsearch.bat @@ -5,6 +5,7 @@ setlocal enableextensions SET params='%*' SET checkpassword=Y +SET enrolltocluster=N :loop FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO ( @@ -33,6 +34,11 @@ FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO ( SET checkpassword=N ) + IF "!current!" == "--enrollment-token" ( + SHIFT + SET enrollmenttoken=%~1 + ) + IF "!silent!" == "Y" ( SET nopauseonerror=Y ) ELSE ( @@ -68,6 +74,13 @@ IF "%checkpassword%"=="Y" ( ) ) +set ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster +set ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env +set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli +call "%~dp0elasticsearch-cli.bat" --enrollment-token %enrollmenttoken% + || goto exit + + if not defined ES_TMPDIR ( for /f "tokens=* usebackq" %%a in (`CALL %JAVA% -cp "!ES_CLASSPATH!" "org.elasticsearch.tools.launchers.TempDirectory"`) do set ES_TMPDIR=%%a ) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java similarity index 99% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java index a571d2fea6d00..568bca46ed83e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/CommandLineHttpClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.security.tool; +package org.elasticsearch.xpack.core.security; import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.io.Streams; @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.common.socket.SocketAccess; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.security.tool.HttpResponse.HttpResponseBuilder; +import org.elasticsearch.xpack.core.security.HttpResponse.HttpResponseBuilder; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java similarity index 98% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java index 50cbb74a65b93..ed482ded030cf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.security.enrollment; +package org.elasticsearch.xpack.core.security; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/HttpResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/HttpResponse.java similarity index 97% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/HttpResponse.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/HttpResponse.java index b44201aadc0ec..f102b1ce137ed 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/HttpResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/HttpResponse.java @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -package org.elasticsearch.xpack.security.tool; +package org.elasticsearch.xpack.core.security; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.xcontent.XContentHelper; diff --git a/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy b/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy index ce1c351a05b49..753667c37cd95 100644 --- a/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy +++ b/x-pack/plugin/core/src/main/plugin-metadata/plugin-security.policy @@ -1,4 +1,6 @@ grant { + // CommandLineHttpClient + permission java.lang.RuntimePermission "setFactory"; // bouncy castle permission java.security.SecurityPermission "putProviderProperty.BC"; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/EnrollmentTokenTests.java similarity index 96% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/EnrollmentTokenTests.java index e0b74c1e7e8a4..43d13ce55f300 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/EnrollmentTokenTests.java @@ -5,11 +5,12 @@ * 2.0. */ -package org.elasticsearch.xpack.security.enrollment; +package org.elasticsearch.xpack.core.security; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.EnrollmentToken; import java.nio.charset.StandardCharsets; import java.util.Arrays; diff --git a/x-pack/plugin/security/cli/build.gradle b/x-pack/plugin/security/cli/build.gradle index c1bb37b6f3115..738dcaf338de4 100644 --- a/x-pack/plugin/security/cli/build.gradle +++ b/x-pack/plugin/security/cli/build.gradle @@ -8,7 +8,6 @@ archivesBaseName = 'elasticsearch-security-cli' dependencies { compileOnly project(":server") compileOnly project(path: xpackModule('core')) - compileOnly project(path: xpackModule('security')) api "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" api "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}" testImplementation("com.google.jimfs:jimfs:${versions.jimfs}") { diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index 228522afda970..212a7cbd76c73 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -34,7 +34,6 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.PathUtils; import org.elasticsearch.common.network.InetAddresses; -import org.elasticsearch.common.ssl.PemUtils; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/EnrollNodeToCluster.java similarity index 89% rename from x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java rename to x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/EnrollNodeToCluster.java index 915576f5a38a5..a237f0068fdee 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodes.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/EnrollNodeToCluster.java @@ -14,6 +14,7 @@ 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.ExitCodes; import org.elasticsearch.cli.KeyStoreAwareCommand; import org.elasticsearch.cli.SuppressForbidden; @@ -26,15 +27,16 @@ import org.elasticsearch.common.network.NetworkUtils; import org.elasticsearch.common.settings.KeyStoreWrapper; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.Tuple; import org.elasticsearch.env.Environment; import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; -import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.EnrollmentToken; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import javax.security.auth.x500.X500Principal; import java.io.BufferedWriter; @@ -72,38 +74,38 @@ import static org.elasticsearch.common.ssl.PemUtils.parsePKCS8PemString; import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING; -import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildDnFromDomain; -import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; +import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL; /** * Configures a node to join an existing cluster with security features enabled. */ -public class ConfigAdditionalNodes extends KeyStoreAwareCommand { +public class EnrollNodeToCluster extends KeyStoreAwareCommand { private final OptionSpec enrollmentTokenParam = parser.accepts("enrollment-token", "The enrollment token to use") .withRequiredArg() .required(); private final BiFunction clientFunction; - private static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_node_"; - private static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node"; - private static final String TRANSPORT_AUTOGENERATED_KEYSTORE_NAME = "transport_keystore_all_nodes"; - private static final String TRANSPORT_AUTOGENERATED_KEY_ALIAS = "transport_all_nodes_key"; - private static final String TRANSPORT_AUTOGENERATED_CERT_ALIAS = "transport_all_nodes_cert"; + static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_node_"; + static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node"; + static final String HTTP_AUTOGENERATED_CA_NAME = "http_ca"; + static final String TRANSPORT_AUTOGENERATED_KEYSTORE_NAME = "transport_keystore_all_nodes"; + static final String TRANSPORT_AUTOGENERATED_KEY_ALIAS = "transport_all_nodes_key"; + static final String TRANSPORT_AUTOGENERATED_CERT_ALIAS = "transport_all_nodes_cert"; private static final int HTTP_CERTIFICATE_DAYS = 2 * 365; private static final int HTTP_KEY_SIZE = 4096; - public ConfigAdditionalNodes(BiFunction clientFunction) { + public EnrollNodeToCluster(BiFunction clientFunction) { super("Configures security so that this node can join an existing cluster"); this.clientFunction = clientFunction; } - public ConfigAdditionalNodes() { + public EnrollNodeToCluster() { this(CommandLineHttpClient::new); } public static void main(String[] args) throws Exception { - exit(new ConfigAdditionalNodes().main(args, Terminal.DEFAULT)); + exit(new EnrollNodeToCluster().main(args, Terminal.DEFAULT)); } @Override @@ -139,6 +141,8 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th ); } + checkExistingConfiguration(env.settings()); + final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC); final String instantAutoConfigName = TLS_CONFIG_DIR_NAME_PREFIX + autoConfigDate.toInstant().getEpochSecond(); final Path instantAutoConfigDir = env.configFile().resolve(instantAutoConfigName); @@ -173,13 +177,13 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th final EnrollmentToken enrollmentToken; try { enrollmentToken = EnrollmentToken.decodeFromString(enrollmentTokenParam.value(options)); - } catch (IOException e) { + } catch (Exception e) { try { Files.deleteIfExists(instantAutoConfigDir); } catch (Exception ex) { e.addSuppressed(ex); } - throw new UserException(ExitCodes.USAGE, "Aborting enrolling to cluster. Invalid enrollment token", e); + throw new UserException(ExitCodes.DATA_ERROR, "Aborting enrolling to cluster. Invalid enrollment token", e); } final CommandLineHttpClient client = clientFunction.apply(env, enrollmentToken.getFingerprint()); @@ -199,7 +203,8 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th break; } } - if (null == enrollResponse) { + if (enrollResponse == null || enrollResponse.getHttpStatus() != 200) { + Files.deleteIfExists(instantAutoConfigDir); throw new UserException( ExitCodes.UNAVAILABLE, "Aborting enrolling to cluster. " + @@ -210,6 +215,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th } final Map responseMap = enrollResponse.getResponseBody(); if (responseMap == null) { + Files.deleteIfExists(instantAutoConfigDir); throw new UserException( ExitCodes.DATA_ERROR, "Aborting enrolling to cluster. Empty response when calling the enroll node API (" + enrollNodeUrl + ")" @@ -240,7 +246,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th final PrivateKey transportKey = transport.v1(); final X509Certificate transportCert = transport.v2(); - final X500Principal certificatePrincipal = new X500Principal(buildDnFromDomain(System.getenv("HOSTNAME"))); + final X500Principal certificatePrincipal = new X500Principal("CN=Autogenerated by Elasticsearch"); // this does DNS resolve and could block final GeneralNames subjectAltNames = getSubjectAltNames(); @@ -272,6 +278,27 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th ); } + 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) { + try { + Files.deleteIfExists(instantAutoConfigDir); + } catch (Exception ex) { + e.addSuppressed(ex); + } + throw new UserException( + ExitCodes.IO_ERROR, + "Aborting enrolling to cluster. Could not store necessary key and certificates.", + e + ); + } + // save original keystore before updating (replacing) final Path keystoreBackupPath = env.configFile() .resolve(KeyStoreWrapper.KEYSTORE_FILENAME + "." + autoConfigDate.toInstant().getEpochSecond() + ".orig"); @@ -304,7 +331,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th || nodeKeystore.getSettingNames().contains("xpack.security.http.ssl.keystore.secure_password")) { throw new UserException( ExitCodes.CONFIG, - "Aborting auto configuration because the node keystore contains password and settings already" + "Aborting enrolling to cluster. The node keystore contains TLS related settings already." ); } try (SecureString httpKeystorePassword = newKeystorePassword()) { @@ -607,4 +634,17 @@ private Tuple parseKeyCertFromPem(String pemFormatt } } + void checkExistingConfiguration(Settings settings) throws UserException { + if (XPackSettings.SECURITY_ENABLED.exists(settings)) { + throw new UserException(ExitCodes.CONFIG, "Aborting enrolling to cluster. It appears that security is already configured."); + } + if (XPackSettings.ENROLLMENT_ENABLED.exists(settings) && false == XPackSettings.ENROLLMENT_ENABLED.get(settings)) { + throw new UserException(ExitCodes.CONFIG, "Aborting enrolling to cluster. Enrollment is explicitly disabled."); + } + if (false == settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX).isEmpty() || + false == settings.getByPrefix(XPackSettings.HTTP_SSL_PREFIX).isEmpty()) { + throw new UserException(ExitCodes.CONFIG, "Aborting enrolling to cluster. It appears that TLS is already configured."); + } + } + } diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java deleted file mode 100644 index 01f83090804b8..0000000000000 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/ConfigAdditionalNodesTests.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 com.google.common.jimfs.Configuration; -import com.google.common.jimfs.Jimfs; - -import org.elasticsearch.common.CheckedSupplier; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.core.CheckedFunction; -import org.elasticsearch.core.PathUtilsForTesting; -import org.elasticsearch.core.internal.io.IOUtils; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class ConfigAdditionalNodesTests extends ESTestCase { - - static FileSystem jimfs; - private Path confDir; - private CommandLineHttpClient client; - - @BeforeClass - public static void setupJimfs() { - Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix").build(); - jimfs = Jimfs.newFileSystem(conf); - PathUtilsForTesting.installMock(jimfs); - } - - @Before - public void setup() throws Exception { - Path homeDir = jimfs.getPath("eshome"); - IOUtils.rm(homeDir); - confDir = homeDir.resolve("config"); - Files.createDirectories(confDir); - - HttpResponse nodeEnrollResponse = new HttpResponse( - HttpURLConnection.HTTP_OK, - Map.of("status", randomFrom("yellow", "green")) - ); - this.client = mock(CommandLineHttpClient.class); - when( - client.execute( - anyString(), - any(URL.class), - anyString(), - any(SecureString.class), - any(CheckedSupplier.class), - any(CheckedFunction.class) - ) - ).thenReturn(nodeEnrollResponse); - - } - - @AfterClass - public static void closeJimfs() throws IOException { - if (jimfs != null) { - jimfs.close(); - jimfs = null; - } - } - - -} diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/EnrollNodeToClusterTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/EnrollNodeToClusterTests.java new file mode 100644 index 0000000000000..378256ce8314a --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/EnrollNodeToClusterTests.java @@ -0,0 +1,314 @@ +/* + * 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 com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; + +import org.elasticsearch.Version; +import org.elasticsearch.cli.Command; +import org.elasticsearch.cli.CommandTestCase; +import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.CheckedSupplier; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.PemUtils; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.PathUtilsForTesting; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.env.Environment; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.EnrollmentToken; +import org.elasticsearch.xpack.core.security.HttpResponse; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.HTTP_AUTOGENERATED_CA_NAME; +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.HTTP_AUTOGENERATED_KEYSTORE_NAME; +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.TLS_CONFIG_DIR_NAME_PREFIX; +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.TRANSPORT_AUTOGENERATED_CERT_ALIAS; +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.TRANSPORT_AUTOGENERATED_KEYSTORE_NAME; +import static org.elasticsearch.xpack.security.cli.EnrollNodeToCluster.TRANSPORT_AUTOGENERATED_KEY_ALIAS; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class EnrollNodeToClusterTests extends CommandTestCase { + + static FileSystem jimfs; + private Path confDir; + private CommandLineHttpClient client; + static String keystoresPassword; + Settings settings; + + @Override + protected Command newCommand() { + return new EnrollNodeToCluster(((environment, pinnedCaCertFingerprint) -> client)) { + @Override + protected Environment createEnv(Map settings) { + return new Environment(EnrollNodeToClusterTests.this.settings, confDir); + } + + @Override + SecureString newKeystorePassword() { + return new SecureString(keystoresPassword.toCharArray()); + } + }; + } + + @BeforeClass + public static void setupJimfs() { + Configuration conf = Configuration.unix().toBuilder().setAttributeViews("posix", "owner").build(); + jimfs = Jimfs.newFileSystem(conf); + PathUtilsForTesting.installMock(jimfs); + } + + @Before + @SuppressWarnings("unchecked") + @SuppressForbidden(reason = "Cannot use getDataPath() as Paths.get() throws UnsupportedOperationException for jimfs") + public void setup() throws Exception { + Path homeDir = jimfs.getPath("eshome"); + IOUtils.rm(homeDir); + confDir = homeDir.resolve("config"); + Files.createDirectories(confDir); + settings = Settings.builder().put("path.home", homeDir).build(); + Files.createFile(confDir.resolve("elasticsearch.yml")); + String httpCaCertPemString = Files.readAllLines( + Paths.get(getClass().getResource("http_ca.crt").toURI()).toAbsolutePath().normalize() + ).stream().filter(l -> l.contains("-----") == false).collect(Collectors.joining()); + String httpCaKeyPemString = Files.readAllLines( + Paths.get(getClass().getResource("http_ca.key").toURI()).toAbsolutePath().normalize() + ).stream().filter(l -> l.contains("-----") == false).collect(Collectors.joining()); + String transportKeyPemString = Files.readAllLines( + Paths.get(getClass().getResource("transport.key").toURI()).toAbsolutePath().normalize() + ).stream().filter(l -> l.contains("-----") == false).collect(Collectors.joining()); + String transportCertPemString = Files.readAllLines( + Paths.get(getClass().getResource("transport.crt").toURI()).toAbsolutePath().normalize() + ).stream().filter(l -> l.contains("-----") == false).collect(Collectors.joining()); + + HttpResponse nodeEnrollResponse = new HttpResponse( + HttpURLConnection.HTTP_OK, + Map.of( + "http_ca_key", + httpCaKeyPemString, + "http_ca_cert", + httpCaCertPemString, + "transport_key", + transportKeyPemString, + "transport_cert", + transportCertPemString, + "nodes_addresses", + List.of("127.0.0.1:9300", "192.168.1.10:9301") + ) + ); + this.client = mock(CommandLineHttpClient.class); + when(client.execute(anyString(), any(URL.class), any(SecureString.class), any(CheckedSupplier.class), any(CheckedFunction.class))) + .thenReturn(nodeEnrollResponse); + keystoresPassword = randomAlphaOfLengthBetween(14, 18); + } + + @AfterClass + public static void closeJimfs() throws IOException { + if (jimfs != null) { + jimfs.close(); + jimfs = null; + } + } + + @SuppressForbidden(reason = "Cannot use getDataPath() as Paths.get() throws UnsupportedOperationException for jimfs") + public void testEnrollmentSuccess() throws Exception { + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + execute("--enrollment-token", enrollmentToken.getEncoded()); + final Path autoConfigDir = assertAutoConfigurationFilesCreated(); + assertTransportKeystore( + autoConfigDir.resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12"), + Paths.get(getClass().getResource("transport.key").toURI()).toAbsolutePath().normalize(), + Paths.get(getClass().getResource("transport.crt").toURI()).toAbsolutePath().normalize() + ); + assertHttpKeystore( + autoConfigDir.resolve(HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12"), + Paths.get(getClass().getResource("http_ca.key").toURI()).toAbsolutePath().normalize(), + Paths.get(getClass().getResource("http_ca.crt").toURI()).toAbsolutePath().normalize() + ); + } + + public void testEnrollmentExitsOnAlreadyConfiguredNode() throws Exception { + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + Path dataDir = Files.createDirectory(jimfs.getPath("eshome").resolve("data")); + Files.createFile(dataDir.resolve("foo")); + settings = Settings.builder().put(settings).put("path.data", dataDir).put("xpack.security.enrollment.enabled", true).build(); + UserException e = expectThrows(UserException.class, () -> execute("--enrollment-token", enrollmentToken.getEncoded())); + assertThat(e.getMessage(), equalTo("Aborting enrolling to cluster. It appears that this is not the first time this node starts.")); + assertAutoConfigurationFilesNotCreated(); + } + + public void testEnrollmentExitsOnInvalidEnrollmentToken() throws Exception { + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + + UserException e = expectThrows( + UserException.class, + () -> execute( + "--enrollment-token", + enrollmentToken.getEncoded().substring(0, enrollmentToken.getEncoded().length() - randomIntBetween(6, 12)) + ) + ); + assertThat(e.getMessage(), equalTo("Aborting enrolling to cluster. Invalid enrollment token")); + assertAutoConfigurationFilesNotCreated(); + } + + @SuppressWarnings("unchecked") + public void testEnrollmentExitsOnUnexpectedResponse() throws Exception { + when(client.execute(anyString(), any(URL.class), any(SecureString.class), any(CheckedSupplier.class), any(CheckedFunction.class))) + .thenReturn(new HttpResponse(randomFrom(401, 403, 500), Map.of())); + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + UserException e = expectThrows(UserException.class, () -> execute("--enrollment-token", enrollmentToken.getEncoded())); + assertThat( + e.getMessage(), + equalTo( + "Aborting enrolling to cluster. " + + "Could not communicate with the initial node in any of the addresses from the enrollment token. All of " + + enrollmentToken.getBoundAddress() + + "where attempted." + ) + ); + assertAutoConfigurationFilesNotCreated(); + } + + public void testEnrollmentExitsOnExistingSecurityConfiguration() throws Exception { + settings = Settings.builder().put(settings).put("xpack.security.enabled", true).build(); + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + UserException e = expectThrows(UserException.class, () -> execute("--enrollment-token", enrollmentToken.getEncoded())); + assertThat(e.getMessage(), equalTo("Aborting enrolling to cluster. It appears that security is already configured.")); + assertAutoConfigurationFilesNotCreated(); + } + + public void testEnrollmentExitsOnExistingTlsConfiguration() throws Exception { + settings = Settings.builder() + .put(settings) + .put("xpack.security.transport.ssl.enabled", true) + .put("xpack.security.http.ssl.enabled", true) + .build(); + final EnrollmentToken enrollmentToken = new EnrollmentToken( + randomAlphaOfLength(12), + randomAlphaOfLength(12), + Version.CURRENT.toString(), + List.of("127.0.0.1:9200") + ); + UserException e = expectThrows(UserException.class, () -> execute("--enrollment-token", enrollmentToken.getEncoded())); + assertThat(e.getMessage(), equalTo("Aborting enrolling to cluster. It appears that TLS is already configured.")); + assertAutoConfigurationFilesNotCreated(); + } + + private Path assertAutoConfigurationFilesCreated() throws Exception { + List f = Files.find( + confDir, + 2, + ((path, basicFileAttributes) -> Files.isDirectory(path) && path.getFileName().toString().startsWith(TLS_CONFIG_DIR_NAME_PREFIX)) + ).collect(Collectors.toList()); + assertThat(f.size(), equalTo(1)); + final Path autoConfigDir = f.get(0); + assertThat(Files.isRegularFile(autoConfigDir.resolve(HTTP_AUTOGENERATED_CA_NAME + ".crt")), is(true)); + assertThat(Files.isRegularFile(autoConfigDir.resolve(HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12")), is(true)); + assertThat(Files.isRegularFile(autoConfigDir.resolve(TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12")), is(true)); + + return autoConfigDir; + } + + private void assertAutoConfigurationFilesNotCreated() throws Exception { + List f = Files.find( + confDir, + 2, + ((path, basicFileAttributes) -> Files.isDirectory(path) && path.getFileName().toString().startsWith(TLS_CONFIG_DIR_NAME_PREFIX)) + ).collect(Collectors.toList()); + assertThat(f.size(), equalTo(0)); + } + + private void assertTransportKeystore(Path keystorePath, Path keyPath, Path certPath) throws Exception { + try (InputStream in = Files.newInputStream(keystorePath)) { + final KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(in, keystoresPassword.toCharArray()); + assertThat(keyStore.size(), equalTo(2)); + assertThat(keyStore.isKeyEntry(TRANSPORT_AUTOGENERATED_KEY_ALIAS), is(true)); + assertThat(keyStore.isCertificateEntry(TRANSPORT_AUTOGENERATED_CERT_ALIAS), is(true)); + assertThat( + keyStore.getKey(TRANSPORT_AUTOGENERATED_KEY_ALIAS, keystoresPassword.toCharArray()), + equalTo(PemUtils.readPrivateKey(keyPath, () -> null)) + ); + assertThat( + keyStore.getCertificate(TRANSPORT_AUTOGENERATED_CERT_ALIAS), + equalTo(CertParsingUtils.readX509Certificate(certPath)) + ); + assertThat(keyStore.getCertificate(TRANSPORT_AUTOGENERATED_KEY_ALIAS), equalTo(CertParsingUtils.readX509Certificate(certPath))); + } + } + + private void assertHttpKeystore(Path keystorePath, Path keyPath, Path certPath) throws Exception { + try (InputStream in = Files.newInputStream(keystorePath)) { + final KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(in, keystoresPassword.toCharArray()); + assertThat(keyStore.size(), equalTo(2)); + assertThat(keyStore.isKeyEntry(HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca"), is(true)); + assertThat(keyStore.isKeyEntry(HTTP_AUTOGENERATED_KEYSTORE_NAME), is(true)); + assertThat( + keyStore.getCertificate(HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca"), + equalTo(CertParsingUtils.readX509Certificate(certPath)) + ); + assertThat( + keyStore.getKey(HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca", keystoresPassword.toCharArray()), + equalTo(PemUtils.readPrivateKey(keyPath, () -> null)) + ); + keyStore.getCertificate(HTTP_AUTOGENERATED_KEYSTORE_NAME).verify(CertParsingUtils.readX509Certificate(certPath).getPublicKey()); + // Certificate#verify didn't throw + } + } + +} diff --git a/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.crt b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.crt new file mode 100644 index 0000000000000..261ef84c1901a --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFvzCCA6egAwIBAgIUe1O6tJamvnXJHOyni1Lq7MHP+acwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEoMCYGA1UEAwwfRWxhc3RpY3NlYXJj +aCBUZXN0IENhc2UgSFRUUCBDQTAeFw0yMTA5MDQxMjU5MTRaFw0yMjA5MDQxMjU5 +MTRaMG8xCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxKDAmBgNVBAMMH0VsYXN0aWNzZWFy +Y2ggVGVzdCBDYXNlIEhUVFAgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDZ3Iln8S81Y80z4DKVo8+UbB8hBwHSr17BE/YuiIFthoCne9T21Kg1h48v +CqAYH8gSZHiQYUVwqbuoLbdoHvPEDmqPPAJGbnyTHum1O1aEEduALShfsFUZ5PQH +8Hx4ycqLV/63iIYvb2If0pAEyzZ95qzn7SF7F2Qus1L20hmU9ZVA+ogcsOIui5OT +21fNrUcKxAGM1KuZx0pK0aBG5mMLLJ5VbO/rSuetc/9EbKSnmW/LvDm1arBoEI9s +tMBA6fcLKK95JV7n5eWyCyLBjdGCWNZNn172BmxY4FS4dKMLtKoKUTaWrPgJgrvr +p+G7e+9o9f3eBuP4+ja6pjTlxcMR+pzF2TWGZn+oyey3Uy15djuuMNbZINXui/bw +TEK5X43vx1QHEaTGiwbUovLHqmw5hHY6uWaLV7zCisuy7V2HWVKBIAf/78fvukNz +ohkxQ08FxMLDOvzIruwAQ/Yu4aGcZcKxTUDdQhEJaYjsEQ7edf5UnTPj+gSHGihW +qxHkGtwTtnat0zcVothNwn9HX2TpojPr2H3NpzeJcnlKDYGmAY8/ENxZk+vz3F0p +W5jyWZOYwx2CctoZLdYEAsI2QTwLH93rLDRF1Fl3rp1px8XwfKkJS7Dfqjt6qaI0 +2yiYS/YkkfouPEKR3PFEY0wXq2EzBlj1ovbWlV9PgCK0DhnQZwIDAQABo1MwUTAd +BgNVHQ4EFgQUx0DyyJ1FgjIZDOTlY0UZjc+q9o0wHwYDVR0jBBgwFoAUx0DyyJ1F +gjIZDOTlY0UZjc+q9o0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAOqJFIKBacWV30m1RVxC0GA9GOidsvN0e1WB2DM7cmff9SsdGkbz8zVNovWz6 +7jIpLibByj+ER6gbaF/zUPrpMZQa0qGgK6qpwqRl7wJ0qDH9jf1FM//0O4yrnjBd +5WG2y5vmIlHAwhH+UVs2EiN5QYycB8jDpuV2dhhNop14/FIk24O4N0PmeDa3e5OP +psodj2vOR7B0Rx17cmHc1gLbjFgikHpq2JIlCD6Fpnr/RbUPUYxFZRsV5VpX8a34 +m+EzTD9QqsWC7+mo1JMnt+SoIOlsCGAq0BH3ysh9Ha1AU8/UboY14jrNxNRRrJZx +bvWyrX5nBsFxo6qZeae16cmExFQpeZqGZ2kLuihkSF5BJZb01A7Uq1Zr6iqGYhSE +zOj89e2/v6cwiMnkowsnUdSOzzRnGDZZMOalGcxdA6zJfZJ9ydmsMPfBNt4C4s4u +rpkOIy2+bzcbwbOc/9KYIqiCItmbxXq81Hz86y5R1jFUn1VsTHHUIlMT+4w4PqW7 +gIPoat1y1sVqRLFf8TkJ6AUq0/zlv+dBFYeaHuutrh5KnCte9u5n3+W2Rv0q9v9C +XXWviHSYfjsRBUx2PFDmFjSZ6sQeNQD0yQV1WJ79aBp3agfYfAN3ke7o+YnBFbAX +y4g+2aFAtflESwkpvo3pOSz+JSBUTPgLR3f4dA/cWx187Zc= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.key b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.key new file mode 100644 index 0000000000000..72cb4ad738e38 --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/http_ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +G7FQu8rjmDHW2o8lcCYim1H3ueoN +-----END PRIVATE KEY----- diff --git a/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.crt b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.crt new file mode 100644 index 0000000000000..20c521de47ab8 --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFrzCCA5egAwIBAgIUE6nu/E57NrAOtFhYBCEZQMdFk6gwDQYJKoZIhvcNAQEL +BQAwZzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEgMB4GA1UEAwwXRWxhc3RpY3NlYXJj +aCBUZXN0IENhc2UwHhcNMjEwOTA0MTI1NzQxWhcNMjIwOTA0MTI1NzQxWjBnMQsw +CQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJu +ZXQgV2lkZ2l0cyBQdHkgTHRkMSAwHgYDVQQDDBdFbGFzdGljc2VhcmNoIFRlc3Qg +Q2FzZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALsYS+TrqxmYJxi3 +VoYhYkOEKv445UbbCmZcMsKOF8cj1xQOwgXC4dp0L8Eok8DGap19R/APM+0dZttf +8oPzjn77CRvpoLIXP9eljhqZhQnmOrHnv1EQgbHIVpLrHu53rH+Ths4U43qaU0py +83zbngvl4cRSWRQZIaD1P9Jr9lfKCA1A1UTmRl479HsaJ0VoQxEHVR5TNcnaL3CJ +QS3uhdWxobz1etFBF6VTe05s7gQVqpL8JMZWP2ezbk6uCHzmqQ5LWVV3LRI9HmV+ +rVcMV8SdoM2JSf8Hecq9dfj4qv9CNpqYcptd+LoQsun5LHHLvSU9ecRQ2jGOUItL +w29ZlNxm6OBA8T59iDZLkIraYVQLQ6buxHx/zyXnTztEXsRo+zd6nUn/Kf6tYWhw +9pyavwGzfl70rusTqY48oqmwTD7Ig3ZmlwyE9pzlusPxsDW5U4VP8tNyuvycHpmM +lGao067tuM0KLofhEO8v9ETdCQD7PwJwgWO7pKvJ0lzLxwVjs8pylx6psbiXtGmr +nAKK5KQrcTd67wHoZ5INBo0ZVMZe4eWEnR9fo1/RvUzCMr2oWVSc1mf0lkVEaXs9 +Vdp6cLgZHHz3XFsQ+yoZXLd7wZH5dZdqN46kveLkSWhm56d1XorSVR8cW2rSkw7j +0l52xnRWAn+z7G6hlNwCM6H2mZ4nAgMBAAGjUzBRMB0GA1UdDgQWBBR7/9vDOfzv +XDa6O/5H+PWi5gXNlzAfBgNVHSMEGDAWgBR7/9vDOfzvXDa6O/5H+PWi5gXNlzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQApx/b6qVRC37XVx2Vs +CwWevqyt8urPBrYxYxNjz5s2x2GTv/EQEvF1gqJH4tpX1He7M035+ZOH0CvzFsNS +L0TtfXbLXPhZv9h+HyP3mBFE9A72BCYNkvsjBrjgXTzj4b0D3kZjrWLRfwK1Ln5r +CY6v63UK/yjnRbzz1Vak5sYuVsVgE0XvIMEPcnxRExvz9pIhCoNYMEFRjFFAyhiQ +r87EJIXq8TbrUm7mDihqulIh0rEJuXLcte4cXty9X/UA8q/1kKPobeLZz6rtVzUL +c9bUdw+qMbwNYwRGfmTN4pG2GEBszqu3xIfkBKGdEKwg3hYJN6KDi5w7UEtBD3Fw +WRbPTben2Q2NjBVtMNpiJdu5iVU/incNp1VBN23+FcLV2T9A8C98Qu14sDq2t9o5 ++JdzexfssBy6uI4YN2E+Bh3bpg4RWHZsnoRaOrR/x3LtvlaY96BuJve/3A1m6AfB +2yHWOAE/HOVts+wUIlIpwm1Y2l5qCirkl9rU9YM8bPqDmQ79StOe5Vu+vtyV9UD2 +SbRe07e5xmKcvMLvg+vhJ7EvvZIm94I9h4wC3CA/hT322K+1MoqCIQl3LzGnyV1o +0O7hEUY4Pb1iUPLmIRLCWjUsBB/XpPrRLJhX2fhm2jzFCcyuAZQXByKbvK+MjIsZ +Qm7MGBa0T5Izkewv56Ye1epPEQ== +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.key b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.key new file mode 100644 index 0000000000000..2a8383320f15b --- /dev/null +++ b/x-pack/plugin/security/cli/src/test/resources/org/elasticsearch/xpack/security/cli/transport.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +zVaOu9bBO00c55m9Yvhm28tnDF+c7fs= +-----END PRIVATE KEY----- diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java index f6f262c4ad133..0202b312a33ec 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordTool.java @@ -23,15 +23,15 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.function.Function; -import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; +import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL; import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword; public class ResetElasticPasswordTool extends BaseRunAsSuperuserCommand { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index e5e4ebdfee7a0..0a761f05c3a0a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -36,9 +36,9 @@ import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; import org.elasticsearch.xpack.core.security.user.RemoteMonitoringUser; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.tool.HttpResponse; -import org.elasticsearch.xpack.security.tool.HttpResponse.HttpResponseBuilder; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; +import org.elasticsearch.xpack.core.security.HttpResponse.HttpResponseBuilder; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; import javax.net.ssl.SSLException; import java.io.ByteArrayOutputStream; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java index 69107411bb5ee..6b19f687fd18e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGenerator.java @@ -22,11 +22,12 @@ import org.elasticsearch.core.Tuple; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.EnrollmentToken; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction; import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction; import org.elasticsearch.xpack.core.ssl.SSLService; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import java.io.IOException; import java.io.InputStream; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java index abb6ce4dcb715..38ecc8d709dc1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNode.java @@ -24,10 +24,10 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.core.security.EnrollmentToken; import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import java.io.IOException; import java.net.HttpURLConnection; @@ -36,7 +36,7 @@ import java.net.URL; import java.util.function.Function; -import static org.elasticsearch.xpack.security.tool.CommandLineHttpClient.createURL; +import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL; import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword; public class BootstrapPasswordAndEnrollmentTokenForInitialNode extends KeyStoreAwareCommand { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java index 34495e3a2b59c..a5dc2947cc7b1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java @@ -20,7 +20,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; import org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; import java.util.List; import java.util.function.Function; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java index c34ef68d9c6c1..270d80d0b68f3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java @@ -21,6 +21,8 @@ import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java index 558d7c71aa3ad..3515f7bcc179b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java @@ -9,16 +9,18 @@ import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.SslUtil; import org.elasticsearch.common.ssl.SslVerificationMode; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.http.MockResponse; import org.elasticsearch.test.http.MockWebServer; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettingsTests; import org.elasticsearch.xpack.core.ssl.TestsSSLService; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; -import org.elasticsearch.xpack.security.tool.HttpResponse.HttpResponseBuilder; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; +import org.elasticsearch.xpack.core.security.HttpResponse.HttpResponseBuilder; import org.junit.After; import org.junit.Before; @@ -28,6 +30,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.security.cert.X509Certificate; import static org.hamcrest.Matchers.containsString; @@ -40,11 +43,13 @@ public class CommandLineHttpClientTests extends ESTestCase { private MockWebServer webServer; private Path certPath; private Path keyPath; + private Path caCertPath; @Before public void setup() throws Exception { - certPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"); - keyPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.pem"); + certPath = getDataPath("/org/elasticsearch/xpack/security/authc/esnative/tool/http.crt"); + keyPath = getDataPath("/org/elasticsearch/xpack/security/authc/esnative/tool/http.key"); + caCertPath = getDataPath("/org/elasticsearch/xpack/security/authc/esnative/tool/ca.crt"); webServer = createMockWebServer(); webServer.enqueue(new MockResponse().setResponseCode(200).setBody("{\"test\": \"complete\"}")); @@ -58,7 +63,7 @@ public void shutdown() { public void testCommandLineHttpClientCanExecuteAndReturnCorrectResultUsingSSLSettings() throws Exception { Settings settings = getHttpSslSettings() - .put("xpack.security.http.ssl.certificate_authorities", certPath.toString()) + .put("xpack.security.http.ssl.certificate_authorities", caCertPath.toString()) .put("xpack.security.http.ssl.verification_mode", SslVerificationMode.CERTIFICATE) .build(); CommandLineHttpClient client = new CommandLineHttpClient(TestEnvironment.newEnvironment(settings)); @@ -70,6 +75,20 @@ public void testCommandLineHttpClientCanExecuteAndReturnCorrectResultUsingSSLSet assertEquals("Http response body does not match", "complete", httpResponse.getResponseBody().get("test")); } + public void testCommandLineClientCanTrustPinnedCaCertificateFingerprint() throws Exception { + X509Certificate caCert = CertParsingUtils.readX509Certificate(caCertPath); + CommandLineHttpClient client = new CommandLineHttpClient( + (TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build())), + SslUtil.calculateFingerprint(caCert, "SHA256") + ); + HttpResponse httpResponse = client.execute("GET", new URL("https://localhost:" + webServer.getPort() + "/test"), "u1", + new SecureString(new char[]{'p'}), () -> null, is -> responseBuilder(is)); + + assertNotNull("Should have http response", httpResponse); + assertEquals("Http status code does not match", 200, httpResponse.getHttpStatus()); + assertEquals("Http response body does not match", "complete", httpResponse.getResponseBody().get("test")); + } + public void testGetDefaultURLFailsWithHelpfulMessage() { Settings settings = Settings.builder() .put("path.home", createTempDir()) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java index b3d0400193314..aa3028447604e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java @@ -30,9 +30,9 @@ import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; -import org.elasticsearch.xpack.security.tool.HttpResponse.HttpResponseBuilder; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; +import org.elasticsearch.xpack.core.security.HttpResponse.HttpResponseBuilder; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Rule; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java index df79e06e1aa9d..04e3aee9a5ed3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/EnrollmentTokenGeneratorTests.java @@ -19,8 +19,8 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.user.ElasticUser; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.BeforeClass; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java index cec2694d8178f..fb8addc77738e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BootstrapPasswordAndEnrollmentTokenForInitialNodeTests.java @@ -17,9 +17,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; -import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.EnrollmentToken; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.junit.Before; import org.junit.BeforeClass; diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.crt new file mode 100644 index 0000000000000..5224f7971887e --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUTfurHjkErRe+2LS2qfWZHBqsHr4wDQYJKoZIhvcNAQEL +BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l +cmF0ZWQgQ0EwHhcNMjEwOTA0MjA0NDAzWhcNMjQwOTAzMjA0NDAzWjA0MTIwMAYD +VQQDEylFbGFzdGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNxdhf3MFl4052kLD+1poPW +ONI99j6VNjdLBRBQZOfAAQaA8Im6bcaIKdhITfga4flB00EjrMmMvQriEDKz/UiP +Cctk2KAwEJBNmbE4kIMSITCMpKf1zOVG9dY4pswlAvk1A47uboXI5yq9l0X2GPo8 +G1Jf/y8WhZTXTQLxfo9jQ62dSj+9nZ7HQ6dKsFPJON1osFvcrj4hFvap6VoIoRBU +WPlIUnLtSv8PeAFN6TzHgAwbGxDXQgtkMvjn8MwcVoj1D3S8Omdl5WDpoDa9WDDX +HVzw/4QeUwyf47vFpQvN+ALV38rysalXEisR8DAFzxdr0TeVEwG08RKdB342ZREC +AwEAAaNTMFEwHQYDVR0OBBYEFH2x5XOJL1ysUvpw4e9j4yVv8Lg/MB8GA1UdIwQY +MBaAFH2x5XOJL1ysUvpw4e9j4yVv8Lg/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBADNx/xkz4gdIcxsp3xaOTUh3tQOKjXGV96PZX3ExcdAnSHgV +El2tRq4D02fPQOMFKi3s9Y2E4a1BBi4++tdQ6+8A2WiRo+IWMz4Ap8lDpGnDFUdE +Guc4WJZDB30tAo2Z8AFBy2aGVzni3pWvE824Oo1yFvUkbbgQhY8szqIR1b4k4rp3 +PsU76cnerEXRU3ODmxgDn5pSlNTcTTyTMbbwCFpiHDbVh4GB/wknmVDWULfYhI8R +fmt11d1h4dgoHMosPvwuGemuavEh/fxQxwNxzIW4lFckJ7pQla7Xr0bBD50NKoJk +wdcszLi8vg7ubekkevIVunIY1sqMy5NsgzABGgo= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.key new file mode 100644 index 0000000000000..33b232a2a6b84 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAo3F2F/cwWXjTnaQsP7Wmg9Y40j32PpU2N0sFEFBk58ABBoDw +ibptxogp2EhN+Brh+UHTQSOsyYy9CuIQMrP9SI8Jy2TYoDAQkE2ZsTiQgxIhMIyk +p/XM5Ub11jimzCUC+TUDju5uhcjnKr2XRfYY+jwbUl//LxaFlNdNAvF+j2NDrZ1K +P72dnsdDp0qwU8k43WiwW9yuPiEW9qnpWgihEFRY+UhScu1K/w94AU3pPMeADBsb +ENdCC2Qy+OfwzBxWiPUPdLw6Z2XlYOmgNr1YMNcdXPD/hB5TDJ/ju8WlC834AtXf +yvKxqVcSKxHwMAXPF2vRN5UTAbTxEp0HfjZlEQIDAQABAoIBAAImSEaSlG2fem8A +vVPoLG9gRkveOeptoB0pjNXqV21kgnmcAViFCGkQ74tIkNKcwzs55lCdar9q1634 +i6coZAIhNCCF7BhtPBEzHWdDMRlcMsNhR3rDBOXAtr58F8DTEsnaweLObXXaeXnV +LP5j+RpKEepAg3S9KoFq3yMqWxRWf+GAfcMvWI95w2yIBazUC5+QF6J0PreHMayD +nDNPNb1GfTSC4mSd2K/fcia8joTYHuWOZBlxQeLa3BBzfdKj4lnaSYNjr8oyJtv8 +hGKlxLpo/80z/efUFJUThtHSBT6ewONgQ+D6eOGe1G9+lya7avkzICkiev+jR0s/ +Pygk1FECgYEA4gxTV2mfatXyaP/5dbyiZ0JHNC2Ye29DcAZ8bv8b52BHsOthYOIc +/SM5ouYJYAhyjzFzVKXSDr2CIp1geN4Ac/l6WJcUnY/b2XCnjmRRwrY/OqUbPdUY +PIp+57xLJqG83K5Nd6ActBf9GFP2QFYaT6ta2yDPks3vnzWgR0qzvEkCgYEAuRmK +6nFmBvqIzJtBOlZ8ZcIft/coJ31vxZFSUWqFDbJ06eRX4uNWN1HVYTerbKV6EfF2 +8c5834Nrunpm45Uw82UjiA8F72WT+usSgu9O+97RJWlcfmO15XRovmnOlBqd1rTk +gQtF6CfGaxqMCcAPLVy9ndLcuiimRwoYrHt9kokCgYBoNM5myaZYFfD+PqK9iAxG +FePFRg+5N32bMyJB+RDgBR6HjLsDcrlyaL61Pd6sirhlEqLcLuU7LlnDo1FJ5u3G +iQfBt0Qlrp/nCWv01IJshJ95ZYu9YXMe1anTIpZyZLUv9pp3vzP8QeLHSE2JMyhF ++fSUd/e48X65onsU4nchIQKBgGxGRLxfGQ97/gmxx7YYYSwlIei66wIunfMzzrNR +XANnIHyw1bgiw4wYknkL13r2UTGtzaYk42bbuWibsRPvcXLu9pngL9iZ0rY0S9/L +nKg47p6zwycrrHtMXPkFa5G3AB1YM1JJBduHaMm9/ay2bCpc1Y48imFa5ekoPsam +dg4BAoGAFKbJlaQwyGjESltdsUnCv4YLJMBymsOYpUeG3RcqllkGuMvxv9R5vr3L +c2TnWMHn5u0ViJqD7cAbM9+WCbkmZtE7sC2f0hg6OwN5C9p0iH2pgnprxmd3Va4H +PcOQs9S1n0F+qN9XKdMs07szVkdlgJyrJ3+lStd39733WiYh54s= +-----END RSA PRIVATE KEY----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.crt new file mode 100644 index 0000000000000..15f8f3c13d1b9 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.crt @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIVALxB8jp/9SeMjdKG75Iov618ng/YMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMB4XDTIxMDkwNDIwNDcxOFoXDTMxMDkwNDIwNDcxOFowFDESMBAG +A1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +1G13abvWL9BlP9QxkEAF/i83/Pe+FSRghvGbF2pdqqf7A78kmh3HPsNdeGONByy6 +0b5DfUZFtXA9Z4AVY7Z4hgeWyq3AkWE6q1fE24BEq64W3TYr+p9l+huH++sBV5JM ++/jX4sqHkUThgnx7MMGyRsiv036cVoU0hnvhZoyvNNI7q/uAQ8ICYmxYCZmhWEdA +xssGjRV4YGVqXPDM67KrAc/xkYI8nLOoTrfaEsgeeJonr6ppqPCjEnchZ4R4zmfY +Nt3C9Pb79uVnCL5luiDjC0qjDJTDX5EZFAMJ4sxXVgLNOTrpLelM+vutaZ5wTkOf +rkUJ6ebARtoLfmLh2d14ywIDAQABo4HgMIHdMB0GA1UdDgQWBBSqli12+VXA+ZBT +NJ0WR8iqvp5shTAfBgNVHSMEGDAWgBR9seVziS9crFL6cOHvY+Mlb/C4PzCBjwYD +VR0RBIGHMIGEgglsb2NhbGhvc3SCF2xvY2FsaG9zdDYubG9jYWxkb21haW42hwR/ +AAABhxAAAAAAAAAAAAAAAAAAAAABggpsb2NhbGhvc3Q0ggpsb2NhbGhvc3Q2ghVs +b2NhbGhvc3QubG9jYWxkb21haW6CF2xvY2FsaG9zdDQubG9jYWxkb21haW40MAkG +A1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAD4g/+KEwJmRFdCygUx+AiiS60+d +mzoLBJRqFr2q3U6mP90BfcZw5Ev5TEw2TpZjaQmjSphmufvQVMzv7jTya1NiGdTl +3aE4xosky3Pp5hJeeNfg6ZkxyeUy1P4C9i0ltKIFEQN/MUbFoXWP8Utwwg47vRhJ +cgveXYsMEdLSA6mWUiy/RQ4QsotasncqbXuvxyk0qOeMWMVzxzkBPo3kAN6seKW1 +B5guVjHTevXUgcDEUwnvQ0PnRjrH/oh/ss3aEguv1G2G/13CVgC2cmUhgjGtOJeb +c1Atl5U6a8fc7/iBBdbS8bRvPDusu/XKC5ekvSj3/+VYmNZxuu5rMOWepGU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUTfurHjkErRe+2LS2qfWZHBqsHr4wDQYJKoZIhvcNAQEL +BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l +cmF0ZWQgQ0EwHhcNMjEwOTA0MjA0NDAzWhcNMjQwOTAzMjA0NDAzWjA0MTIwMAYD +VQQDEylFbGFzdGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNxdhf3MFl4052kLD+1poPW +ONI99j6VNjdLBRBQZOfAAQaA8Im6bcaIKdhITfga4flB00EjrMmMvQriEDKz/UiP +Cctk2KAwEJBNmbE4kIMSITCMpKf1zOVG9dY4pswlAvk1A47uboXI5yq9l0X2GPo8 +G1Jf/y8WhZTXTQLxfo9jQ62dSj+9nZ7HQ6dKsFPJON1osFvcrj4hFvap6VoIoRBU +WPlIUnLtSv8PeAFN6TzHgAwbGxDXQgtkMvjn8MwcVoj1D3S8Omdl5WDpoDa9WDDX +HVzw/4QeUwyf47vFpQvN+ALV38rysalXEisR8DAFzxdr0TeVEwG08RKdB342ZREC +AwEAAaNTMFEwHQYDVR0OBBYEFH2x5XOJL1ysUvpw4e9j4yVv8Lg/MB8GA1UdIwQY +MBaAFH2x5XOJL1ysUvpw4e9j4yVv8Lg/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBADNx/xkz4gdIcxsp3xaOTUh3tQOKjXGV96PZX3ExcdAnSHgV +El2tRq4D02fPQOMFKi3s9Y2E4a1BBi4++tdQ6+8A2WiRo+IWMz4Ap8lDpGnDFUdE +Guc4WJZDB30tAo2Z8AFBy2aGVzni3pWvE824Oo1yFvUkbbgQhY8szqIR1b4k4rp3 +PsU76cnerEXRU3ODmxgDn5pSlNTcTTyTMbbwCFpiHDbVh4GB/wknmVDWULfYhI8R +fmt11d1h4dgoHMosPvwuGemuavEh/fxQxwNxzIW4lFckJ7pQla7Xr0bBD50NKoJk +wdcszLi8vg7ubekkevIVunIY1sqMy5NsgzABGgo= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.key new file mode 100644 index 0000000000000..32319aa7c21a6 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/esnative/tool/http.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1G13abvWL9BlP9QxkEAF/i83/Pe+FSRghvGbF2pdqqf7A78k +mh3HPsNdeGONByy60b5DfUZFtXA9Z4AVY7Z4hgeWyq3AkWE6q1fE24BEq64W3TYr ++p9l+huH++sBV5JM+/jX4sqHkUThgnx7MMGyRsiv036cVoU0hnvhZoyvNNI7q/uA +Q8ICYmxYCZmhWEdAxssGjRV4YGVqXPDM67KrAc/xkYI8nLOoTrfaEsgeeJonr6pp +qPCjEnchZ4R4zmfYNt3C9Pb79uVnCL5luiDjC0qjDJTDX5EZFAMJ4sxXVgLNOTrp +LelM+vutaZ5wTkOfrkUJ6ebARtoLfmLh2d14ywIDAQABAoIBAFHswb/yZEFecp3y +PQcv2d/U4Bfch99pfxyr8y9No0Actq8UOY6Ca4QmHlc+tXYN5yLa6PZYcqvt1JCl +Ehq5lbPDG4GhDwJCaDkKiW5KArHfWwwHR2DJtq+FjKU4VfUrBCutJb1edHgvA3Wq +gmwkV5f67+x2CN9JUh+HiT9ehHeu5L3nU47geRAADR1vuxJeI2aqhEEm4oVobzr7 +xlVhPc6qkpeyRF5W5suYsEGqZ+aXlX383wGHHgfAu1DqU4jhGpas/Yu86+0GFOd/ +04xrjcLopMLOcX4nxzs7YW/Legieol5+/px/qziNtq3LR1XZjkvQskwPa+Xiwa0Z +0efJnPECgYEA4Q8DBdsZUAFEmELpfPMjZY6OsWGAJWFkHDJgb/wadLfS+ZlX5n2N +dRjkrhj5mV4kbCBufozKqPJyEKDoTiRq7iDxJl5Ay7wZxtgajadznP15zMd9w2t+ +KFRpZefO3G3FoIgKXRXJWamNT4c3NmK7EgQUR13VgUbzYTPdb/bGN1MCgYEA8aHn +DWSOfBCz6EaAkrwh4jNDbGswfMDYrukIK71kOFsuwhUlCdt/ZrBV2i8TjPaRxImr +du5Ye49BG2oWyfRGT7+c22Dp/UiPYbNsTGMbXpnZeErUp5C4TlDzBL9F3Ffb/ayG +3KmB97YuSnumLymFfjmfFukchQaJ7BPkjV6x4akCgYAzXQa2lmtve+qYyWSaVK82 +ZVOhnfvuDA0Z9lFWHXY4Px/SQTHzc6IOIHFIXoDQhNDRMJGnJiC2cCaiLub1tpAE +6tW+iPJGzRYg8H8W5ymWoa7jkn/cUrMHJ0Sqsj3extq8qW+cEPXUFcMfbF+odN32 +3aF3LC4nA/dVrs2R2QMh6wKBgQCADjmRE3Wfsopl0tdY7HNYmaplzvjHZLxxFfbK +l3iBlXFWUjZq3vTJyzH0i3ZlAleGPR+ty+sAsI7kpHinVtncccQDT30ySj4SnTAL +24opvIdQBPhmRYAnoSbpoSS4/acM7V4bm4nRaa9msdkErZCdsJPrZGdE6I43muNJ +OKI04QKBgHsphhJdV7sapWCkTGcoQWyRxl9Bpki6Yls23lADGGCXsGaTT3M6mZhM +c7aQus8obYs2d0HtaQnCE2Y301J+wtqEbZZ1khreuDVmPqU103AGoLPGcqAqdcVG +xXw3PQdoqeb0hMjTjep6FeokLZ+d+NbLR5LBcDD8C2MVUaCVzeh6 +-----END RSA PRIVATE KEY----- diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordToolTests.java index 6485cd1a1f5d5..e61d3bcc0999d 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetElasticPasswordToolTests.java @@ -22,8 +22,8 @@ import org.elasticsearch.core.PathUtilsForTesting; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BaseRunAsSuperuserCommandTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BaseRunAsSuperuserCommandTests.java index 3f65572952942..5da81fd21cbd3 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BaseRunAsSuperuserCommandTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/BaseRunAsSuperuserCommandTests.java @@ -29,8 +29,8 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java index 5a7964de2ce39..ac607ee1f21d9 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java @@ -23,10 +23,10 @@ import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.security.enrollment.EnrollmentToken; +import org.elasticsearch.xpack.core.security.EnrollmentToken; import org.elasticsearch.xpack.security.enrollment.EnrollmentTokenGenerator; -import org.elasticsearch.xpack.security.tool.CommandLineHttpClient; -import org.elasticsearch.xpack.security.tool.HttpResponse; +import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.HttpResponse; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; From 598b4e15c86b1e77720e4c7ef1de7da6098ef730 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 6 Sep 2021 00:32:03 +0300 Subject: [PATCH 04/12] simplify arg parsing --- distribution/src/bin/elasticsearch | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index 01fc5cac512d1..10db18f0a9501 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -18,22 +18,18 @@ source "`dirname "$0"`"/elasticsearch-env CHECK_KEYSTORE=true DAEMONIZE=false ENROLL_TO_CLUSTER=false -for option in "$@"; do - case "$option" in - -h|--help|-V|--version) - CHECK_KEYSTORE=false - ;; - -d|--daemonize) - DAEMONIZE=true - ;; - esac -done +# Store original arg array as we will be shifting through it below +ARG_LIST=$@ while [ $# -gt 0 ]; do if [[ $1 == "--enrollment-token" ]]; then - ENROLL_TO_CLUSTER=true - ENROLLMENT_TOKEN="$2" + ENROLL_TO_CLUSTER=true + ENROLLMENT_TOKEN="$2" + elif [[ $1 == "-h" || $1 == "--help" || $1 == "-V" || $1 == "--version" ]]; then + CHECK_KEYSTORE=false + elif [[ $1 == "-d" || $1 == "--daemonize" ]]; then + DAEMONIZE=true fi - shift + shift done if [ -z "$ES_TMPDIR" ]; then @@ -83,7 +79,7 @@ if [[ $DAEMONIZE = false ]]; then -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "$@" <<<"$KEYSTORE_PASSWORD" + "$ARG_LIST" <<<"$KEYSTORE_PASSWORD" else exec \ "$JAVA" \ @@ -96,7 +92,7 @@ else -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "$@" \ + "$ARG_LIST" \ <<<"$KEYSTORE_PASSWORD" & retval=$? pid=$! From b6c2784696de81d0f6d55b45df8732269634469f Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 6 Sep 2021 00:33:31 +0300 Subject: [PATCH 05/12] Update docs/changelog/77292.yaml --- docs/changelog/77292.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/77292.yaml diff --git a/docs/changelog/77292.yaml b/docs/changelog/77292.yaml new file mode 100644 index 0000000000000..ecf642cfb7eb2 --- /dev/null +++ b/docs/changelog/77292.yaml @@ -0,0 +1,5 @@ +pr: 77292 +summary: Enroll additional nodes to cluster +area: "Infra/CLI, Security, Packaging" +type: enhancement +issues: [] From 56fb2b8043b8d39f7b1cad401930f4ef83734c40 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 6 Sep 2021 00:56:09 +0300 Subject: [PATCH 06/12] remove explicit CLI tool --- .../security/src/main/bin/elasticsearch-enroll-node | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node deleted file mode 100644 index cce0b32dec638..0000000000000 --- a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node +++ /dev/null @@ -1,12 +0,0 @@ -#!/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.ConfigAdditionalNodes \ - ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \ - ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \ - "$(dirname "$0")/elasticsearch-cli" \ - "$@" From 6d61210690efba72c559fdced1cf44561c6d5a73 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 6 Sep 2021 00:58:42 +0300 Subject: [PATCH 07/12] update changelog --- docs/changelog/77292.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog/77292.yaml b/docs/changelog/77292.yaml index ecf642cfb7eb2..6edc14dff2838 100644 --- a/docs/changelog/77292.yaml +++ b/docs/changelog/77292.yaml @@ -1,5 +1,5 @@ pr: 77292 summary: Enroll additional nodes to cluster -area: "Infra/CLI, Security, Packaging" +area: "Security" type: enhancement issues: [] From 3930fce015fc49c74d7a75c6a0a74cd9e4cd48cf Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 6 Sep 2021 01:23:30 +0300 Subject: [PATCH 08/12] array expanding --- distribution/src/bin/elasticsearch | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index 10db18f0a9501..eae15e3b52be4 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -19,7 +19,7 @@ CHECK_KEYSTORE=true DAEMONIZE=false ENROLL_TO_CLUSTER=false # Store original arg array as we will be shifting through it below -ARG_LIST=$@ +ARG_LIST=($@) while [ $# -gt 0 ]; do if [[ $1 == "--enrollment-token" ]]; then ENROLL_TO_CLUSTER=true @@ -79,7 +79,7 @@ if [[ $DAEMONIZE = false ]]; then -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "$ARG_LIST" <<<"$KEYSTORE_PASSWORD" + "${ARG_LIST[@]}" <<<"$KEYSTORE_PASSWORD" else exec \ "$JAVA" \ @@ -92,7 +92,7 @@ else -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "$ARG_LIST" \ + "${ARG_LIST[@]}" \ <<<"$KEYSTORE_PASSWORD" & retval=$? pid=$! From eaaa797a9e752434f2ac2c43d3be24b87e08f2dc Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 7 Sep 2021 16:51:47 +0300 Subject: [PATCH 09/12] feedback --- distribution/src/bin/elasticsearch | 1 + distribution/src/bin/elasticsearch.bat | 14 ++++++++------ .../org/elasticsearch/common/ssl/PemUtils.java | 16 ++++++++++------ .../core/security/CommandLineHttpClient.java | 2 +- .../xpack/core/security/EnrollmentToken.java | 2 +- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index eae15e3b52be4..ed9fe0c6c81b2 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -24,6 +24,7 @@ while [ $# -gt 0 ]; do if [[ $1 == "--enrollment-token" ]]; then ENROLL_TO_CLUSTER=true ENROLLMENT_TOKEN="$2" + shift elif [[ $1 == "-h" || $1 == "--help" || $1 == "-V" || $1 == "--version" ]]; then CHECK_KEYSTORE=false elif [[ $1 == "-d" || $1 == "--daemonize" ]]; then diff --git a/distribution/src/bin/elasticsearch.bat b/distribution/src/bin/elasticsearch.bat index b5c4efcfec09c..6d7935f481713 100644 --- a/distribution/src/bin/elasticsearch.bat +++ b/distribution/src/bin/elasticsearch.bat @@ -36,6 +36,7 @@ FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO ( IF "!current!" == "--enrollment-token" ( SHIFT + SET enrolltocluster=Y SET enrollmenttoken=%~1 ) @@ -74,12 +75,13 @@ IF "%checkpassword%"=="Y" ( ) ) -set ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster -set ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env -set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli -call "%~dp0elasticsearch-cli.bat" --enrollment-token %enrollmenttoken% - || goto exit - +IF "%enrolltocluster%"=="Y" () + SET ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster + SET ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env + SET ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli + CALL "%~dp0elasticsearch-cli.bat" --enrollment-token %enrollmenttoken% + || GOTO EXIT +) if not defined ES_TMPDIR ( for /f "tokens=* usebackq" %%a in (`CALL %JAVA% -cp "!ES_CLASSPATH!" "org.elasticsearch.tools.launchers.TempDirectory"`) do set ES_TMPDIR=%%a diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java index cb4505e271ff1..d56459746b9cf 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java @@ -207,14 +207,18 @@ private static PrivateKey parsePKCS8(BufferedReader bReader) throws IOException, if (null == line || PKCS8_FOOTER.equals(line.trim()) == false) { throw new IOException("Malformed PEM file, PEM footer is invalid or missing"); } - byte[] keyBytes = Base64.getDecoder().decode(sb.toString()); - String keyAlgo = getKeyAlgorithmIdentifier(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo); - return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + return parsePKCS8PemString(sb.toString()); } - public static PrivateKey parsePKCS8PemString(String pem) throws IOException, GeneralSecurityException{ - byte[] keyBytes = Base64.getDecoder().decode(pem); + /** + * Creates a {@link PrivateKey} from a String that contains the PEM encoded representation of a plaintext private key encoded in PKCS8 + * @param pemString the PEM encoded representation of a plaintext private key encoded in PKCS8 + * @return {@link PrivateKey} + * @throws IOException if the algorithm identifier can not be parsed from DER + * @throws GeneralSecurityException if the private key can't be generated from the {@link PKCS8EncodedKeySpec} + */ + public static PrivateKey parsePKCS8PemString(String pemString) throws IOException, GeneralSecurityException{ + byte[] keyBytes = Base64.getDecoder().decode(pemString); String keyAlgo = getKeyAlgorithmIdentifier(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo); return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java index 568bca46ed83e..3a1bb2da31319 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/CommandLineHttpClient.java @@ -333,7 +333,7 @@ public void checkClientTrusted(X509Certificate[] chain, String authType) throws } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - final Certificate caCertFromChain = chain[chain.length-1]; + final Certificate caCertFromChain = chain[1]; MessageDigest sha256 = MessageDigests.sha256(); sha256.update(caCertFromChain.getEncoded()); if (MessageDigests.toHexString(sha256.digest()).equals(pinnedCaCertFingerprint) == false ) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java index ed482ded030cf..a46d57881a957 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java @@ -86,7 +86,7 @@ public String getEncoded() throws Exception { } /** - * Decodes and parses an enrollment token from it's serialized form (created with {@link EnrollmentToken#getEncoded()} + * Decodes and parses an enrollment token from its serialized form (created with {@link EnrollmentToken#getEncoded()} * @param encoded The Base64 encoded JSON representation of the enrollment token * @return the parsed EnrollmentToken * @throws IOException when failing to decode the serialized token From 47b12c35e84db992451696cf386c12b703be4c70 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 7 Sep 2021 19:20:50 +0300 Subject: [PATCH 10/12] revert changes to startup scripts for now --- distribution/src/bin/elasticsearch | 35 ++++++------------- distribution/src/bin/elasticsearch.bat | 15 -------- .../src/main/bin/elasticsearch-enroll-node | 0 .../main/bin/elasticsearch-enroll-node.bat | 0 4 files changed, 11 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node create mode 100644 x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index ed9fe0c6c81b2..c5805ea2ebd64 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -17,20 +17,15 @@ source "`dirname "$0"`"/elasticsearch-env CHECK_KEYSTORE=true DAEMONIZE=false -ENROLL_TO_CLUSTER=false -# Store original arg array as we will be shifting through it below -ARG_LIST=($@) -while [ $# -gt 0 ]; do - if [[ $1 == "--enrollment-token" ]]; then - ENROLL_TO_CLUSTER=true - ENROLLMENT_TOKEN="$2" - shift - elif [[ $1 == "-h" || $1 == "--help" || $1 == "-V" || $1 == "--version" ]]; then - CHECK_KEYSTORE=false - elif [[ $1 == "-d" || $1 == "--daemonize" ]]; then - DAEMONIZE=true - fi - shift +for option in "$@"; do + case "$option" in + -h|--help|-V|--version) + CHECK_KEYSTORE=false + ;; + -d|--daemonize) + DAEMONIZE=true + ;; + esac done if [ -z "$ES_TMPDIR" ]; then @@ -50,14 +45,6 @@ then fi fi -if [[ $ENROLL_TO_CLUSTER = true ]]; then - ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster \ - ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \ - ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \ - "`dirname "$0"`"/elasticsearch-cli \ - --enrollment-token "$ENROLLMENT_TOKEN" -fi - # The JVM options parser produces the final JVM options to start Elasticsearch. # It does this by incorporating JVM options in the following way: # - first, system JVM options are applied (these are hardcoded options in the @@ -80,7 +67,7 @@ if [[ $DAEMONIZE = false ]]; then -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "${ARG_LIST[@]}" <<<"$KEYSTORE_PASSWORD" + "$@" <<<"$KEYSTORE_PASSWORD" else exec \ "$JAVA" \ @@ -93,7 +80,7 @@ else -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "${ARG_LIST[@]}" \ + "$@" \ <<<"$KEYSTORE_PASSWORD" & retval=$? pid=$! diff --git a/distribution/src/bin/elasticsearch.bat b/distribution/src/bin/elasticsearch.bat index 6d7935f481713..7d4d58010ba33 100644 --- a/distribution/src/bin/elasticsearch.bat +++ b/distribution/src/bin/elasticsearch.bat @@ -5,7 +5,6 @@ setlocal enableextensions SET params='%*' SET checkpassword=Y -SET enrolltocluster=N :loop FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO ( @@ -34,12 +33,6 @@ FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO ( SET checkpassword=N ) - IF "!current!" == "--enrollment-token" ( - SHIFT - SET enrolltocluster=Y - SET enrollmenttoken=%~1 - ) - IF "!silent!" == "Y" ( SET nopauseonerror=Y ) ELSE ( @@ -75,14 +68,6 @@ IF "%checkpassword%"=="Y" ( ) ) -IF "%enrolltocluster%"=="Y" () - SET ES_MAIN_CLASS=org.elasticsearch.xpack.security.cli.EnrollNodeToCluster - SET ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env - SET ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli - CALL "%~dp0elasticsearch-cli.bat" --enrollment-token %enrollmenttoken% - || GOTO EXIT -) - if not defined ES_TMPDIR ( for /f "tokens=* usebackq" %%a in (`CALL %JAVA% -cp "!ES_CLASSPATH!" "org.elasticsearch.tools.launchers.TempDirectory"`) do set ES_TMPDIR=%%a ) diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat new file mode 100644 index 0000000000000..e69de29bb2d1d From ddcaa8f81d569c4c4ad6d06d522ed581b67f43a6 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 7 Sep 2021 19:21:43 +0300 Subject: [PATCH 11/12] Add CLI tool that allows to configure TLS for a node so that it can join an existing cluster --- .../src/main/bin/elasticsearch-enroll-node | 12 +++++++++++ .../main/bin/elasticsearch-enroll-node.bat | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node index e69de29bb2d1d..3aa50df01c10f 100644 --- a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node +++ b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node @@ -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.EnrollNodeToCluster \ + ES_ADDITIONAL_SOURCES="x-pack-env;x-pack-security-env" \ + ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli \ + "$(dirname "$0")/elasticsearch-cli" \ + "$@" diff --git a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat index e69de29bb2d1d..b3b3f192f439d 100644 --- a/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.bat +++ b/x-pack/plugin/security/src/main/bin/elasticsearch-enroll-node.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.EnrollNodeToCluster +set ES_ADDITIONAL_SOURCES=x-pack-env;x-pack-security-env +set ES_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/security-cli +call "%~dp0elasticsearch-cli.bat " ^ + %%* ^ + || goto exit + +endlocal +endlocal +:exit +exit /b %ERRORLEVEL% From e70fe6ef8a70fbcbff511551a6fad9e26b187e18 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 7 Sep 2021 19:55:27 +0300 Subject: [PATCH 12/12] fix algorithm identifier --- .../authc/esnative/tool/CommandLineHttpClientTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java index 3515f7bcc179b..1725c89652fee 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/CommandLineHttpClientTests.java @@ -79,7 +79,7 @@ public void testCommandLineClientCanTrustPinnedCaCertificateFingerprint() throws X509Certificate caCert = CertParsingUtils.readX509Certificate(caCertPath); CommandLineHttpClient client = new CommandLineHttpClient( (TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build())), - SslUtil.calculateFingerprint(caCert, "SHA256") + SslUtil.calculateFingerprint(caCert, "SHA-256") ); HttpResponse httpResponse = client.execute("GET", new URL("https://localhost:" + webServer.getPort() + "/test"), "u1", new SecureString(new char[]{'p'}), () -> null, is -> responseBuilder(is));