Browse Source

Remove support of creating CA on the fly when generating certificates (#65590)

Generating certificates with the cert sub-command now requires either: 1) a CA
to be provided with --ca or --ca-cert/--ca-key; or 2) make them self-signed
with the --self-signed option. Generating a CA on the fly is no longer
supported. The --keep-ca-key option is removed and the tool throws an error 
saying the CA needs to be generated separately if the option is specified.

This is a follow-up PR for #61884, which deprecated the "ca-on-the-fly" usage.
Yang Wang 4 years ago
parent
commit
b018c761e9

+ 5 - 9
docs/reference/commands/certutil.asciidoc

@@ -18,7 +18,7 @@ bin/elasticsearch-certutil
 | (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
 | (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
 [--ca-dn <name>] [--ca-pass <password>] [--days <n>]
 [--ca-dn <name>] [--ca-pass <password>] [--days <n>]
 [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
 [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
-[--keep-ca-key] [--multiple] [--name <file_name>] [--pem] [--self-signed])
+[--multiple] [--name <file_name>] [--pem] [--self-signed])
 
 
 | (csr [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
 | (csr [--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
 [--name <file_name>])
 [--name <file_name>])
@@ -79,9 +79,8 @@ cannot perform hostname verification and you might need to configure the
 about this setting, see <<security-settings>>.
 about this setting, see <<security-settings>>.
 
 
 All certificates that are generated by this command are signed by a CA unless
 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."]
+the `--self-signed` parameter is specified. You must provide your own CA with the
+`--ca` or `--ca-cert` and `--ca-key` parameters unless `--self-signed` is specified.
 For more information about generating a CA, see the
 For more information about generating a CA, see the
 <<certutil-ca,CA mode of this command>>.
 <<certutil-ca,CA mode of this command>>.
 To generate self-signed certificates, use the `--self-signed` parameter.
 To generate self-signed certificates, use the `--self-signed` parameter.
@@ -90,7 +89,7 @@ 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
 the instance certificate, the instance private key, and the CA certificate. If
 you specify the `--pem` parameter, the command generates PEM formatted
 you specify the `--pem` parameter, the command generates PEM formatted
 certificates and keys and packages them into a zip file.
 certificates and keys and packages them into a zip file.
-If you specify the `--keep-ca-key`, `--multiple` or `--in` parameters,
+If you specify the `--multiple` or `--in` parameters,
 the command produces a zip file containing the generated certificates and keys.
 the command produces a zip file containing the generated certificates and keys.
 
 
 [discrete]
 [discrete]
@@ -180,9 +179,6 @@ parameter.
 `--ip <IP_addresses>`:: Specifies a comma-separated list of IP addresses. This
 `--ip <IP_addresses>`:: Specifies a comma-separated list of IP addresses. This
 parameter cannot be used with the `ca` parameter.
 parameter cannot be used with the `ca` parameter.
 
 
-`--keep-ca-key`:: When running in `cert` mode with an automatically-generated
-CA, specifies to retain the CA private key for future use.
-
 `--keysize <bits>`::
 `--keysize <bits>`::
 Defines the number of bits that are used in generated RSA keys. The default
 Defines the number of bits that are used in generated RSA keys. The default
 value is `2048`.
 value is `2048`.
@@ -305,7 +301,7 @@ parameter to specify the location of the file. For example:
 
 
 [source, sh]
 [source, sh]
 --------------------------------------------------
 --------------------------------------------------
-bin/elasticsearch-certutil cert --silent --in instances.yml --out test1.zip --pass testpassword --keep-ca-key
+bin/elasticsearch-certutil cert --silent --in instances.yml --out test1.zip --pass testpassword --ca elastic-stack-ca.p12
 --------------------------------------------------
 --------------------------------------------------
 
 
 This command generates a compressed `test1.zip` file. After you decompress the
 This command generates a compressed `test1.zip` file. After you decompress the

+ 2 - 1
x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc

@@ -72,7 +72,8 @@ services:
     command: >
     command: >
       bash -c '
       bash -c '
         if [[ ! -f /certs/bundle.zip ]]; then
         if [[ ! -f /certs/bundle.zip ]]; then
-          bin/elasticsearch-certutil cert --silent --pem --in config/certificates/instances.yml -out /certs/bundle.zip;
+          bin/elasticsearch-certutil ca --silent --out /certs/ca.p12 --pass ""; <1>
+          bin/elasticsearch-certutil cert --silent --pem --ca /certs/ca.p12 --in config/certificates/instances.yml -out /certs/bundle.zip;
           unzip /certs/bundle.zip -d /certs; <1>
           unzip /certs/bundle.zip -d /certs; <1>
         fi;
         fi;
         chown -R 1000:0 /certs
         chown -R 1000:0 /certs

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

@@ -5,6 +5,7 @@
  */
  */
 package org.elasticsearch.xpack.security.cli;
 package org.elasticsearch.xpack.security.cli;
 
 
+import joptsimple.OptionException;
 import joptsimple.OptionParser;
 import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 import joptsimple.OptionSpec;
@@ -145,6 +146,21 @@ public class CertificateTool extends LoggingAwareMultiCommand {
         subcommands.put("http", new HttpCertificateCommand());
         subcommands.put("http", new HttpCertificateCommand());
     }
     }
 
 
+    @Override
+    protected void execute(Terminal terminal, OptionSet options) throws Exception {
+        try {
+            super.execute(terminal, options);
+        } catch (OptionException e) {
+            if (e.options().size() == 1 && e.options().contains("keep-ca-key")) {
+                throw new UserException(ExitCodes.USAGE,
+                    "Generating certificates without providing a CA is no longer supported.\n" +
+                        "Please first generate a CA with the 'ca' sub-command and provide the ca file \n" +
+                        "with either --ca or --ca-cert/--ca-key to generate certificates.");
+            } else {
+                throw e;
+            }
+        }
+    }
 
 
     static final String INTRO_TEXT = "This tool assists you in the generation of X.509 certificates and certificate\n" +
     static final String INTRO_TEXT = "This tool assists you in the generation of X.509 certificates and certificate\n" +
         "signing requests for use with SSL/TLS in the Elastic stack.";
         "signing requests for use with SSL/TLS in the Elastic stack.";
@@ -187,7 +203,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
         OptionSpec<String> caKeyPathSpec;
         OptionSpec<String> caKeyPathSpec;
         OptionSpec<String> caPasswordSpec;
         OptionSpec<String> caPasswordSpec;
         OptionSpec<String> caDnSpec;
         OptionSpec<String> caDnSpec;
-        OptionSpec<Void> keepCaKeySpec;
 
 
         OptionSpec<Void> multipleNodesSpec;
         OptionSpec<Void> multipleNodesSpec;
         OptionSpec<String> nameSpec;
         OptionSpec<String> nameSpec;
@@ -219,10 +234,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 .requiredIf(caCertPathSpec)
                 .requiredIf(caCertPathSpec)
                 .withRequiredArg();
                 .withRequiredArg();
 
 
-            keepCaKeySpec = parser.accepts("keep-ca-key", "retain the CA private key for future use")
-                .availableUnless(caPkcs12PathSpec)
-                .availableUnless(caCertPathSpec);
-
             caPasswordSpec = parser.accepts("ca-pass", "password for an existing ca private key or the generated ca private key")
             caPasswordSpec = parser.accepts("ca-pass", "password for an existing ca private key or the generated ca private key")
                 .withOptionalArg();
                 .withOptionalArg();
 
 
@@ -295,10 +306,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             }
             }
         }
         }
 
 
