Browse Source

Deprecate cert gen without a CA and add self-signed option (#64037)

Generating a CA on the fly is an attempt at workflow optimisation that was
inherited from certgen. There are potential pitfalls with this approach. Overall
it is recommended to separate the step of CA creation and mandate a CA to be
specified when generating certificate.

This PR add a deprecation message if the cert command is used without specifying
a CA. A follow up PR will throw error for this usage in 8.0.

For use case where we explicitly trust a certificate without needing a CA, e.g.
SAML message signing, the PR adds a --self-signed option to the cert sub-command
to generate self-signed certificate.
Yang Wang 4 years ago
parent
commit
bdd99b250f

+ 21 - 7
docs/reference/commands/certutil.asciidoc

@@ -18,7 +18,7 @@ bin/elasticsearch-certutil
 | (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
 [--ca-dn <name>] [--ca-pass <password>] [--days <n>]
 [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
-[--keep-ca-key] [--multiple] [--name <file_name>] [--pem])
+[--keep-ca-key] [--multiple] [--name <file_name>] [--pem] [--self-signed])
 
 | (csr [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
 [--name <file_name>])
@@ -73,15 +73,18 @@ directory name, you must also specify a file name in the `--name` command
 parameter or in the `filename` field in an input YAML file.
 
 You can optionally provide IP addresses or DNS names for each instance. If
-neither IP addresses nor DNS names are specified, the Elastic stack products
+neither IP addresses nor DNS names are specified, the Elastic Stack products
 cannot perform hostname verification and you might need to configure the
 `verification_mode` security setting to `certificate` only. For more information
 about this setting, see <<security-settings>>.
 
-All certificates that are generated by this command are signed by a CA. You can
-provide your own CA with the `--ca` or `--ca-cert` parameters. Otherwise, the
-command automatically generates a new CA for you. For more information about
-generating a CA, see the <<certutil-ca,CA mode of this command>>.
+All certificates that are generated by this command are signed by a CA unless
+the `--self-signed` parameter is specified. You can provide your own CA with the
+`--ca` or `--ca-cert` and `--ca-key` parameters. Otherwise, the command automatically generates a new CA for you.
+deprecated:[7.11.0,"Generating certificates without specifying a CA certificate and key is deprecated. In the next major version you must provide a CA certificate unless the `--self-signed` option is specified."]
+For more information about generating a CA, see the
+<<certutil-ca,CA mode of this command>>.
+To generate self-signed certificates, use the `--self-signed` parameter.
 
 By default, the `cert` mode produces a single PKCS#12 output file which holds
 the instance certificate, the instance private key, and the CA certificate. If
@@ -211,6 +214,17 @@ wish to password-protect your PEM keys, then do not specify
 `--pem`:: Generates certificates and keys in PEM format instead of PKCS#12. This
 parameter cannot be used with the `csr` parameter.
 
+`--self-signed`:: Generates self-signed certificates. This parameter is only
+applicable to the `cert` parameter.
++
+--
+NOTE: This option is not recommended for <<ssl-tls,setting up TLS on a cluster>>.
+In fact, a self-signed certificate should be used only when you can be sure
+that a CA is definitely not needed and trust is directly given to the
+certificate itself.
+
+--
+
 `-s, --silent`:: Shows minimal output.
 
 `-v, --verbose`:: Shows verbose output.
@@ -300,7 +314,7 @@ output file, there is a directory for each instance that was listed in the
 file, which contains the instance certificate, instance private key, and CA
 certificate.
 
-You an also use the YAML file to generate certificate signing requests. For
+You can also use the YAML file to generate certificate signing requests. For
 example:
 
 [source, sh]

+ 1 - 1
x-pack/docs/en/security/authentication/saml-guide.asciidoc

@@ -512,7 +512,7 @@ signing certificate with the following command:
 
 [source, sh]
 --------------------------------------------------
-bin/elasticsearch-certutil cert -pem -days 1100 -name saml-sign -out saml-sign.zip
+bin/elasticsearch-certutil cert --self-signed --pem --days 1100 --name saml-sign --out saml-sign.zip
 --------------------------------------------------
 
 This will

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

@@ -147,7 +147,7 @@ public class CertGenUtils {
      *                           empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
      * @return a signed {@link X509Certificate}
      */
-    private static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair,
+    public static X509Certificate generateSignedCertificate(X500Principal principal, GeneralNames subjectAltNames, KeyPair keyPair,
                                                              X509Certificate caCert, PrivateKey caPrivKey, boolean isCa,
                                                              int days, String signatureAlgorithm)
         throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {

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

@@ -166,9 +166,10 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             "      disable hostname verification in your SSL configuration.";
 
     static final String CA_EXPLANATION =
-        "    * All certificates generated by this tool will be signed by a certificate authority (CA).\n" +
-            "    * The tool can automatically generate a new CA for you, or you can provide your own with the\n" +
-            "         -ca or -ca-cert command line options.";
+        "    * All certificates generated by this tool will be signed by a certificate authority (CA)\n" +
+            "      unless the --self-signed command line option is specified.\n" +
+            "      The tool can automatically generate a new CA for you, or you can provide your own with\n" +
+            "      the --ca or --ca-cert command line options.";
 
 
     abstract static class CertificateCommand extends EnvironmentAwareCommand {
@@ -330,6 +331,9 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             } else if (options.has(caCertPathSpec)) {
                 return loadPemCA(terminal, options, env);
             } else {
+                terminal.println("Note: Generating certificates without providing a CA certificate is deprecated.");
+                terminal.println("      A CA certificate will become mandatory in the next major release.");
+                terminal.println("");
                 return generateCA(terminal, options);
             }
         }
@@ -645,12 +649,16 @@ public class CertificateTool extends LoggingAwareMultiCommand {
 
     static class GenerateCertificateCommand extends CertificateCommand {
 
+        OptionSpec<Void> selfSigned;
+
         GenerateCertificateCommand() {
             super("generate X.509 certificates and keys");
             acceptCertificateGenerationOptions();
             acceptInstanceDetails();
             acceptsCertificateAuthority();
             acceptInputFile();
+            selfSigned = parser.accepts("self-signed", "generate self signed certificates")
+                .availableUnless(caPkcs12PathSpec, caCertPathSpec);
         }
 
         @Override
@@ -710,7 +718,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 terminal.println(Terminal.Verbosity.NORMAL, "This file should be properly secured as it contains the private key for ");
                 terminal.print(Terminal.Verbosity.NORMAL, "your instance");
             }
-            if (caInfo.generated && keepCaKey) {
+            if (caInfo != null && caInfo.generated && keepCaKey) {
                 terminal.println(Terminal.Verbosity.NORMAL, " and for the certificate authority.");
             } else {
                 terminal.println(Terminal.Verbosity.NORMAL, ".");
@@ -735,12 +743,17 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             terminal.println(filesDescription + " to the relevant configuration directory");
             terminal.println("and then follow the SSL configuration instructions in the product guide.");
             terminal.println("");
-            if (usePemFormat || caInfo.generated == false) {
+            if (usePemFormat || (caInfo != null && caInfo.generated == false)) {
                 terminal.println("For client applications, you may only need to copy the CA certificate and");
                 terminal.println("configure the client to trust this certificate.");
             }
         }
 
+        @Override
+        CAInfo getCAInfo(Terminal terminal, OptionSet options, Environment env) throws Exception {
+            return options.has(selfSigned) ? null : super.getCAInfo(terminal, options, env);
+        }
+
         /**
          * Generates signed certificates in either PKCS#12 format or PEM format, wrapped in a zip file if necessary.
          *
@@ -765,7 +778,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 final boolean usePassword = super.useOutputPassword(options);
                 fullyWriteZipFile(output, (outputStream, pemWriter) -> {
                     // write out the CA info first if it was generated
-                    if (caInfo.generated) {
+                    if (caInfo != null && caInfo.generated) {
                         final boolean writeCAKey = keepCaKey(options);
                         if (usePem) {
                             writeCAInfo(outputStream, pemWriter, caInfo, writeCAKey);
@@ -807,7 +820,8 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                         } else {
                             final String fileName = entryBase + ".p12";
                             outputStream.putNextEntry(new ZipEntry(fileName));
-                            writePkcs12(fileName, outputStream, certificateInformation.name.originalName, pair, caInfo.certAndKey.cert,
+                            writePkcs12(fileName, outputStream, certificateInformation.name.originalName, pair,
+                                caInfo == null ? null : caInfo.certAndKey.cert,
                                 outputPassword, terminal);
                             outputStream.closeEntry();
                         }
@@ -818,17 +832,26 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 CertificateInformation certificateInformation = certs.iterator().next();
                 CertificateAndKey pair = generateCertificateAndKey(certificateInformation, caInfo, keySize, days);
                 fullyWriteFile(output, stream -> writePkcs12(output.getFileName().toString(), stream,
-                    certificateInformation.name.originalName, pair, caInfo.certAndKey.cert, outputPassword, terminal));
+                    certificateInformation.name.originalName, pair,
+                    caInfo == null ? null : caInfo.certAndKey.cert, outputPassword, terminal));
             }
         }
 
         private CertificateAndKey generateCertificateAndKey(CertificateInformation certificateInformation, CAInfo caInfo,
                                                             int keySize, int days) throws Exception {
             KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
-            Certificate certificate = CertGenUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
-                getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
-                    certificateInformation.commonNames),
-                keyPair, caInfo.certAndKey.cert, caInfo.certAndKey.key, days);
+            Certificate certificate;
+            if (caInfo != null) {
+                certificate = CertGenUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
+                    getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
+                        certificateInformation.commonNames),
+                    keyPair, caInfo.certAndKey.cert, caInfo.certAndKey.key, days);
+            } else {
+                certificate = CertGenUtils.generateSignedCertificate(certificateInformation.name.x500Principal,
+                    getSubjectAlternativeNamesValue(certificateInformation.ipAddresses, certificateInformation.dnsNames,
+                        certificateInformation.commonNames),
+                    keyPair, null, null, false, days, null);
+            }
             return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate());
         }
 

+ 45 - 15
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java

@@ -116,7 +116,7 @@ public class CertificateToolTests extends ESTestCase {
     }
 
     @BeforeClass
-    public static void chechFipsJvm() {
+    public static void checkFipsJvm() {
         assumeFalse("Can't run in a FIPS JVM, depends on Non FIPS BouncyCastle", inFipsJvm());
     }
 
@@ -305,19 +305,21 @@ public class CertificateToolTests extends ESTestCase {
         KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
         X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
 
+        final boolean selfSigned = randomBoolean();
         final boolean generatedCa = randomBoolean();
         final boolean keepCaKey = generatedCa && randomBoolean();
         final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
 
         assertFalse(Files.exists(outputFile));
-        CAInfo caInfo = new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
+        CAInfo caInfo = selfSigned ? null :
+            new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
         final GenerateCertificateCommand command = new GenerateCertificateCommand();
         List<String> args = CollectionUtils.arrayAsArrayList("-keysize", String.valueOf(keySize), "-days", String.valueOf(days), "-pem");
         if (keyPassword != null) {
             args.add("-pass");
             args.add(keyPassword);
         }
-        if (keepCaKey) {
+        if (selfSigned == false && keepCaKey) {
             args.add("-keep-ca-key");
         }
         final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
@@ -333,7 +335,7 @@ public class CertificateToolTests extends ESTestCase {
         FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
         Path zipRoot = fileSystem.getPath("/");
 
-        if (generatedCa) {
+        if (selfSigned == false && generatedCa) {
             assertTrue(Files.exists(zipRoot.resolve("ca")));
             assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt")));
             // check the CA cert
@@ -393,6 +395,10 @@ public class CertificateToolTests extends ESTestCase {
             try (InputStream input = Files.newInputStream(cert)) {
                 X509Certificate certificate = readX509Certificate(input);
                 assertEquals(certInfo.name.x500Principal.toString(), certificate.getSubjectX500Principal().getName());
+                if (selfSigned) {
+                    assertEquals(certificate.getSubjectX500Principal(), certificate.getIssuerX500Principal());
+                    assertEquals(-1, certificate.getBasicConstraints());
+                }
                 final int sanCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
                 if (sanCount == 0) {
                     assertNull(certificate.getSubjectAlternativeNames());
@@ -467,14 +473,19 @@ public class CertificateToolTests extends ESTestCase {
         options = command.getParser().parse(Strings.toStringArray(args));
         caInfo = command.getCAInfo(terminal, options, env);
         caCK = caInfo.certAndKey;
-
-        assertTrue(terminal.getOutput().isEmpty());
+        assertThat(terminal.getOutput(),
+            containsString("Generating certificates without providing a CA certificate is deprecated."));
         assertThat(caCK.cert, instanceOf(X509Certificate.class));
         assertEquals(caCK.cert.getSubjectX500Principal().getName(), "CN=foo bar");
         assertThat(caCK.key.getAlgorithm(), containsString("RSA"));
         assertTrue(caInfo.generated);
         assertEquals(keySize, getKeySize(caCK.key));
         assertEquals(days, getDurationInDays(caCK.cert));
+
+        // test self-signed
+        args = CollectionUtils.arrayAsArrayList("-self-signed");
+        options = command.getParser().parse(Strings.toStringArray(args));
+        assertNull(command.getCAInfo(terminal, options, env));
     }
 
     public void testNameValues() throws Exception {
@@ -540,7 +551,7 @@ public class CertificateToolTests extends ESTestCase {
      * A multi-stage test that:
      * - Create a new CA
      * - Uses that CA to create 2 node certificates
-     * - Creates a 3rd node certificate using an auto-generated CA
+     * - Creates a 3rd node certificate using an auto-generated CA or a self-signed cert
      * - Checks that the first 2 node certificates trust one another
      * - Checks that the 3rd node certificate is _not_ trusted
      * - Checks that all 3 certificates have the right values based on the command line options provided during generation
@@ -620,16 +631,25 @@ public class CertificateToolTests extends ESTestCase {
 
         assertThat(node2File, pathExists());
 
-        // Node 3 uses an auto generated CA, and therefore should not be trusted by the other nodes.
+        // Node 3 uses an auto generated CA or a self-signed cert, and therefore should not be trusted by the other nodes.
+        final List<String> gen3Args = CollectionUtils.arrayAsArrayList(
+            "-pass", node3Password,
+            "-out", "<node3>",
+            "-keysize", String.valueOf(node3KeySize),
+            "-days", String.valueOf(days),
+            "-dns", "node03.cluster2.es.internal.corp.net",
+            "-ip", node3Ip
+        );
+        final boolean selfSigned = randomBoolean();
+        if (selfSigned) {
+            gen3Args.add("-self-signed");
+        } else {
+            gen3Args.add("-ca-dn");
+            gen3Args.add("CN=My ElasticSearch Cluster 2");
+        }
         final GenerateCertificateCommand gen3Command = new PathAwareGenerateCertificateCommand(null, node3File);
         final OptionSet gen3Options = gen3Command.getParser().parse(
-                "-ca-dn", "CN=My ElasticSearch Cluster 2",
-                "-pass", node3Password,
-                "-out", "<node3>",
-                "-keysize", String.valueOf(node3KeySize),
-                "-days", String.valueOf(days),
-                "-dns", "node03.cluster2.es.internal.corp.net",
-                "-ip", node3Ip);
+                Strings.toStringArray(gen3Args));
         gen3Command.execute(terminal, gen3Options, env);
 
         assertThat(node3File, pathExists());
@@ -666,6 +686,16 @@ public class CertificateToolTests extends ESTestCase {
         assertThat(getDurationInDays((X509Certificate) node3Cert), equalTo(days));
         final Key node3Key = node3KeyStore.getKey(CertificateTool.DEFAULT_CERT_NAME, node3Password.toCharArray());
         assertThat(getKeySize(node3Key), equalTo(node3KeySize));
+        final Certificate[] certificateChain = node3KeyStore.getCertificateChain(CertificateTool.DEFAULT_CERT_NAME);
+        final X509Certificate node3x509Certificate = (X509Certificate) certificateChain[0];
+        if (selfSigned) {
+            assertEquals(1, certificateChain.length);
+            assertEquals(node3x509Certificate.getSubjectX500Principal(), node3x509Certificate.getIssuerX500Principal());
+        } else {
+            assertEquals(2, certificateChain.length);
+            assertEquals(node3x509Certificate.getIssuerX500Principal(),
+                ((X509Certificate) certificateChain[1]).getSubjectX500Principal());
+        }
     }