Skip to content

Commit 9398aac

Browse files
authored
Add passphrase support to elasticsearch-keystore (#38498)
This change adds support for keystore passphrases to all subcommands of the elasticsearch-keystore cli tool and adds a subcommand for changing the passphrase of an existing keystore. The work to read the passphrase in Elasticsearch when loading, which will be addressed in a different PR. Subcommands of elasticsearch-keystore can handle (open and create) passphrase protected keystores When reading a keystore, a user is only prompted for a passphrase only if the keystore is passphrase protected. When creating a keystore, a user is allowed (default behavior) to create one with an empty passphrase Passphrase can be set to be empty when changing/setting it for an existing keystore Relates to: #32691 Supersedes: #37472
1 parent 5b0b591 commit 9398aac

20 files changed

+557
-245
lines changed

distribution/docker/docker-test-entrypoint.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/bin/bash
22
cd /usr/share/elasticsearch/bin/
3-
./elasticsearch-users useradd x_pack_rest_user -p x-pack-test-password -r superuser || true
3+
./elasticsearch-users useradd x_pack_rest_user -p x-pack-test-password -r superuser || true
4+
./elasticsearch-keystore create
45
echo "testnode" > /tmp/password
56
cat /tmp/password | ./elasticsearch-keystore add -x -f -v 'xpack.security.transport.ssl.keystore.secure_password'
67
cat /tmp/password | ./elasticsearch-keystore add -x -f -v 'xpack.security.http.ssl.keystore.secure_password'

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

+8-21
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
import joptsimple.OptionSet;
2828
import joptsimple.OptionSpec;
29-
import org.elasticsearch.cli.EnvironmentAwareCommand;
3029
import org.elasticsearch.cli.ExitCodes;
3130
import org.elasticsearch.cli.Terminal;
3231
import org.elasticsearch.cli.UserException;
@@ -37,13 +36,13 @@
3736
/**
3837
* A subcommand for the keystore cli which adds a file setting.
3938
*/
40-
class AddFileKeyStoreCommand extends EnvironmentAwareCommand {
39+
class AddFileKeyStoreCommand extends BaseKeyStoreCommand {
4140

4241
private final OptionSpec<Void> forceOption;
4342
private final OptionSpec<String> arguments;
4443

4544
AddFileKeyStoreCommand() {
46-
super("Add a file setting to the keystore");
45+
super("Add a file setting to the keystore", false);
4746
this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
4847
// jopt simple has issue with multiple non options, so we just get one set of them here
4948
// and convert to File when necessary
@@ -52,27 +51,15 @@ class AddFileKeyStoreCommand extends EnvironmentAwareCommand {
5251
}
5352

5453
@Override
55-
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
56-
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
57-
if (keystore == null) {
58-
if (options.has(forceOption) == false &&
59-
terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
60-
terminal.println("Exiting without creating keystore.");
61-
return;
62-
}
63-
keystore = KeyStoreWrapper.create();
64-
keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
65-
terminal.println("Created elasticsearch keystore in " + env.configFile());
66-
} else {
67-
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
68-
}
54+
protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
6955

7056
List<String> argumentValues = arguments.values(options);
7157
if (argumentValues.size() == 0) {
7258
throw new UserException(ExitCodes.USAGE, "Missing setting name");
7359
}
7460
String setting = argumentValues.get(0);
75-
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
61+
final KeyStoreWrapper keyStore = getKeyStore();
62+
if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
7663
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
7764
terminal.println("Exiting without modifying keystore.");
7865
return;
@@ -90,11 +77,11 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
9077
throw new UserException(ExitCodes.USAGE, "Unrecognized extra arguments [" +
9178
String.join(", ", argumentValues.subList(2, argumentValues.size())) + "] after filepath");
9279
}
93-
keystore.setFile(setting, Files.readAllBytes(file));
94-
keystore.save(env.configFile(), new char[0]);
80+
keyStore.setFile(setting, Files.readAllBytes(file));
81+
keyStore.save(env.configFile(), getKeyStorePassword().getChars());
9582
}
9683

97-
@SuppressForbidden(reason="file arg for cli")
84+
@SuppressForbidden(reason = "file arg for cli")
9885
private Path getPath(String file) {
9986
return PathUtils.get(file);
10087
}

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

+11-34
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@
2020
package org.elasticsearch.common.settings;
2121

2222
import java.io.BufferedReader;
23-
import java.io.CharArrayWriter;
2423
import java.io.InputStream;
2524
import java.io.InputStreamReader;
2625
import java.nio.charset.StandardCharsets;
2726
import java.util.Arrays;
2827

2928
import joptsimple.OptionSet;
3029
import joptsimple.OptionSpec;
31-
import org.elasticsearch.cli.EnvironmentAwareCommand;
3230
import org.elasticsearch.cli.ExitCodes;
3331
import org.elasticsearch.cli.Terminal;
3432
import org.elasticsearch.cli.UserException;
@@ -37,14 +35,14 @@
3735
/**
3836
* A subcommand for the keystore cli which adds a string setting.
3937
*/
40-
class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
38+
class AddStringKeyStoreCommand extends BaseKeyStoreCommand {
4139

4240
private final OptionSpec<Void> stdinOption;
4341
private final OptionSpec<Void> forceOption;
4442
private final OptionSpec<String> arguments;
4543

4644
AddStringKeyStoreCommand() {
47-
super("Add a string setting to the keystore");
45+
super("Add a string setting to the keystore", false);
4846
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin");
4947
this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
5048
this.arguments = parser.nonOptions("setting name");
@@ -56,26 +54,13 @@ InputStream getStdin() {
5654
}
5755

5856
@Override
59-
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
60-
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
61-
if (keystore == null) {
62-
if (options.has(forceOption) == false &&
63-
terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
64-
terminal.println("Exiting without creating keystore.");
65-
return;
66-
}
67-
keystore = KeyStoreWrapper.create();
68-
keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
69-
terminal.println("Created elasticsearch keystore in " + env.configFile());
70-
} else {
71-
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
72-
}
73-
57+
protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
7458
String setting = arguments.value(options);
7559
if (setting == null) {
7660
throw new UserException(ExitCodes.USAGE, "The setting name can not be null");
7761
}
78-
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
62+
final KeyStoreWrapper keyStore = getKeyStore();
63+
if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
7964
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
8065
terminal.println("Exiting without modifying keystore.");
8166
return;
@@ -84,26 +69,18 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
8469

8570
final char[] value;
8671
if (options.has(stdinOption)) {
87-
try (BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
88-
CharArrayWriter writer = new CharArrayWriter()) {
89-
int charInt;
90-
while ((charInt = stdinReader.read()) != -1) {
91-
if ((char) charInt == '\r' || (char) charInt == '\n') {
92-
break;
93-
}
94-
writer.write((char) charInt);
95-
}
96-
value = writer.toCharArray();
97-
}
72+
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
73+
value = stdinReader.readLine().toCharArray();
9874
} else {
9975
value = terminal.readSecret("Enter value for " + setting + ": ");
10076
}
10177

10278
try {
103-
keystore.setString(setting, value);
104-
} catch (final IllegalArgumentException e) {
79+
keyStore.setString(setting, value);
80+
} catch (IllegalArgumentException e) {
10581
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
10682
}
107-
keystore.save(env.configFile(), new char[0]);
83+
keyStore.save(env.configFile(), getKeyStorePassword().getChars());
84+
10885
}
10986
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.common.settings;
21+
22+
import joptsimple.OptionSet;
23+
import org.elasticsearch.cli.EnvironmentAwareCommand;
24+
import org.elasticsearch.cli.ExitCodes;
25+
import org.elasticsearch.cli.Terminal;
26+
import org.elasticsearch.cli.UserException;
27+
import org.elasticsearch.env.Environment;
28+
29+
import java.nio.file.Path;
30+
import java.util.Arrays;
31+
32+
public abstract class BaseKeyStoreCommand extends EnvironmentAwareCommand {
33+
34+
private KeyStoreWrapper keyStore;
35+
private SecureString keyStorePassword;
36+
private final boolean keyStoreMustExist;
37+
38+
public BaseKeyStoreCommand(String description, boolean keyStoreMustExist) {
39+
super(description);
40+
this.keyStoreMustExist = keyStoreMustExist;
41+
}
42+
43+
@Override
44+
protected final void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
45+
try {
46+
final Path configFile = env.configFile();
47+
keyStore = KeyStoreWrapper.load(configFile);
48+
if (keyStore == null) {
49+
if (keyStoreMustExist) {
50+
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found at [" +
51+
KeyStoreWrapper.keystorePath(env.configFile()) + "]. Use 'create' command to create one.");
52+
} else {
53+
if (terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
54+
terminal.println("Exiting without creating keystore.");
55+
return;
56+
}
57+
}
58+
keyStorePassword = new SecureString(new char[0]);
59+
keyStore = KeyStoreWrapper.create();
60+
keyStore.save(configFile, keyStorePassword.getChars());
61+
} else {
62+
keyStorePassword = keyStore.hasPassword() ? readPassword(terminal, false) : new SecureString(new char[0]);
63+
keyStore.decrypt(keyStorePassword.getChars());
64+
}
65+
executeCommand(terminal, options, env);
66+
} catch (SecurityException e) {
67+
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
68+
} finally {
69+
if (keyStorePassword != null) {
70+
keyStorePassword.close();
71+
}
72+
}
73+
}
74+
75+
protected KeyStoreWrapper getKeyStore() {
76+
return keyStore;
77+
}
78+
79+
protected SecureString getKeyStorePassword() {
80+
return keyStorePassword;
81+
}
82+
83+
/**
84+
* Reads the keystore password from the {@link Terminal}, prompting for verification where applicable and returns it as a
85+
* {@link SecureString}.
86+
*
87+
* @param terminal the terminal to use for user inputs
88+
* @param withVerification whether the user should be prompted for password verification
89+
* @return a SecureString with the password the user entered
90+
* @throws UserException If the user is prompted for verification and enters a different password
91+
*/
92+
static SecureString readPassword(Terminal terminal, boolean withVerification) throws UserException {
93+
final char[] passwordArray;
94+
if (withVerification) {
95+
passwordArray = terminal.readSecret("Enter new password for the elasticsearch keystore (empty for no password): ");
96+
char[] passwordVerification = terminal.readSecret("Enter same password again: ");
97+
if (Arrays.equals(passwordArray, passwordVerification) == false) {
98+
throw new UserException(ExitCodes.DATA_ERROR, "Passwords are not equal, exiting.");
99+
}
100+
Arrays.fill(passwordVerification, '\u0000');
101+
} else {
102+
passwordArray = terminal.readSecret("Enter password for the elasticsearch keystore : ");
103+
}
104+
final SecureString password = new SecureString(passwordArray);
105+
return password;
106+
}
107+
108+
/**
109+
* This is called after the keystore password has been read from the stdin and the keystore is decrypted and
110+
* loaded. The keystore and keystore passwords are available to classes extending {@link BaseKeyStoreCommand}
111+
* using {@link BaseKeyStoreCommand#getKeyStore()} and {@link BaseKeyStoreCommand#getKeyStorePassword()}
112+
* respectively.
113+
*/
114+
protected abstract void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception;
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.common.settings;
21+
22+
import joptsimple.OptionSet;
23+
import org.elasticsearch.cli.ExitCodes;
24+
import org.elasticsearch.cli.Terminal;
25+
import org.elasticsearch.cli.UserException;
26+
import org.elasticsearch.env.Environment;
27+
28+
/**
29+
* A sub-command for the keystore cli which changes the password.
30+
*/
31+
class ChangeKeyStorePasswordCommand extends BaseKeyStoreCommand {
32+
33+
ChangeKeyStorePasswordCommand() {
34+
super("Changes the password of a keystore", true);
35+
}
36+
37+
@Override
38+
protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
39+
try (SecureString newPassword = readPassword(terminal, true)) {
40+
final KeyStoreWrapper keyStore = getKeyStore();
41+
keyStore.save(env.configFile(), newPassword.getChars());
42+
terminal.println("Elasticsearch keystore password changed successfully.");
43+
} catch (SecurityException e) {
44+
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)