-        boolean keepCaKey(OptionSet options) {
-            return options.has(keepCaKeySpec);
-        }
-
         boolean usePemFormat(OptionSet options) {
         boolean usePemFormat(OptionSet options) {
             return options.has(pemFormatSpec);
             return options.has(pemFormatSpec);
         }
         }
@@ -521,24 +528,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             }
             }
         }
         }
 
 
-        /**
-         * This method handles writing out the certificate authority in PKCS#12 format to a zip file.
-         *
-         * @param outputStream the output stream to write to
-         * @param info         the certificate authority information
-         * @param terminal     used to prompt for a password (if not already supplied)
-         */
-        static void writeCAInfo(ZipOutputStream outputStream, CAInfo info, Terminal terminal) throws Exception {
-            final String dirName = createCaDirectory(outputStream);
-            final String fileName = dirName + "ca.p12";
-            outputStream.putNextEntry(new ZipEntry(fileName));
-            withPassword("Generated CA", info.password, terminal, caPassword -> {
-                writePkcs12(fileName, outputStream, "ca", info.certAndKey, null, caPassword, null);
-                return null;
-            });
-            outputStream.closeEntry();
-        }
-
         private static String createCaDirectory(ZipOutputStream outputStream) throws IOException {
         private static String createCaDirectory(ZipOutputStream outputStream) throws IOException {
             final String caDirName = "ca/";
             final String caDirName = "ca/";
             ZipEntry zipEntry = new ZipEntry(caDirName);
             ZipEntry zipEntry = new ZipEntry(caDirName);
@@ -684,7 +673,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
             terminal.println("");
             terminal.println("");
             terminal.println("If you specify any of the following options:");
             terminal.println("If you specify any of the following options:");
             terminal.println("    * -pem (PEM formatted output)");
             terminal.println("    * -pem (PEM formatted output)");
-            terminal.println("    * -keep-ca-key (retain generated CA key)");
             terminal.println("    * -multiple (generate multiple certificates)");
             terminal.println("    * -multiple (generate multiple certificates)");
             terminal.println("    * -in (generate certificates from an input file)");
             terminal.println("    * -in (generate certificates from an input file)");
             terminal.println("then the output will be be a zip file containing individual certificate/key files");
             terminal.println("then the output will be be a zip file containing individual certificate/key files");
@@ -692,9 +680,8 @@ public class CertificateTool extends LoggingAwareMultiCommand {
 
 
             CAInfo caInfo = getCAInfo(terminal, options, env);
             CAInfo caInfo = getCAInfo(terminal, options, env);
             Collection<CertificateInformation> certInfo = getCertificateInformationList(terminal, options);
             Collection<CertificateInformation> certInfo = getCertificateInformationList(terminal, options);
-            final boolean keepCaKey = keepCaKey(options);
             final boolean usePemFormat = usePemFormat(options);
             final boolean usePemFormat = usePemFormat(options);
-            final boolean writeZipFile = options.has(multipleNodesSpec) || options.has(inputFileSpec) || keepCaKey || usePemFormat;
+            final boolean writeZipFile = options.has(multipleNodesSpec) || options.has(inputFileSpec) || usePemFormat;
 
 
             final String outputName;
             final String outputName;
             if (writeZipFile) {
             if (writeZipFile) {
@@ -716,12 +703,7 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 terminal.print(Terminal.Verbosity.NORMAL, "all instances");
                 terminal.print(Terminal.Verbosity.NORMAL, "all instances");
             } else {
             } else {
                 terminal.println(Terminal.Verbosity.NORMAL, "This file should be properly secured as it contains the private key for ");
                 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 != null && caInfo.generated && keepCaKey) {
-                terminal.println(Terminal.Verbosity.NORMAL, " and for the certificate authority.");
-            } else {
-                terminal.println(Terminal.Verbosity.NORMAL, ".");
+                terminal.print(Terminal.Verbosity.NORMAL, "your instance.");
             }
             }
             terminal.println("");
             terminal.println("");
             final String filesDescription;
             final String filesDescription;
@@ -751,6 +733,9 @@ public class CertificateTool extends LoggingAwareMultiCommand {
 
 
         @Override
         @Override
         CAInfo getCAInfo(Terminal terminal, OptionSet options, Environment env) throws Exception {
         CAInfo getCAInfo(Terminal terminal, OptionSet options, Environment env) throws Exception {
+            if (false == options.has(selfSigned) && false == options.has(caPkcs12PathSpec) && false == options.has(caCertPathSpec)) {
+                throw new UserException(ExitCodes.USAGE, "Must specify either --ca or --ca-cert/--ca-key or --self-signed");
+            }
             return options.has(selfSigned) ? null : super.getCAInfo(terminal, options, env);
             return options.has(selfSigned) ? null : super.getCAInfo(terminal, options, env);
         }
         }
 
 
@@ -777,16 +762,6 @@ public class CertificateTool extends LoggingAwareMultiCommand {
                 final boolean usePem = usePemFormat(options);
                 final boolean usePem = usePemFormat(options);
                 final boolean usePassword = super.useOutputPassword(options);
                 final boolean usePassword = super.useOutputPassword(options);
                 fullyWriteZipFile(output, (outputStream, pemWriter) -> {
                 fullyWriteZipFile(output, (outputStream, pemWriter) -> {
-                    // write out the CA info first if it was generated
-                    if (caInfo != null && caInfo.generated) {
-                        final boolean writeCAKey = keepCaKey(options);
-                        if (usePem) {
-                            writeCAInfo(outputStream, pemWriter, caInfo, writeCAKey);
-                        } else if (writeCAKey) {
-                            writeCAInfo(outputStream, caInfo, terminal);
-                        }
-                    }
-
                     for (CertificateInformation certificateInformation : certs) {
                     for (CertificateInformation certificateInformation : certs) {
                         CertificateAndKey pair = generateCertificateAndKey(certificateInformation, caInfo, keySize, days);
                         CertificateAndKey pair = generateCertificateAndKey(certificateInformation, caInfo, keySize, days);
 
 

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

@@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.cli;
 
 
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
 import com.google.common.jimfs.Jimfs;
+import joptsimple.NonOptionArgumentSpec;
 import joptsimple.OptionSet;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 import joptsimple.OptionSpec;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@@ -21,8 +22,6 @@ import org.bouncycastle.asn1.x509.Extensions;
 import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.asn1.x509.GeneralNames;
 import org.bouncycastle.asn1.x509.GeneralNames;
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.X509CertificateHolder;
-import org.bouncycastle.openssl.PEMDecryptorProvider;
-import org.bouncycastle.openssl.PEMEncryptedKeyPair;
 import org.bouncycastle.openssl.PEMParser;
 import org.bouncycastle.openssl.PEMParser;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.elasticsearch.cli.MockTerminal;
 import org.elasticsearch.cli.MockTerminal;
@@ -46,11 +45,9 @@ import org.elasticsearch.xpack.security.cli.CertificateTool.CertificateInformati
 import org.elasticsearch.xpack.security.cli.CertificateTool.GenerateCertificateCommand;
 import org.elasticsearch.xpack.security.cli.CertificateTool.GenerateCertificateCommand;
 import org.elasticsearch.xpack.security.cli.CertificateTool.Name;
 import org.elasticsearch.xpack.security.cli.CertificateTool.Name;
 import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
 import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
-import org.elasticsearch.xpack.core.ssl.PemUtils;
 import org.hamcrest.Matchers;
 import org.hamcrest.Matchers;
 import org.junit.After;
 import org.junit.After;
 import org.junit.BeforeClass;
 import org.junit.BeforeClass;
-import org.mockito.Mockito;
 
 
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.TrustManagerFactory;
@@ -72,7 +69,6 @@ import java.security.Key;
 import java.security.KeyPair;
 import java.security.KeyPair;
 import java.security.KeyStore;
 import java.security.KeyStore;
 import java.security.Principal;
 import java.security.Principal;
-import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAKey;
 import java.security.interfaces.RSAKey;
@@ -97,6 +93,9 @@ import static org.hamcrest.Matchers.in;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 
 /**
 /**
  * Unit tests for the tool used to simplify SSL certificate generation
  * Unit tests for the tool used to simplify SSL certificate generation
@@ -306,22 +305,17 @@ public class CertificateToolTests extends ESTestCase {
         X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
         X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
 
 
         final boolean selfSigned = randomBoolean();
         final boolean selfSigned = randomBoolean();
-        final boolean generatedCa = randomBoolean();
-        final boolean keepCaKey = generatedCa && randomBoolean();
         final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
         final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
 
 
         assertFalse(Files.exists(outputFile));
         assertFalse(Files.exists(outputFile));
         CAInfo caInfo = selfSigned ? null :
         CAInfo caInfo = selfSigned ? null :
-            new CAInfo(caCert, keyPair.getPrivate(), generatedCa, keyPassword == null ? null : keyPassword.toCharArray());
+            new CAInfo(caCert, keyPair.getPrivate(), false, keyPassword == null ? null : keyPassword.toCharArray());
         final GenerateCertificateCommand command = new GenerateCertificateCommand();
         final GenerateCertificateCommand command = new GenerateCertificateCommand();
         List<String> args = CollectionUtils.arrayAsArrayList("-keysize", String.valueOf(keySize), "-days", String.valueOf(days), "-pem");
         List<String> args = CollectionUtils.arrayAsArrayList("-keysize", String.valueOf(keySize), "-days", String.valueOf(days), "-pem");
         if (keyPassword != null) {
         if (keyPassword != null) {
             args.add("-pass");
             args.add("-pass");
             args.add(keyPassword);
             args.add(keyPassword);
         }
         }
-        if (selfSigned == false && keepCaKey) {
-            args.add("-keep-ca-key");
-        }
         final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
         final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
 
 
         command.generateAndWriteSignedCertificates(outputFile, true, options, certInfos, caInfo, null);
         command.generateAndWriteSignedCertificates(outputFile, true, options, certInfos, caInfo, null);
@@ -335,49 +329,7 @@ public class CertificateToolTests extends ESTestCase {
         FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
         FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + outputFile.toUri()), Collections.emptyMap());
         Path zipRoot = fileSystem.getPath("/");
         Path zipRoot = fileSystem.getPath("/");
 
 
-        if (selfSigned == false && generatedCa) {
-            assertTrue(Files.exists(zipRoot.resolve("ca")));
-            assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.crt")));
-            // check the CA cert
-            try (InputStream input = Files.newInputStream(zipRoot.resolve("ca").resolve("ca.crt"))) {
-                X509Certificate parsedCaCert = readX509Certificate(input);
-                assertThat(parsedCaCert.getSubjectX500Principal().getName(), containsString("test ca"));
-                assertEquals(caCert, parsedCaCert);
-                long daysBetween = getDurationInDays(caCert);
-                assertEquals(days, (int) daysBetween);
-            }
-
-            if (keepCaKey) {
-                assertTrue(Files.exists(zipRoot.resolve("ca").resolve("ca.key")));
-                // check the CA key
-                if (keyPassword != null) {
-                    try (Reader reader = Files.newBufferedReader(zipRoot.resolve("ca").resolve("ca.key"))) {
-                        PEMParser pemParser = new PEMParser(reader);
-                        Object parsed = pemParser.readObject();
-                        assertThat(parsed, instanceOf(PEMEncryptedKeyPair.class));
-                        // Verify we are using AES encryption
-                        final PEMDecryptorProvider pemDecryptorProvider = Mockito.mock(PEMDecryptorProvider.class);
-                        try {
-                            ((PEMEncryptedKeyPair) parsed).decryptKeyPair(pemDecryptorProvider);
-                        } catch (Exception e) {
-                            // Catch error thrown by the empty mock, we are only interested in the argument passed in
-                        }
-                        finally {
-                            Mockito.verify(pemDecryptorProvider).get("AES-128-CBC");
-                        }
-                        char[] zeroChars = new char[caInfo.password.length];
-                        Arrays.fill(zeroChars, (char) 0);
-                        assertArrayEquals(zeroChars, caInfo.password);
-                    }
-                }
-
-                PrivateKey privateKey = PemUtils.readPrivateKey(zipRoot.resolve("ca").resolve("ca.key"),
-                    () -> keyPassword != null ? keyPassword.toCharArray() : null);
-                assertEquals(caInfo.certAndKey.key, privateKey);
-            }
-        } else {
-            assertFalse(Files.exists(zipRoot.resolve("ca")));
-        }
+        assertFalse(Files.exists(zipRoot.resolve("ca")));
 
 
         for (CertificateInformation certInfo : certInfos) {
         for (CertificateInformation certInfo : certInfos) {
             String filename = certInfo.name.filename;
             String filename = certInfo.name.filename;
@@ -413,6 +365,22 @@ public class CertificateToolTests extends ESTestCase {
         }
         }
     }
     }
 
 
+    @SuppressWarnings("unchecked")
+    public void testErrorMessageOnInvalidKeepCaOption() {
+        final CertificateTool certificateTool = new CertificateTool();
+        final OptionSet optionSet = mock(OptionSet.class);
+        when(optionSet.valuesOf(any(OptionSpec.class))).thenAnswer(invocation -> {
+            if (invocation.getArguments()[0] instanceof NonOptionArgumentSpec) {
+                return List.of("cert", "--keep-ca-key");
+            } else {
+                return List.of();
+            }
+        });
+        final UserException e = expectThrows(UserException.class,
+            () -> certificateTool.execute(new MockTerminal(), optionSet));
+        assertThat(e.getMessage(), containsString("Generating certificates without providing a CA is no longer supported"));
+    }
+
     public void testGetCAInfo() throws Exception {
     public void testGetCAInfo() throws Exception {
         Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build());
         Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build());
         Path testNodeCertPath = getDataPath("/org/elasticsearch/xpack/security/cli/testnode.crt");
         Path testNodeCertPath = getDataPath("/org/elasticsearch/xpack/security/cli/testnode.crt");
@@ -470,17 +438,9 @@ public class CertificateToolTests extends ESTestCase {
             }
             }
         }
         }
 
 
-        options = command.getParser().parse(Strings.toStringArray(args));
-        caInfo = command.getCAInfo(terminal, options, env);
-        caCK = caInfo.certAndKey;
-        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));
+        final OptionSet options2 = command.getParser().parse(Strings.toStringArray(args));
+        final UserException e = expectThrows(UserException.class, () -> command.getCAInfo(terminal, options2, env));
+        assertThat(e.getMessage(), containsString("Must specify either --ca or --ca-cert/--ca-key or --self-signed"));
 
 
         // test self-signed
         // test self-signed
         args = CollectionUtils.arrayAsArrayList("-self-signed");
         args = CollectionUtils.arrayAsArrayList("-self-signed");
@@ -551,7 +511,7 @@ public class CertificateToolTests extends ESTestCase {
      * A multi-stage test that:
      * A multi-stage test that:
      * - Create a new CA
      * - Create a new CA
      * - Uses that CA to create 2 node certificates
      * - Uses that CA to create 2 node certificates
-     * - Creates a 3rd node certificate using an auto-generated CA or a self-signed cert
+     * - Creates a 3rd node certificate as a self-signed cert
      * - Checks that the first 2 node certificates trust one another
      * - Checks that the first 2 node certificates trust one another
      * - Checks that the 3rd node certificate is _not_ trusted
      * - 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
      * - Checks that all 3 certificates have the right values based on the command line options provided during generation
@@ -567,14 +527,12 @@ public class CertificateToolTests extends ESTestCase {
         final Path node2File = tempDir.resolve("node2.p12").toAbsolutePath();
         final Path node2File = tempDir.resolve("node2.p12").toAbsolutePath();
         final Path node3File = tempDir.resolve("node3.p12").toAbsolutePath();
         final Path node3File = tempDir.resolve("node3.p12").toAbsolutePath();
 
 
-        final int caKeySize = randomIntBetween(4, 8) * 512;
         final int node1KeySize = randomIntBetween(2, 6) * 512;
         final int node1KeySize = randomIntBetween(2, 6) * 512;
         final int node2KeySize = randomIntBetween(2, 6) * 512;
         final int node2KeySize = randomIntBetween(2, 6) * 512;
         final int node3KeySize = randomIntBetween(1, 4) * 512;
         final int node3KeySize = randomIntBetween(1, 4) * 512;
 
 
         final int days = randomIntBetween(7, 1500);
         final int days = randomIntBetween(7, 1500);
 
 
-        final String caPassword = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node1Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node1Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node2Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node2Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node3Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
         final String node3Password = randomFrom("", randomAlphaOfLengthBetween(4, 16));
@@ -583,23 +541,7 @@ public class CertificateToolTests extends ESTestCase {
         final String node2Ip = "200.182." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
         final String node2Ip = "200.182." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
         final String node3Ip = "200.183." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
         final String node3Ip = "200.183." + randomIntBetween(1, 250) + "." + randomIntBetween(1, 250);
 
 
-        final CertificateAuthorityCommand caCommand = new CertificateAuthorityCommand() {
-            @Override
-            Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) throws IOException {
-                // Needed to work within the security manager
-                return caFile;
-            }
-        };
-        final OptionSet caOptions = caCommand.getParser().parse(
-                "-ca-dn", "CN=My ElasticSearch Cluster",
-                "-pass", caPassword,
-                "-out", caFile.toString(),
-                "-keysize", String.valueOf(caKeySize),
-                "-days", String.valueOf(days)
-        );
-        caCommand.execute(terminal, caOptions, env);
-
-        assertThat(caFile, pathExists());
+        final String caPassword = generateCA(caFile, terminal, env);
 
 
         final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1File);
         final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1File);
         final OptionSet gen1Options = gen1Command.getParser().parse(
         final OptionSet gen1Options = gen1Command.getParser().parse(
@@ -640,13 +582,7 @@ public class CertificateToolTests extends ESTestCase {
             "-dns", "node03.cluster2.es.internal.corp.net",
             "-dns", "node03.cluster2.es.internal.corp.net",
             "-ip", node3Ip
             "-ip", node3Ip
         );
         );
-        final boolean selfSigned = randomBoolean();
-        if (selfSigned) {
-            gen3Args.add("-self-signed");
-        } else {
-            gen3Args.add("-ca-dn");
-            gen3Args.add("CN=My ElasticSearch Cluster 2");
-        }
+        gen3Args.add("-self-signed");
         final GenerateCertificateCommand gen3Command = new PathAwareGenerateCertificateCommand(null, node3File);
         final GenerateCertificateCommand gen3Command = new PathAwareGenerateCertificateCommand(null, node3File);
         final OptionSet gen3Options = gen3Command.getParser().parse(
         final OptionSet gen3Options = gen3Command.getParser().parse(
                 Strings.toStringArray(gen3Args));
                 Strings.toStringArray(gen3Args));
@@ -688,14 +624,8 @@ public class CertificateToolTests extends ESTestCase {
         assertThat(getKeySize(node3Key), equalTo(node3KeySize));
         assertThat(getKeySize(node3Key), equalTo(node3KeySize));
         final Certificate[] certificateChain = node3KeyStore.getCertificateChain(CertificateTool.DEFAULT_CERT_NAME);
         final Certificate[] certificateChain = node3KeyStore.getCertificateChain(CertificateTool.DEFAULT_CERT_NAME);
         final X509Certificate node3x509Certificate = (X509Certificate) certificateChain[0];
         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());
-        }
+        assertEquals(1, certificateChain.length);
+        assertEquals(node3x509Certificate.getSubjectX500Principal(), node3x509Certificate.getIssuerX500Principal());
     }
     }
 
 
 
 
@@ -711,18 +641,21 @@ public class CertificateToolTests extends ESTestCase {
         final MockTerminal terminal = new MockTerminal();
         final MockTerminal terminal = new MockTerminal();
         Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", tempDir).build());
         Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", tempDir).build());
 
 
-        final Path pkcs12Zip = tempDir.resolve("p12.zip");
+        final Path caFile = tempDir.resolve("ca.p12");
+        final String caPassword = generateCA(caFile, terminal, env);
+
+        final Path node1Pkcs12 = tempDir.resolve("node1.p12");
         final Path pemZip = tempDir.resolve("pem.zip");
         final Path pemZip = tempDir.resolve("pem.zip");
 
 
         final int keySize = randomIntBetween(4, 8) * 512;
         final int keySize = randomIntBetween(4, 8) * 512;
         final int days = randomIntBetween(500, 1500);
         final int days = randomIntBetween(500, 1500);
 
 
-        final String caPassword = randomAlphaOfLengthBetween(4, 16);
         final String node1Password = randomAlphaOfLengthBetween(4, 16);
         final String node1Password = randomAlphaOfLengthBetween(4, 16);
 
 
-        final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(null, pkcs12Zip);
+        final GenerateCertificateCommand gen1Command = new PathAwareGenerateCertificateCommand(caFile, node1Pkcs12);
         final OptionSet gen1Options = gen1Command.getParser().parse(
         final OptionSet gen1Options = gen1Command.getParser().parse(
-                "-keep-ca-key",
+                "-ca", "<ca>",
+                "-ca-pass", caPassword,
                 "-out", "<zip>",
                 "-out", "<zip>",
                 "-keysize", String.valueOf(keySize),
                 "-keysize", String.valueOf(keySize),
                 "-days", String.valueOf(days),
                 "-days", String.valueOf(days),
@@ -730,22 +663,12 @@ public class CertificateToolTests extends ESTestCase {
                 "-name", "node01"
                 "-name", "node01"
         );
         );
 
 
-        terminal.addSecretInput(caPassword);
         terminal.addSecretInput(node1Password);
         terminal.addSecretInput(node1Password);
         gen1Command.execute(terminal, gen1Options, env);
         gen1Command.execute(terminal, gen1Options, env);
 
 
-        assertThat(pkcs12Zip, pathExists());
+        assertThat(node1Pkcs12, pathExists());
 
 
-        FileSystem zip1FS = FileSystems.newFileSystem(new URI("jar:" + pkcs12Zip.toUri()), Collections.emptyMap());
-        Path zip1Root = zip1FS.getPath("/");
-
-        final Path caP12 = zip1Root.resolve("ca/ca.p12");
-        assertThat(caP12, pathExists());
-
-        final Path node1P12 = zip1Root.resolve("node01/node01.p12");
-        assertThat(node1P12, pathExists());
-
-        final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caP12, pemZip);
+        final GenerateCertificateCommand gen2Command = new PathAwareGenerateCertificateCommand(caFile, pemZip);
         final OptionSet gen2Options = gen2Command.getParser().parse(
         final OptionSet gen2Options = gen2Command.getParser().parse(
                 "-ca", "<ca>",
                 "-ca", "<ca>",
                 "-out", "<zip>",
                 "-out", "<zip>",
@@ -772,11 +695,11 @@ public class CertificateToolTests extends ESTestCase {
         final Path node2Key = zip2Root.resolve("node02/node02.key");
         final Path node2Key = zip2Root.resolve("node02/node02.key");
         assertThat(node2Key, pathExists());
         assertThat(node2Key, pathExists());
 
 
-        final KeyStore node1KeyStore = CertParsingUtils.readKeyStore(node1P12, "PKCS12", node1Password.toCharArray());
+        final KeyStore node1KeyStore = CertParsingUtils.readKeyStore(node1Pkcs12, "PKCS12", node1Password.toCharArray());
         final KeyStore node1TrustStore = node1KeyStore;
         final KeyStore node1TrustStore = node1KeyStore;
 
 
         final KeyStore node2KeyStore = CertParsingUtils.getKeyStoreFromPEM(node2Cert, node2Key, new char[0]);
         final KeyStore node2KeyStore = CertParsingUtils.getKeyStoreFromPEM(node2Cert, node2Key, new char[0]);
-        final KeyStore node2TrustStore = CertParsingUtils.readKeyStore(caP12, "PKCS12", caPassword.toCharArray());
+        final KeyStore node2TrustStore = CertParsingUtils.readKeyStore(caFile, "PKCS12", caPassword.toCharArray());
 
 
         checkTrust(node1KeyStore, node1Password.toCharArray(), node2TrustStore, true);
         checkTrust(node1KeyStore, node1Password.toCharArray(), node2TrustStore, true);
         checkTrust(node2KeyStore, new char[0], node1TrustStore, true);
         checkTrust(node2KeyStore, new char[0], node1TrustStore, true);
@@ -808,8 +731,9 @@ public class CertificateToolTests extends ESTestCase {
             }
             }
         };
         };
 
 
-        final String optionThatTriggersZip = randomFrom("-pem", "-keep-ca-key", "-multiple", "-in=input.yml");
+        final String optionThatTriggersZip = randomFrom("-pem", "-multiple", "-in=input.yml");
         final OptionSet genOptions = genCommand.getParser().parse(
         final OptionSet genOptions = genCommand.getParser().parse(
+                "--self-signed",
                 "-out", "<zip>",
                 "-out", "<zip>",
                 optionThatTriggersZip
                 optionThatTriggersZip
         );
         );
@@ -982,4 +906,30 @@ public class CertificateToolTests extends ESTestCase {
             return outFile;
             return outFile;
         }
         }
     }
     }
+
+    private String generateCA(Path caFile, Terminal terminal, Environment env) throws Exception {
+        final int caKeySize = randomIntBetween(4, 8) * 512;
+        final int days = randomIntBetween(7, 1500);
+        final String caPassword = randomFrom("", randomAlphaOfLengthBetween(4, 16));
+
+        final CertificateAuthorityCommand caCommand = new CertificateAuthorityCommand() {
+            @Override
+            Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) {
+                // Needed to work within the security manager
+                return caFile;
+            }
+        };
+        final OptionSet caOptions = caCommand.getParser().parse(
+            "-ca-dn", "CN=My ElasticSearch Cluster",
+            "-pass", caPassword,
+            "-out", caFile.toString(),
+            "-keysize", String.valueOf(caKeySize),
+            "-days", String.valueOf(days)
+        );
+        caCommand.execute(terminal, caOptions, env);
+
+        assertThat(caFile, pathExists());
+
+        return caPassword;
+    }
 }
 }