|
@@ -9,11 +9,14 @@ package org.elasticsearch.xpack.security.cli;
|
|
|
|
|
|
import joptsimple.OptionSet;
|
|
|
|
|
|
+import joptsimple.OptionSpec;
|
|
|
+
|
|
|
import org.apache.commons.io.FileUtils;
|
|
|
import org.apache.lucene.util.SetOnce;
|
|
|
import org.bouncycastle.asn1.x509.GeneralName;
|
|
|
import org.bouncycastle.asn1.x509.GeneralNames;
|
|
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
|
|
+import org.elasticsearch.ExceptionsHelper;
|
|
|
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
|
|
import org.elasticsearch.cli.ExitCodes;
|
|
|
import org.elasticsearch.cli.Terminal;
|
|
@@ -21,6 +24,7 @@ import org.elasticsearch.cli.UserException;
|
|
|
import org.elasticsearch.cluster.coordination.ClusterBootstrapService;
|
|
|
import org.elasticsearch.cluster.node.DiscoveryNode;
|
|
|
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
|
|
|
+import org.elasticsearch.common.Strings;
|
|
|
import org.elasticsearch.common.UUIDs;
|
|
|
import org.elasticsearch.common.network.NetworkAddress;
|
|
|
import org.elasticsearch.common.network.NetworkService;
|
|
@@ -36,12 +40,18 @@ import org.elasticsearch.env.Environment;
|
|
|
import org.elasticsearch.http.HttpTransportSettings;
|
|
|
import org.elasticsearch.node.Node;
|
|
|
import org.elasticsearch.xpack.core.XPackSettings;
|
|
|
+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 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;
|
|
@@ -54,17 +64,27 @@ 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.ArrayList;
|
|
|
+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 java.util.stream.Stream;
|
|
|
import javax.security.auth.x500.X500Principal;
|
|
|
|
|
|
+import static org.elasticsearch.common.ssl.PemUtils.parsePKCS8PemString;
|
|
|
+import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
|
|
|
+import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
|
|
|
+
|
|
|
/**
|
|
|
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
|
|
|
* Security enabled. Used to configure only the initial node of a cluster, and only before the first time that the node
|
|
@@ -74,47 +94,61 @@ import javax.security.auth.x500.X500Principal;
|
|
|
* This will NOT run if Security is explicitly configured or if the existing configuration otherwise clashes with the
|
|
|
* intent of this (i.e. the node is configured so it might not form a single node cluster).
|
|
|
*/
|
|
|
-public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
+public class AutoConfigureNode extends EnvironmentAwareCommand {
|
|
|
|
|
|
public static final String AUTO_CONFIG_ALT_DN = "CN=Elasticsearch security auto-configuration HTTP CA";
|
|
|
// the transport keystore is also used as a truststore
|
|
|
+ private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
|
|
|
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 TRANSPORT_CERTIFICATE_DAYS = 99 * 365;
|
|
|
+ private static final int TRANSPORT_CA_CERTIFICATE_DAYS = 99 * 365;
|
|
|
private static final int TRANSPORT_KEY_SIZE = 4096;
|
|
|
- private static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node";
|
|
|
- private static final String HTTP_AUTOGENERATED_CA_NAME = "http_ca";
|
|
|
+ private static final int TRANSPORT_CA_KEY_SIZE = 4096;
|
|
|
+ static final String HTTP_AUTOGENERATED_KEYSTORE_NAME = "http_keystore_local_node";
|
|
|
+ static final String HTTP_AUTOGENERATED_CA_NAME = "http_ca";
|
|
|
private static final int HTTP_CA_CERTIFICATE_DAYS = 3 * 365;
|
|
|
private static final int HTTP_CA_KEY_SIZE = 4096;
|
|
|
private static final int HTTP_CERTIFICATE_DAYS = 2 * 365;
|
|
|
private static final int HTTP_KEY_SIZE = 4096;
|
|
|
- private static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_initial_node_";
|
|
|
+ static final String TLS_CONFIG_DIR_NAME_PREFIX = "tls_auto_config_";
|
|
|
+ static final String AUTO_CONFIGURATION_START_MARKER =
|
|
|
+ "#----------------------- Security auto configuration start -----------------------#";
|
|
|
+ static final String AUTO_CONFIGURATION_END_MARKER =
|
|
|
+ "#----------------------- Security auto configuration end -------------------------#";
|
|
|
|
|
|
- public ConfigInitialNode() {
|
|
|
- super("Generates all the necessary security configuration for the initial node of a new secure cluster");
|
|
|
- // This "cli utility" must be invoked EXCLUSIVELY from the node startup script, where it is passed all the
|
|
|
+ private final OptionSpec<String> enrollmentTokenParam = parser.accepts("enrollment-token", "The enrollment token to use")
|
|
|
+ .withRequiredArg();
|
|
|
+ private final BiFunction<Environment, String, CommandLineHttpClient> clientFunction;
|
|
|
+
|
|
|
+ public AutoConfigureNode(BiFunction<Environment, String, CommandLineHttpClient> clientFunction) {
|
|
|
+ super("Generates all the necessary security configuration for a node in a secured cluster");
|
|
|
+ // This "cli utility" is invoked from the node startup script, where it is passed all the
|
|
|
// node startup options unfiltered. It cannot consume most of them, but it does need to inspect the `-E` ones.
|
|
|
parser.allowsUnrecognizedOptions();
|
|
|
+ this.clientFunction = clientFunction;
|
|
|
+ }
|
|
|
+
|
|
|
+ public AutoConfigureNode() {
|
|
|
+ this(CommandLineHttpClient::new);
|
|
|
}
|
|
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
|
- exit(new ConfigInitialNode().main(args, Terminal.DEFAULT));
|
|
|
+ exit(new AutoConfigureNode().main(args, Terminal.DEFAULT));
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
|
|
- // Silently skipping security auto configuration because node considered as restarting.
|
|
|
+ final boolean inEnrollmentMode = options.has(enrollmentTokenParam);
|
|
|
+
|
|
|
+ // skipping security auto configuration because node considered as restarting.
|
|
|
for (Path dataPath : env.dataFiles()) {
|
|
|
if (Files.isDirectory(dataPath) && false == isDirEmpty(dataPath)) {
|
|
|
- terminal.println(Terminal.Verbosity.VERBOSE,
|
|
|
- "Skipping security auto configuration because it appears that the node is not starting up for the first time.");
|
|
|
- terminal.println(Terminal.Verbosity.VERBOSE,
|
|
|
- "The node might already be part of a cluster and this auto setup utility is designed to configure Security for new " +
|
|
|
- "clusters only.");
|
|
|
- // we wish the node to start as usual during a restart
|
|
|
- // but still the exit code should indicate that this has not been run
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ final String msg = "Skipping security auto configuration because it appears that the node is not starting up for the "
|
|
|
+ + "first time. The node might already be part of a cluster and this auto setup utility is designed to configure "
|
|
|
+ + "Security for new clusters only.";
|
|
|
+ notifyOfFailure(inEnrollmentMode, terminal, Terminal.Verbosity.VERBOSE, ExitCodes.NOOP, msg);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -124,46 +158,37 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
// it is odd for the `elasticsearch.yml` file to be missing or not be a regular (the node won't start)
|
|
|
// but auto configuration should not be concerned with fixing it (by creating the file) and let the node startup fail
|
|
|
if (false == Files.exists(ymlPath) || false == Files.isRegularFile(ymlPath, LinkOption.NOFOLLOW_LINKS)) {
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.NORMAL,
|
|
|
- String.format(
|
|
|
- Locale.ROOT,
|
|
|
- "Skipping security auto configuration because the configuration file [%s] is missing or is not a regular file",
|
|
|
- ymlPath
|
|
|
- )
|
|
|
+ final String msg = 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);
|
|
|
+ notifyOfFailure(inEnrollmentMode, terminal, Terminal.Verbosity.NORMAL, ExitCodes.CONFIG, msg);
|
|
|
}
|
|
|
// If the node's yml configuration is not readable, most probably auto-configuration isn't run under the suitable user
|
|
|
if (false == Files.isReadable(ymlPath)) {
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.NORMAL,
|
|
|
- String.format(
|
|
|
- Locale.ROOT,
|
|
|
- "Skipping security auto configuration because the current user does not have permission to read "
|
|
|
- + " configuration file [%s]",
|
|
|
- ymlPath
|
|
|
- )
|
|
|
+ final String msg = String.format(
|
|
|
+ Locale.ROOT,
|
|
|
+ "Skipping security auto configuration because the current user does not have permission to read "
|
|
|
+ + " configuration file [%s]",
|
|
|
+ ymlPath
|
|
|
);
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ notifyOfFailure(inEnrollmentMode, terminal, Terminal.Verbosity.NORMAL, ExitCodes.NOOP, msg);
|
|
|
}
|
|
|
// Inform that auto-configuration will not run if keystore cannot be read.
|
|
|
if (Files.exists(keystorePath)
|
|
|
&& (false == Files.isRegularFile(keystorePath, LinkOption.NOFOLLOW_LINKS) || false == Files.isReadable(keystorePath))) {
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.NORMAL,
|
|
|
- String.format(
|
|
|
- Locale.ROOT,
|
|
|
- "Skipping security auto configuration because the node keystore file [%s] is not a readable regular file",
|
|
|
- keystorePath
|
|
|
- )
|
|
|
+ final String msg = String.format(
|
|
|
+ Locale.ROOT,
|
|
|
+ "Skipping security auto configuration because the node keystore file [%s] is not a readable regular file",
|
|
|
+ keystorePath
|
|
|
);
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ notifyOfFailure(inEnrollmentMode, terminal, Terminal.Verbosity.NORMAL, ExitCodes.NOOP, msg);
|
|
|
}
|
|
|
|
|
|
// only perform auto-configuration if the existing configuration is not conflicting (eg Security already enabled)
|
|
|
// if it is, silently skip auto configuration
|
|
|
- checkExistingConfiguration(env.settings(), terminal);
|
|
|
+ checkExistingConfiguration(env.settings(), inEnrollmentMode, terminal);
|
|
|
|
|
|
final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC);
|
|
|
final String instantAutoConfigName = TLS_CONFIG_DIR_NAME_PREFIX + autoConfigDate.toInstant().getEpochSecond();
|
|
@@ -206,64 +231,195 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
+ newFileOwner.getName()
|
|
|
);
|
|
|
}
|
|
|
- final KeyPair transportKeyPair;
|
|
|
+ final X509Certificate transportCaCert;
|
|
|
+ final PrivateKey transportKey;
|
|
|
final X509Certificate transportCert;
|
|
|
- final KeyPair httpCAKeyPair;
|
|
|
- final X509Certificate httpCACert;
|
|
|
- final KeyPair httpKeyPair;
|
|
|
+ final PrivateKey httpCaKey;
|
|
|
+ final X509Certificate httpCaCert;
|
|
|
+ final PrivateKey httpKey;
|
|
|
final X509Certificate httpCert;
|
|
|
+ final List<String> transportAddresses;
|
|
|
+
|
|
|
+ if (inEnrollmentMode) {
|
|
|
+ // this is an enrolling node, get HTTP CA key/certificate and transport layer key/certificate from another node
|
|
|
+ final EnrollmentToken enrollmentToken;
|
|
|
+ try {
|
|
|
+ enrollmentToken = EnrollmentToken.decodeFromString(enrollmentTokenParam.value(options));
|
|
|
+ } catch (Exception e) {
|
|
|
+ try {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ } catch (Exception ex) {
|
|
|
+ e.addSuppressed(ex);
|
|
|
+ }
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Failed to parse enrollment token : " + enrollmentTokenParam.value(options) + " . Error was: " + e.getMessage()
|
|
|
+ );
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ throw new UserException(ExitCodes.DATA_ERROR, "Aborting auto configuration. 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.
|
|
|
+ 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 (enrollResponse == null || enrollResponse.getHttpStatus() != 200) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.UNAVAILABLE,
|
|
|
+ "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<String, Object> responseMap = enrollResponse.getResponseBody();
|
|
|
+ if (responseMap == null) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.DATA_ERROR,
|
|
|
+ "Aborting enrolling to cluster. Empty response when calling the enroll node API (" + enrollNodeUrl + ")"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ final List<String> missingFields = new ArrayList<>();
|
|
|
+ final String httpCaKeyPem = (String) responseMap.get("http_ca_key");
|
|
|
+ if (Strings.isNullOrEmpty(httpCaKeyPem)) {
|
|
|
+ missingFields.add("http_ca_key");
|
|
|
+ }
|
|
|
+ final String httpCaCertPem = (String) responseMap.get("http_ca_cert");
|
|
|
+ if (Strings.isNullOrEmpty(httpCaCertPem)) {
|
|
|
+ missingFields.add("http_ca_cert");
|
|
|
+ }
|
|
|
+ final String transportKeyPem = (String) responseMap.get("transport_key");
|
|
|
+ if (Strings.isNullOrEmpty(transportKeyPem)) {
|
|
|
+ missingFields.add("transport_key");
|
|
|
+ }
|
|
|
+ final String transportCaCertPem = (String) responseMap.get("transport_ca_cert");
|
|
|
+ if (Strings.isNullOrEmpty(transportCaCertPem)) {
|
|
|
+ missingFields.add("transport_ca_cert");
|
|
|
+ }
|
|
|
+ final String transportCertPem = (String) responseMap.get("transport_cert");
|
|
|
+ if (Strings.isNullOrEmpty(transportCertPem)) {
|
|
|
+ missingFields.add("transport_cert");
|
|
|
+ }
|
|
|
+ transportAddresses = getTransportAddresses(responseMap);
|
|
|
+ if (null == transportAddresses || transportAddresses.isEmpty()) {
|
|
|
+ missingFields.add("nodes_addresses");
|
|
|
+ }
|
|
|
+ if (false == missingFields.isEmpty()) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.DATA_ERROR,
|
|
|
+ "Aborting enrolling to cluster. Invalid response when calling the enroll node API ("
|
|
|
+ + enrollNodeUrl
|
|
|
+ + "). "
|
|
|
+ + "The following fields were empty or missing : "
|
|
|
+ + missingFields
|
|
|
+ );
|
|
|
+ }
|
|
|
+ transportCaCert = parseCertificateFromPem(transportCaCertPem, terminal);
|
|
|
+ httpCaKey = parseKeyFromPem(httpCaKeyPem, terminal);
|
|
|
+ httpCaCert = parseCertificateFromPem(httpCaCertPem, terminal);
|
|
|
+ transportKey = parseKeyFromPem(transportKeyPem, terminal);
|
|
|
+ transportCert = parseCertificateFromPem(transportCertPem, terminal);
|
|
|
+ } else {
|
|
|
+ // this is the initial node, generate HTTP CA key/certificate and transport layer key/certificate ourselves
|
|
|
+ try {
|
|
|
+ transportAddresses = List.of();
|
|
|
+ final X500Principal certificatePrincipal = new X500Principal("CN=" + System.getenv("HOSTNAME"));
|
|
|
+ final X500Principal caPrincipal = new X500Principal(AUTO_CONFIG_ALT_DN);
|
|
|
+ final GeneralNames subjectAltNames = getSubjectAltNames();
|
|
|
+ // self-signed CA for transport layer
|
|
|
+ final KeyPair transportCaKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_CA_KEY_SIZE);
|
|
|
+ final PrivateKey transportCaKey = transportCaKeyPair.getPrivate();
|
|
|
+ transportCaCert = CertGenUtils.generateSignedCertificate(
|
|
|
+ caPrincipal,
|
|
|
+ null,
|
|
|
+ transportCaKeyPair,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ true,
|
|
|
+ TRANSPORT_CA_CERTIFICATE_DAYS,
|
|
|
+ SIGNATURE_ALGORITHM
|
|
|
+ );
|
|
|
+ // transport key/certificate
|
|
|
+ final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
|
|
|
+ transportKey = transportKeyPair.getPrivate();
|
|
|
+ transportCert = CertGenUtils.generateSignedCertificate(
|
|
|
+ certificatePrincipal,
|
|
|
+ subjectAltNames,
|
|
|
+ transportKeyPair,
|
|
|
+ transportCaCert,
|
|
|
+ transportCaKey,
|
|
|
+ false,
|
|
|
+ TRANSPORT_CERTIFICATE_DAYS,
|
|
|
+ SIGNATURE_ALGORITHM
|
|
|
+ );
|
|
|
+ final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
|
|
|
+ httpCaKey = httpCaKeyPair.getPrivate();
|
|
|
+ // self-signed CA
|
|
|
+ httpCaCert = CertGenUtils.generateSignedCertificate(
|
|
|
+ caPrincipal,
|
|
|
+ null,
|
|
|
+ httpCaKeyPair,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ true,
|
|
|
+ HTTP_CA_CERTIFICATE_DAYS,
|
|
|
+ SIGNATURE_ALGORITHM
|
|
|
+ );
|
|
|
+ } catch (Throwable t) {
|
|
|
+ deleteDirectory(instantAutoConfigDir);
|
|
|
+ // this is an error which mustn't be ignored during node startup
|
|
|
+ // the exit code for unhandled Exceptions is "1"
|
|
|
+ throw t;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // in either case, generate this node's HTTP key/certificate
|
|
|
try {
|
|
|
- // the transport key-pair is the same across the cluster and is trusted without hostname verification (it is self-signed),
|
|
|
final X500Principal certificatePrincipal = new X500Principal("CN=" + System.getenv("HOSTNAME"));
|
|
|
- final X500Principal caPrincipal = new X500Principal(AUTO_CONFIG_ALT_DN);
|
|
|
- // this does DNS resolve and could block
|
|
|
final GeneralNames subjectAltNames = getSubjectAltNames();
|
|
|
|
|
|
- transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
|
|
|
- // self-signed which is not a CA
|
|
|
- transportCert = CertGenUtils.generateSignedCertificate(
|
|
|
- certificatePrincipal,
|
|
|
- subjectAltNames,
|
|
|
- transportKeyPair,
|
|
|
- null,
|
|
|
- null,
|
|
|
- false,
|
|
|
- TRANSPORT_CERTIFICATE_DAYS,
|
|
|
- "SHA256withRSA"
|
|
|
- );
|
|
|
- httpCAKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
|
|
|
- // self-signed CA
|
|
|
- httpCACert = CertGenUtils.generateSignedCertificate(
|
|
|
- caPrincipal,
|
|
|
- null,
|
|
|
- httpCAKeyPair,
|
|
|
- null,
|
|
|
- null,
|
|
|
- true,
|
|
|
- HTTP_CA_CERTIFICATE_DAYS,
|
|
|
- "SHA256withRSA"
|
|
|
- );
|
|
|
- httpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE);
|
|
|
+ final KeyPair httpKeyPair = CertGenUtils.generateKeyPair(HTTP_KEY_SIZE);
|
|
|
+ httpKey = httpKeyPair.getPrivate();
|
|
|
// non-CA
|
|
|
httpCert = CertGenUtils.generateSignedCertificate(
|
|
|
certificatePrincipal,
|
|
|
subjectAltNames,
|
|
|
httpKeyPair,
|
|
|
- httpCACert,
|
|
|
- httpCAKeyPair.getPrivate(),
|
|
|
+ httpCaCert,
|
|
|
+ httpCaKey,
|
|
|
false,
|
|
|
HTTP_CERTIFICATE_DAYS,
|
|
|
- "SHA256withRSA"
|
|
|
+ SIGNATURE_ALGORITHM
|
|
|
);
|
|
|
|
|
|
// the HTTP CA PEM file is provided "just in case". The node doesn't use it, but clients (configured manually, outside of the
|
|
|
// enrollment process) might indeed need it, and it is currently impossible to retrieve it
|
|
|
-
|
|
|
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);
|
|
|
+ pemWriter.writeObject(httpCaCert);
|
|
|
}
|
|
|
});
|
|
|
} catch (Throwable t) {
|
|
@@ -305,7 +461,7 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
// Still display a message, because this can be tricky to figure out (why auto-conf did not run) if by mistake.
|
|
|
throw new UserException(
|
|
|
ExitCodes.CONFIG,
|
|
|
- "Aborting auto configuration because the node keystore contains password " + "settings already"
|
|
|
+ "Aborting auto configuration because the node keystore contains password settings already"
|
|
|
);
|
|
|
}
|
|
|
try (SecureString transportKeystorePassword = newKeystorePassword()) {
|
|
@@ -314,12 +470,11 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
// the PKCS12 keystore and the contained private key use the same password
|
|
|
transportKeystore.setKeyEntry(
|
|
|
TRANSPORT_AUTOGENERATED_KEY_ALIAS,
|
|
|
- transportKeyPair.getPrivate(),
|
|
|
+ transportKey,
|
|
|
transportKeystorePassword.getChars(),
|
|
|
new Certificate[] { transportCert }
|
|
|
);
|
|
|
- // the transport keystore is used as a trustore too, hence it must contain a certificate entry
|
|
|
- transportKeystore.setCertificateEntry(TRANSPORT_AUTOGENERATED_CERT_ALIAS, transportCert);
|
|
|
+ transportKeystore.setCertificateEntry(TRANSPORT_AUTOGENERATED_CERT_ALIAS, transportCaCert);
|
|
|
fullyWriteFile(
|
|
|
instantAutoConfigDir,
|
|
|
TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12",
|
|
@@ -337,15 +492,15 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
// both keys are encrypted using the same password as the PKCS12 keystore they're contained in
|
|
|
httpKeystore.setKeyEntry(
|
|
|
HTTP_AUTOGENERATED_KEYSTORE_NAME + "_ca",
|
|
|
- httpCAKeyPair.getPrivate(),
|
|
|
+ httpCaKey,
|
|
|
httpKeystorePassword.getChars(),
|
|
|
- new Certificate[] { httpCACert }
|
|
|
+ new Certificate[] { httpCaCert }
|
|
|
);
|
|
|
httpKeystore.setKeyEntry(
|
|
|
HTTP_AUTOGENERATED_KEYSTORE_NAME,
|
|
|
- httpKeyPair.getPrivate(),
|
|
|
+ httpKey,
|
|
|
httpKeystorePassword.getChars(),
|
|
|
- new Certificate[] { httpCert, httpCACert }
|
|
|
+ new Certificate[] { httpCert, httpCaCert }
|
|
|
);
|
|
|
fullyWriteFile(
|
|
|
instantAutoConfigDir,
|
|
@@ -390,6 +545,7 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
bw.newLine();
|
|
|
}
|
|
|
bw.newLine();
|
|
|
+ bw.write(AUTO_CONFIGURATION_START_MARKER);
|
|
|
bw.newLine();
|
|
|
bw.write("###################################################################################");
|
|
|
bw.newLine();
|
|
@@ -397,11 +553,9 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
bw.newLine();
|
|
|
bw.write("# have been automatically generated in order to configure Security. #");
|
|
|
bw.newLine();
|
|
|
- bw.write("# These have been generated the first time that the new node was started, without #");
|
|
|
- bw.newLine();
|
|
|
- bw.write("# joining or enrolling to an existing cluster and only if Security had not been #");
|
|
|
+ bw.write("# These have been generated the first time that the new node was started, only #");
|
|
|
bw.newLine();
|
|
|
- bw.write("# explicitly configured beforehand. #");
|
|
|
+ bw.write("# if Security had not been explicitly configured beforehand. #");
|
|
|
bw.newLine();
|
|
|
bw.write(String.format(Locale.ROOT, "# %-79s #", ""));
|
|
|
bw.newLine();
|
|
@@ -449,19 +603,31 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
"xpack.security.http.ssl.keystore.path: " + instantAutoConfigDir.resolve(HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12")
|
|
|
);
|
|
|
bw.newLine();
|
|
|
-
|
|
|
- // we have configured TLS on the transport layer with newly generated certs and keys,
|
|
|
- // hence this node cannot form a multi-node cluster
|
|
|
- // if we don't set the following the node might trip the discovery bootstrap check
|
|
|
- if (false == DiscoveryModule.isSingleNodeDiscovery(env.settings())
|
|
|
- && false == ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.exists(env.settings())) {
|
|
|
+ if (inEnrollmentMode) {
|
|
|
bw.newLine();
|
|
|
- bw.write("# The initial node with security auto-configured must form a cluster on its own,");
|
|
|
+ bw.write("# We set seed.hosts so that the node can actually discover the existing nodes in the cluster");
|
|
|
bw.newLine();
|
|
|
- bw.write("# and all the subsequent nodes should be added via the node enrollment flow");
|
|
|
- bw.newLine();
|
|
|
- bw.write(ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.getKey() + ": [\"${HOSTNAME}\"]");
|
|
|
+ bw.write(
|
|
|
+ DISCOVERY_SEED_HOSTS_SETTING.getKey()
|
|
|
+ + ": ["
|
|
|
+ + transportAddresses.stream().map(p -> '"' + p + '"').collect(Collectors.joining(", "))
|
|
|
+ + "]"
|
|
|
+ );
|
|
|
bw.newLine();
|
|
|
+ } else {
|
|
|
+ // we have configured TLS on the transport layer with newly generated certs and keys,
|
|
|
+ // hence this node cannot form a multi-node cluster
|
|
|
+ // if we don't set the following the node might trip the discovery bootstrap check
|
|
|
+ if (false == DiscoveryModule.isSingleNodeDiscovery(env.settings())
|
|
|
+ && false == ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.exists(env.settings())) {
|
|
|
+ bw.newLine();
|
|
|
+ bw.write("# The initial node with security auto-configured must form a cluster on its own,");
|
|
|
+ bw.newLine();
|
|
|
+ bw.write("# and all the subsequent nodes should be added via the node enrollment flow");
|
|
|
+ bw.newLine();
|
|
|
+ bw.write(ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.getKey() + ": [\"${HOSTNAME}\"]");
|
|
|
+ bw.newLine();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// if any address settings have been set, assume the admin has thought it through wrt to addresses,
|
|
@@ -481,6 +647,8 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
bw.write(HttpTransportSettings.SETTING_HTTP_HOST.getKey() + ": [_local_, _site_]");
|
|
|
bw.newLine();
|
|
|
}
|
|
|
+ bw.write(AUTO_CONFIGURATION_END_MARKER);
|
|
|
+ bw.newLine();
|
|
|
}
|
|
|
});
|
|
|
} catch (Throwable t) {
|
|
@@ -510,6 +678,16 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
Files.deleteIfExists(keystoreBackupPath);
|
|
|
}
|
|
|
|
|
|
+ private void notifyOfFailure(boolean inEnrollmentMode, Terminal terminal, Terminal.Verbosity verbosity, int exitCode, String message)
|
|
|
+ throws UserException {
|
|
|
+ if (inEnrollmentMode) {
|
|
|
+ throw new UserException(exitCode, message);
|
|
|
+ } else {
|
|
|
+ terminal.println(verbosity, message);
|
|
|
+ throw new UserException(exitCode, null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
@SuppressForbidden(reason = "Uses File API because the commons io library does, which is useful for file manipulation")
|
|
|
private void deleteDirectory(Path directory) throws IOException {
|
|
|
FileUtils.deleteDirectory(directory.toFile());
|
|
@@ -536,66 +714,80 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
* in which case auto-configuration is SILENTLY skipped.
|
|
|
* This assumes the user knows what they are doing when configuring the node.
|
|
|
*/
|
|
|
- void checkExistingConfiguration(Settings settings, Terminal terminal) throws UserException {
|
|
|
+ void checkExistingConfiguration(Settings settings, boolean inEnrollmentMode, Terminal terminal) throws UserException {
|
|
|
// Allow the user to explicitly set that they don't want auto-configuration for security, regardless of our heuristics
|
|
|
if (XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED.get(settings) == false) {
|
|
|
- terminal.println(
|
|
|
+ notifyOfFailure(
|
|
|
+ inEnrollmentMode,
|
|
|
+ terminal,
|
|
|
Terminal.Verbosity.VERBOSE,
|
|
|
+ ExitCodes.NOOP,
|
|
|
"Skipping security auto configuration because [" + XPackSettings.SECURITY_AUTOCONFIGURATION_ENABLED.getKey() + "] is false"
|
|
|
);
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skip security auto configuration when Security is already configured.
|
|
|
+ // Skip security auto configuration when Security is already configured.
|
|
|
// Security is enabled implicitly, but if the admin chooses to enable it explicitly then
|
|
|
// skip the TLS auto-configuration, as this is a sign that the admin is opting for the default behavior
|
|
|
if (XPackSettings.SECURITY_ENABLED.exists(settings)) {
|
|
|
// do not try to validate, correct or fill in any incomplete security configuration,
|
|
|
// instead rely on the regular node startup to do this validation
|
|
|
- terminal.println(
|
|
|
+ notifyOfFailure(
|
|
|
+ inEnrollmentMode,
|
|
|
+ terminal,
|
|
|
Terminal.Verbosity.VERBOSE,
|
|
|
+ ExitCodes.NOOP,
|
|
|
"Skipping security auto configuration because it appears that security is already configured."
|
|
|
);
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
+
|
|
|
+ // Skipping security auto configuration because TLS is already configured
|
|
|
+ if (false == settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX).isEmpty()
|
|
|
+ || false == settings.getByPrefix(XPackSettings.HTTP_SSL_PREFIX).isEmpty()) {
|
|
|
+ // zero validation for the TLS settings as well, let the node boot and do its thing
|
|
|
+ notifyOfFailure(
|
|
|
+ inEnrollmentMode,
|
|
|
+ terminal,
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ ExitCodes.NOOP,
|
|
|
+ "Skipping security auto configuration because it appears that TLS is already configured."
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
// Security auto configuration must not run if the node is configured for multi-node cluster formation (bootstrap or join).
|
|
|
// This is because transport TLS with newly generated certs will hinder cluster formation because the other nodes cannot trust yet.
|
|
|
if (false == isInitialClusterNode(settings)) {
|
|
|
- terminal.println(
|
|
|
+ notifyOfFailure(
|
|
|
+ inEnrollmentMode,
|
|
|
+ terminal,
|
|
|
Terminal.Verbosity.VERBOSE,
|
|
|
+ ExitCodes.NOOP,
|
|
|
"Skipping security auto configuration because this node is configured to bootstrap or to join a "
|
|
|
+ "multi-node cluster, which is not supported."
|
|
|
);
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
- }
|
|
|
- // Silently skip security auto configuration because node cannot become master.
|
|
|
- boolean canBecomeMaster = DiscoveryNode.isMasterNode(settings)
|
|
|
- && false == DiscoveryNode.hasRole(settings, DiscoveryNodeRole.VOTING_ONLY_NODE_ROLE);
|
|
|
- if (false == canBecomeMaster) {
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.VERBOSE,
|
|
|
- "Skipping security auto configuration because the node is configured such that it cannot become master."
|
|
|
- );
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
- }
|
|
|
- // Silently skip security auto configuration, because the node cannot contain the Security index data
|
|
|
- boolean canHoldSecurityIndex = DiscoveryNode.canContainData(settings);
|
|
|
- if (false == canHoldSecurityIndex) {
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.VERBOSE,
|
|
|
- "Skipping security auto configuration because the node is configured such that it cannot contain data."
|
|
|
- );
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
}
|
|
|
- // Silently skipping security auto configuration because TLS is already configured
|
|
|
- if (false == settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX).isEmpty()
|
|
|
- || false == settings.getByPrefix(XPackSettings.HTTP_SSL_PREFIX).isEmpty()) {
|
|
|
- // zero validation for the TLS settings as well, let the node boot and do its thing
|
|
|
- terminal.println(
|
|
|
- Terminal.Verbosity.VERBOSE,
|
|
|
- "Skipping security auto configuration because it appears that TLS is already configured."
|
|
|
- );
|
|
|
- throw new UserException(ExitCodes.NOOP, null);
|
|
|
+
|
|
|
+ if (inEnrollmentMode == false) {
|
|
|
+ // Silently skip security auto configuration because node cannot become master.
|
|
|
+ boolean canBecomeMaster = DiscoveryNode.isMasterNode(settings)
|
|
|
+ && false == DiscoveryNode.hasRole(settings, DiscoveryNodeRole.VOTING_ONLY_NODE_ROLE);
|
|
|
+ if (false == canBecomeMaster) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because the node is configured such that it cannot become master."
|
|
|
+ );
|
|
|
+ throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ }
|
|
|
+ // Silently skip security auto configuration, because the node cannot contain the Security index data
|
|
|
+ boolean canHoldSecurityIndex = DiscoveryNode.canContainData(settings);
|
|
|
+ if (false == canHoldSecurityIndex) {
|
|
|
+ terminal.println(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Skipping security auto configuration because the node is configured such that it cannot contain data."
|
|
|
+ );
|
|
|
+ throw new UserException(ExitCodes.NOOP, null);
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
// auto-configuration runs even if the realms are configured in any way,
|
|
|
// including defining file based users (defining realms is permitted without touching
|
|
|
// the xpack.security.enabled setting)
|
|
@@ -658,4 +850,55 @@ public final class ConfigInitialNode extends EnvironmentAwareCommand {
|
|
|
return false == dirContentsStream.findAny().isPresent();
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private X509Certificate parseCertificateFromPem(String pemFormattedCert, Terminal terminal) throws Exception {
|
|
|
+ try {
|
|
|
+ final List<Certificate> 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");
|
|
|
+ }
|
|
|
+ return (X509Certificate) certs.get(0);
|
|
|
+ } catch (Exception e) {
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Failed to parse Certificate from the response of the Enroll Node API: " + e.getMessage()
|
|
|
+ );
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.DATA_ERROR,
|
|
|
+ "Aborting enrolling to cluster. Failed to parse Certificate from the response of the Enroll Node API",
|
|
|
+ e
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private PrivateKey parseKeyFromPem(String pemFormattedKey, Terminal terminal) throws UserException {
|
|
|
+ try {
|
|
|
+ return parsePKCS8PemString(pemFormattedKey);
|
|
|
+ } catch (Exception e) {
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(
|
|
|
+ Terminal.Verbosity.VERBOSE,
|
|
|
+ "Failed to parse Private Key from the response of the Enroll Node API: " + e.getMessage()
|
|
|
+ );
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
|
|
+ terminal.errorPrintln(Terminal.Verbosity.VERBOSE, "");
|
|
|
+ throw new UserException(
|
|
|
+ ExitCodes.DATA_ERROR,
|
|
|
+ "Aborting enrolling to cluster. Failed to parse Private Key from the response of the Enroll Node API",
|
|
|
+ e
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<String> getTransportAddresses(Map<String, Object> responseMap) {
|
|
|
+ return (List<String>) responseMap.get("nodes_addresses");
|
|
|
+ }
|
|
|
}
|