Skip to content

Commit d1cb96a

Browse files
authored
Handle pwd protected keystores in all CLI tools (#45289)
This change ensures that `elasticsearch-setup-passwords` and `elasticsearch-saml-metadata` can handle a password protected elasticsearch.keystore. For setup passwords the user would be prompted to add the elasticsearch keystore password upon running the tool. There is no option to pass the password as a parameter as we assume the user is present in order to enter the desired passwords for the built-in users. For saml-metadata, we prompt for the keystore password at all times even though we'd only need to read something from the keystore when there is a signing or encryption configuration.
1 parent 4780880 commit d1cb96a

File tree

8 files changed

+276
-124
lines changed

8 files changed

+276
-124
lines changed

distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/BaseKeyStoreCommand.java

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@
2121

2222
import joptsimple.OptionSet;
2323
import joptsimple.OptionSpec;
24-
import org.elasticsearch.cli.EnvironmentAwareCommand;
2524
import org.elasticsearch.cli.ExitCodes;
25+
import org.elasticsearch.cli.KeyStoreAwareCommand;
2626
import org.elasticsearch.cli.Terminal;
2727
import org.elasticsearch.cli.UserException;
2828
import org.elasticsearch.env.Environment;
2929

3030
import java.nio.file.Path;
31-
import java.util.Arrays;
3231

33-
public abstract class BaseKeyStoreCommand extends EnvironmentAwareCommand {
32+
public abstract class BaseKeyStoreCommand extends KeyStoreAwareCommand {
3433

3534
private KeyStoreWrapper keyStore;
3635
private SecureString keyStorePassword;
@@ -82,30 +81,6 @@ protected SecureString getKeyStorePassword() {
8281
return keyStorePassword;
8382
}
8483

85-
/**
86-
* Reads the keystore password from the {@link Terminal}, prompting for verification where applicable and returns it as a
87-
* {@link SecureString}.
88-
*
89-
* @param terminal the terminal to use for user inputs
90-
* @param withVerification whether the user should be prompted for password verification
91-
* @return a SecureString with the password the user entered
92-
* @throws UserException If the user is prompted for verification and enters a different password
93-
*/
94-
static SecureString readPassword(Terminal terminal, boolean withVerification) throws UserException {
95-
final char[] passwordArray;
96-
if (withVerification) {
97-
passwordArray = terminal.readSecret("Enter new password for the elasticsearch keystore (empty for no password): ");
98-
char[] passwordVerification = terminal.readSecret("Enter same password again: ");
99-
if (Arrays.equals(passwordArray, passwordVerification) == false) {
100-
throw new UserException(ExitCodes.DATA_ERROR, "Passwords are not equal, exiting.");
101-
}
102-
Arrays.fill(passwordVerification, '\u0000');
103-
} else {
104-
passwordArray = terminal.readSecret("Enter password for the elasticsearch keystore : ");
105-
}
106-
return new SecureString(passwordArray);
107-
}
108-
10984
/**
11085
* This is called after the keystore password has been read from the stdin and the keystore is decrypted and
11186
* loaded. The keystore and keystore passwords are available to classes extending {@link BaseKeyStoreCommand}

distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/CreateKeyStoreCommand.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@
2525

2626
import joptsimple.OptionSet;
2727
import joptsimple.OptionSpec;
28-
import org.elasticsearch.cli.EnvironmentAwareCommand;
2928
import org.elasticsearch.cli.ExitCodes;
29+
import org.elasticsearch.cli.KeyStoreAwareCommand;
3030
import org.elasticsearch.cli.Terminal;
3131
import org.elasticsearch.cli.UserException;
3232
import org.elasticsearch.env.Environment;
3333

3434
/**
3535
* A sub-command for the keystore cli to create a new keystore.
3636
*/
37-
class CreateKeyStoreCommand extends EnvironmentAwareCommand {
37+
class CreateKeyStoreCommand extends KeyStoreAwareCommand {
3838

3939
private final OptionSpec<Void> passwordOption;
4040

@@ -46,7 +46,7 @@ class CreateKeyStoreCommand extends EnvironmentAwareCommand {
4646
@Override
4747
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
4848
try (SecureString password = options.has(passwordOption) ?
49-
BaseKeyStoreCommand.readPassword(terminal, true) : new SecureString(new char[0])) {
49+
readPassword(terminal, true) : new SecureString(new char[0])) {
5050
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
5151
if (Files.exists(keystoreFile)) {
5252
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.cli;
21+
22+
import joptsimple.OptionSet;
23+
import org.elasticsearch.common.settings.KeyStoreWrapper;
24+
import org.elasticsearch.common.settings.SecureString;
25+
import org.elasticsearch.env.Environment;
26+
27+
import javax.crypto.AEADBadTagException;
28+
import java.io.IOException;
29+
import java.security.GeneralSecurityException;
30+
import java.util.Arrays;
31+
32+
/**
33+
* An {@link org.elasticsearch.cli.EnvironmentAwareCommand} that needs to access the elasticsearch keystore, possibly
34+
* decrypting it if it is password protected.
35+
*/
36+
public abstract class KeyStoreAwareCommand extends EnvironmentAwareCommand {
37+
public KeyStoreAwareCommand(String description) {
38+
super(description);
39+
}
40+
41+
/**
42+
* Reads the keystore password from the {@link Terminal}, prompting for verification where applicable and returns it as a
43+
* {@link SecureString}.
44+
*
45+
* @param terminal the terminal to use for user inputs
46+
* @param withVerification whether the user should be prompted for password verification
47+
* @return a SecureString with the password the user entered
48+
* @throws UserException If the user is prompted for verification and enters a different password
49+
*/
50+
protected static SecureString readPassword(Terminal terminal, boolean withVerification) throws UserException {
51+
final char[] passwordArray;
52+
if (withVerification) {
53+
passwordArray = terminal.readSecret("Enter new password for the elasticsearch keystore (empty for no password): ");
54+
char[] passwordVerification = terminal.readSecret("Enter same password again: ");
55+
if (Arrays.equals(passwordArray, passwordVerification) == false) {
56+
throw new UserException(ExitCodes.DATA_ERROR, "Passwords are not equal, exiting.");
57+
}
58+
Arrays.fill(passwordVerification, '\u0000');
59+
} else {
60+
passwordArray = terminal.readSecret("Enter password for the elasticsearch keystore : ");
61+
}
62+
return new SecureString(passwordArray);
63+
}
64+
65+
/**
66+
* Decrypt the {@code keyStore}, prompting the user to enter the password in the {@link Terminal} if it is password protected
67+
*/
68+
protected static void decryptKeyStore(KeyStoreWrapper keyStore, Terminal terminal)
69+
throws UserException, GeneralSecurityException, IOException {
70+
try (SecureString keystorePassword = keyStore.hasPassword() ?
71+
readPassword(terminal, false) : new SecureString(new char[0])) {
72+
keyStore.decrypt(keystorePassword.getChars());
73+
} catch (SecurityException e) {
74+
if (e.getCause() instanceof AEADBadTagException) {
75+
throw new UserException(ExitCodes.DATA_ERROR, "Wrong password for elasticsearch.keystore");
76+
}
77+
}
78+
}
79+
80+
protected abstract void execute(Terminal terminal, OptionSet options, Environment env) throws Exception;
81+
}

test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ public abstract class CommandTestCase extends ESTestCase {
3030
/** The terminal that execute uses. */
3131
protected final MockTerminal terminal = new MockTerminal();
3232

33-
/** The last command that was executed. */
34-
protected Command command;
35-
3633
@Before
3734
public void resetTerminal() {
3835
terminal.reset();
@@ -43,13 +40,20 @@ public void resetTerminal() {
4340
protected abstract Command newCommand();
4441

4542
/**
46-
* Runs the command with the given args.
43+
* Runs a command with the given args.
4744
*
4845
* Output can be found in {@link #terminal}.
49-
* The command created can be found in {@link #command}.
5046
*/
5147
public String execute(String... args) throws Exception {
52-
command = newCommand();
48+
return execute(newCommand(), args);
49+
}
50+
51+
/**
52+
* Runs the specified command with the given args.
53+
* <p>
54+
* Output can be found in {@link #terminal}.
55+
*/
56+
public String execute(Command command, String... args) throws Exception {
5357
command.mainWithoutErrorHandling(args, terminal);
5458
return terminal.getOutput();
5559
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import joptsimple.OptionSet;
1010
import joptsimple.OptionSpec;
1111
import org.elasticsearch.ExceptionsHelper;
12-
import org.elasticsearch.cli.EnvironmentAwareCommand;
1312
import org.elasticsearch.cli.ExitCodes;
13+
import org.elasticsearch.cli.KeyStoreAwareCommand;
1414
import org.elasticsearch.cli.LoggingAwareMultiCommand;
1515
import org.elasticsearch.cli.Terminal;
1616
import org.elasticsearch.cli.Terminal.Verbosity;
@@ -125,7 +125,7 @@ class AutoSetup extends SetupCommand {
125125
@Override
126126
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
127127
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
128-
setupOptions(options, env);
128+
setupOptions(terminal, options, env);
129129
checkElasticKeystorePasswordValid(terminal, env);
130130
checkClusterHealth(terminal);
131131

@@ -171,7 +171,7 @@ class InteractiveSetup extends SetupCommand {
171171
@Override
172172
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
173173
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
174-
setupOptions(options, env);
174+
setupOptions(terminal, options, env);
175175
checkElasticKeystorePasswordValid(terminal, env);
176176
checkClusterHealth(terminal);
177177

@@ -221,7 +221,7 @@ private void changedPasswordCallback(Terminal terminal, String user, SecureStrin
221221
* An abstract class that provides functionality common to both the auto and
222222
* interactive setup modes.
223223
*/
224-
private abstract class SetupCommand extends EnvironmentAwareCommand {
224+
private abstract class SetupCommand extends KeyStoreAwareCommand {
225225

226226
boolean shouldPrompt;
227227

@@ -248,10 +248,9 @@ public void close() {
248248
}
249249
}
250250

251-
void setupOptions(OptionSet options, Environment env) throws Exception {
251+
void setupOptions(Terminal terminal, OptionSet options, Environment env) throws Exception {
252252
keyStoreWrapper = keyStoreFunction.apply(env);
253-
// TODO: We currently do not support keystore passwords
254-
keyStoreWrapper.decrypt(new char[0]);
253+
decryptKeyStore(keyStoreWrapper, terminal);
255254

256255
Settings.Builder settingsBuilder = Settings.builder();
257256
settingsBuilder.put(env.settings(), true);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
import org.apache.logging.log4j.Level;
3333
import org.apache.logging.log4j.LogManager;
3434
import org.apache.logging.log4j.Logger;
35-
import org.elasticsearch.cli.EnvironmentAwareCommand;
3635
import org.elasticsearch.cli.ExitCodes;
36+
import org.elasticsearch.cli.KeyStoreAwareCommand;
3737
import org.elasticsearch.cli.SuppressForbidden;
3838
import org.elasticsearch.cli.Terminal;
3939
import org.elasticsearch.cli.UserException;
@@ -68,7 +68,7 @@
6868
/**
6969
* CLI tool to generate SAML Metadata for a Service Provider (realm)
7070
*/
71-
public class SamlMetadataCommand extends EnvironmentAwareCommand {
71+
public class SamlMetadataCommand extends KeyStoreAwareCommand {
7272

7373
static final String METADATA_SCHEMA = "saml-schema-metadata-2.0.xsd";
7474

@@ -414,13 +414,12 @@ private SortedSet<String> sorted(Set<String> strings) {
414414
/**
415415
* @TODO REALM-SETTINGS[TIM] This can be redone a lot now the realm settings are keyed by type
416416
*/
417-
private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment env) throws UserException, IOException, Exception {
417+
private RealmConfig findRealm(Terminal terminal, OptionSet options, Environment env) throws Exception {
418418

419419
keyStoreWrapper = keyStoreFunction.apply(env);
420420
final Settings settings;
421421
if (keyStoreWrapper != null) {
422-
// TODO: We currently do not support keystore passwords
423-
keyStoreWrapper.decrypt(new char[0]);
422+
decryptKeyStore(keyStoreWrapper, terminal);
424423

425424
final Settings.Builder settingsBuilder = Settings.builder();
426425
settingsBuilder.put(env.settings(), true);

0 commit comments

Comments
 (0)