Skip to content

Add passphrase support to elasticsearch-keystore #37472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ class ClusterFormationTasks {
* getting the short name requiring the path to already exist.
*/
final Object esKeystoreUtil = "${-> node.binPath().resolve('elasticsearch-keystore').toString()}"
return configureExecTask(name, project, setup, node, esKeystoreUtil, 'create')
return configureExecTask(name, project, setup, node, esKeystoreUtil, 'create', '--nopass')
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,45 +53,40 @@ class AddFileKeyStoreCommand extends EnvironmentAwareCommand {

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
if (keystore == null) {
if (options.has(forceOption) == false &&
terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
terminal.println("Exiting without creating keystore.");
try (KeystoreAndPassphrase keyAndPass = KeyStoreWrapper.readOrCreate(terminal, env.configFile(), options.has(forceOption))) {
if (null == keyAndPass) {
return;
}
keystore = KeyStoreWrapper.create();
keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
terminal.println("Created elasticsearch keystore in " + env.configFile());
} else {
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
}
KeyStoreWrapper keystore = keyAndPass.getKeystore();

List<String> argumentValues = arguments.values(options);
if (argumentValues.size() == 0) {
throw new UserException(ExitCodes.USAGE, "Missing setting name");
}
String setting = argumentValues.get(0);
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
List<String> argumentValues = arguments.values(options);
if (argumentValues.size() == 0) {
throw new UserException(ExitCodes.USAGE, "Missing setting name");
}
String setting = argumentValues.get(0);
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
}
}
}

if (argumentValues.size() == 1) {
throw new UserException(ExitCodes.USAGE, "Missing file name");
}
Path file = getPath(argumentValues.get(1));
if (Files.exists(file) == false) {
throw new UserException(ExitCodes.IO_ERROR, "File [" + file.toString() + "] does not exist");
}
if (argumentValues.size() > 2) {
throw new UserException(ExitCodes.USAGE, "Unrecognized extra arguments [" +
String.join(", ", argumentValues.subList(2, argumentValues.size())) + "] after filepath");
if (argumentValues.size() == 1) {
throw new UserException(ExitCodes.USAGE, "Missing file name");
}
Path file = getPath(argumentValues.get(1));
if (Files.exists(file) == false) {
throw new UserException(ExitCodes.IO_ERROR, "File [" + file.toString() + "] does not exist");
}
if (argumentValues.size() > 2) {
throw new UserException(ExitCodes.USAGE, "Unrecognized extra arguments [" +
String.join(", ", argumentValues.subList(2, argumentValues.size())) + "] after filepath");
}
keystore.setFile(setting, Files.readAllBytes(file));
keystore.save(env.configFile(), keyAndPass.getPassphrase());
} catch (SecurityException e) {
throw new UserException(ExitCodes.DATA_ERROR, "Failed to access the keystore. Please make sure the passphrase was correct.");
}
keystore.setFile(setting, Files.readAllBytes(file));
keystore.save(env.configFile(), new char[0]);
}

@SuppressForbidden(reason="file arg for cli")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,44 +56,39 @@ InputStream getStdin() {

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
if (keystore == null) {
if (options.has(forceOption) == false &&
terminal.promptYesNo("The elasticsearch keystore does not exist. Do you want to create it?", false) == false) {
terminal.println("Exiting without creating keystore.");
try (KeystoreAndPassphrase keyAndPass = KeyStoreWrapper.readOrCreate(terminal, env.configFile(), options.has(forceOption))) {
if (null == keyAndPass) {
return;
}
keystore = KeyStoreWrapper.create();
keystore.save(env.configFile(), new char[0] /* always use empty passphrase for auto created keystore */);
terminal.println("Created elasticsearch keystore in " + env.configFile());
} else {
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
}
KeyStoreWrapper keystore = keyAndPass.getKeystore();

String setting = arguments.value(options);
if (setting == null) {
throw new UserException(ExitCodes.USAGE, "The setting name can not be null");
}
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
String setting = arguments.value(options);
if (setting == null) {
throw new UserException(ExitCodes.USAGE, "The setting name can not be null");
}
if (keystore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
}
}
}

final char[] value;
if (options.has(stdinOption)) {
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
value = stdinReader.readLine().toCharArray();
} else {
value = terminal.readSecret("Enter value for " + setting + ": ");
}
final char[] value;
if (options.has(stdinOption)) {
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
value = stdinReader.readLine().toCharArray();
} else {
value = terminal.readSecret("Enter value for " + setting + ": ");
}

try {
keystore.setString(setting, value);
} catch (IllegalArgumentException e) {
throw new UserException(ExitCodes.DATA_ERROR, "String value must contain only ASCII");
try {
keystore.setString(setting, value);
} catch (IllegalArgumentException e) {
throw new UserException(ExitCodes.DATA_ERROR, "String value must contain only ASCII");
}
keystore.save(env.configFile(), keyAndPass.getPassphrase());
} catch (SecurityException e) {
throw new UserException(ExitCodes.DATA_ERROR, "Failed to access the keystore. Please make sure the passphrase was correct.");
}
keystore.save(env.configFile(), new char[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.common.settings;

import joptsimple.OptionSet;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;

import java.util.Arrays;

public class ChangeKeyStorePassphraseCommand extends EnvironmentAwareCommand {


ChangeKeyStorePassphraseCommand() {
super("Changes the passphrase of a keystore");
}

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
char[] newPassphrase = null;
try (KeystoreAndPassphrase keyAndPass = KeyStoreWrapper.readOrCreate(terminal, env.configFile(), false)) {
if (null == keyAndPass) {
return;
}
KeyStoreWrapper keystore = keyAndPass.getKeystore();
newPassphrase = KeyStoreWrapper.readPassphrase(terminal, true);
keystore.save(env.configFile(), newPassphrase);
terminal.println("Elasticsearch keystore passphrase changed successfully." + env.configFile());
} catch (SecurityException e) {
throw new UserException(ExitCodes.DATA_ERROR, "Failed to access the keystore. Please make sure the passphrase was correct.");
} finally {
if (null != newPassphrase) {
Arrays.fill(newPassphrase, '\u0000');
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,49 @@

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;

import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;

/**
* A subcommand for the keystore cli to create a new keystore.
*/
class CreateKeyStoreCommand extends EnvironmentAwareCommand {

private final OptionSpec<Void> noPassOption;

CreateKeyStoreCommand() {
super("Creates a new elasticsearch keystore");
this.noPassOption = parser.acceptsAll(Arrays.asList("n", "nopass"), "Creates an obfuscated (not passphrase protected) keystore");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not mimic tools like ssh-keygen which uses entering empty passphrase twice to mean no passphrase? I think this would be less confusing? If we go with an explicit command, then we should prohibit an empty passphrase to distinguish the two?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main driver for an option is that it is much easier to script if needed, rather then having to handle empty input twice. I also dislike hitting enter twice for no reason in existing tools (openssl, keytool, ssh-keygen) but the fact that all of them use this pattern probably adds to your argument.

I don't think that adding a parameter means that we should prohibit emtpy passphrases on prompt. The explicit parameter has the same effect (empty passphrase) as entering an empty passphrase twice so maybe it'd be worth renaming nopass to emptypass for it to be less confusing?

That means we do get to keep the default behavior that people are used to, but if someone needs to willingly set an empty password they can - we could throw a warning in the terminal in this case too, to make it crystal clear.

}

@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
if (Files.exists(keystoreFile)) {
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
terminal.println("Exiting without creating keystore.");
return;
char[] passphrase = null;
try {
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
if (Files.exists(keystoreFile)) {
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
terminal.println("Exiting without creating keystore.");
return;
}
}
KeyStoreWrapper keystore = KeyStoreWrapper.create();
passphrase = options.has(noPassOption) ? new char[0] : KeyStoreWrapper.readPassphrase(terminal, true);
keystore.save(env.configFile(), passphrase);
terminal.println("Created elasticsearch keystore in " + env.configFile());
} catch (SecurityException e) {
throw new UserException(ExitCodes.IO_ERROR, "Error creating the elasticsearch keystore.", e);
} finally {
if (null != passphrase) {
Arrays.fill(passphrase, '\u0000');
}
}


char[] password = new char[0];// terminal.readSecret("Enter passphrase (empty for no passphrase): ");
/* TODO: uncomment when entering passwords on startup is supported
char[] passwordRepeat = terminal.readSecret("Enter same passphrase again: ");
if (Arrays.equals(password, passwordRepeat) == false) {
throw new UserException(ExitCodes.DATA_ERROR, "Passphrases are not equal, exiting.");
}*/

KeyStoreWrapper keystore = KeyStoreWrapper.create();
keystore.save(env.configFile(), password);
terminal.println("Created elasticsearch keystore in " + env.configFile());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ private KeyStoreCli() {
subcommands.put("add", new AddStringKeyStoreCommand());
subcommands.put("add-file", new AddFileKeyStoreCommand());
subcommands.put("remove", new RemoveSettingKeyStoreCommand());
subcommands.put("passwd", new ChangeKeyStorePassphraseCommand());
}

public static void main(String[] args) throws Exception {
Expand Down
Loading