diff --git a/distribution/build.gradle b/distribution/build.gradle index 9606604036101..41b634301c0db 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -549,6 +549,14 @@ subprojects { 'license.text': [ 'deb': licenseText, ], + + 'keystore.passwd.check': [ + /* Avoid unfortunate 5-second delay for elasticsearch-cli + scripts on certain OSX/JVM combos by reading the + "encrypted" flag byte in the keystore directly */ + 'tar': '[ "$(echo -n "\000")" = "$(dd if="$ES_PATH_CONF"/elasticsearch.keystore ibs=1 skip=31 count=1 2>/dev/null)" ]', + 'def': '"`dirname "$0"`"/elasticsearch-keystore list <&-' + ], ] Map result = [:] expansions = expansions.each { key, value -> diff --git a/distribution/docker/src/docker/bin/docker-entrypoint.sh b/distribution/docker/src/docker/bin/docker-entrypoint.sh index b9aab95bc2e96..f24b69263990f 100644 --- a/distribution/docker/src/docker/bin/docker-entrypoint.sh +++ b/distribution/docker/src/docker/bin/docker-entrypoint.sh @@ -84,8 +84,20 @@ if [[ -f bin/elasticsearch-users ]]; then # honor the variable if it's present. if [[ -n "$ELASTIC_PASSWORD" ]]; then [[ -f /usr/share/elasticsearch/config/elasticsearch.keystore ]] || (run_as_other_user_if_needed elasticsearch-keystore create) - if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then - (run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password') + if (run_as_other_user_if_needed elasticsearch-keystore list >/dev/null 2>&1) ; then + # keystore is unencrypted + if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then + (run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password') + fi + else + # keystore requires password + # ugh - when keystore is password protected, prompt gets mixed up in list output + if ! (run_as_other_user_if_needed echo "$KEYSTORE_PASSWORD" \ + | elasticsearch-keystore list \ + | grep -q '^\(.*: \)\?bootstrap.password$') ; then + COMMANDS="$(printf "%s\n%s" "$KEYSTORE_PASSWORD" "$ELASTIC_PASSWORD")" + (run_as_other_user_if_needed echo "$COMMANDS" | elasticsearch-keystore add -x 'bootstrap.password') + fi fi fi fi @@ -97,4 +109,4 @@ if [[ "$(id -u)" == "0" ]]; then fi fi -run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch "${es_opts[@]}" +run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch "${es_opts[@]}" <<<"$KEYSTORE_PASSWORD" diff --git a/distribution/src/bin/elasticsearch b/distribution/src/bin/elasticsearch index b7ed2b648b76f..f6534c9aa3f59 100755 --- a/distribution/src/bin/elasticsearch +++ b/distribution/src/bin/elasticsearch @@ -24,6 +24,32 @@ ES_JVM_OPTIONS="$ES_PATH_CONF"/jvm.options JVM_OPTIONS=`"$JAVA" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.JvmOptionsParser "$ES_JVM_OPTIONS"` ES_JAVA_OPTS="${JVM_OPTIONS//\$\{ES_TMPDIR\}/$ES_TMPDIR}" +unset KEYSTORE_PASSWORD +KEYSTORE_PASSWORD= +# If keystore exists and is encrypted, prompt for a password +if [ -f "$ES_PATH_CONF"/elasticsearch.keystore ] \ + && ! echo $* | grep -E '(^-h |-h$| -h |--help$|--help )' >/dev/null 2>&1 \ + && ! echo $* | grep -E '(^-V |-V$| -V |--version$|--version )' >/dev/null 2>&1 \ + && ! ${keystore.passwd.check} >/dev/null 2>&1 +then + if [ -e "$ES_KEYSTORE_PASSPHRASE_FILE" ] ; then + # read from a file or a FIFO + if [ -p "$ES_KEYSTORE_PASSPHRASE_FILE" ] ; then + echo "Reading keystore passphrase from FIFO $ES_KEYSTORE_PASSPHRASE_FILE (blocking operation)" 1>&2 + else + echo "Reading keystore passphrase from file $ES_KEYSTORE_PASSPHRASE_FILE" 1>&2 + fi + read -s KEYSTORE_PASSWORD <"$ES_KEYSTORE_PASSPHRASE_FILE" + else + # read from standard input + if ! read -s -p "Elasticsearch keystore password: " KEYSTORE_PASSWORD ; then + echo "Failed to read keystore password on standard input" 1>&2 + exit 1 + fi + echo + fi +fi + # manual parsing to find out, if process should be detached if ! echo $* | grep -E '(^-d |-d$| -d |--daemonize$|--daemonize )' > /dev/null; then exec \ @@ -36,7 +62,7 @@ if ! echo $* | grep -E '(^-d |-d$| -d |--daemonize$|--daemonize )' > /dev/null; -Des.bundled_jdk="$ES_BUNDLED_JDK" \ -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ - "$@" + "$@" <<<"$KEYSTORE_PASSWORD" else exec \ "$JAVA" \ @@ -49,7 +75,7 @@ else -cp "$ES_CLASSPATH" \ org.elasticsearch.bootstrap.Elasticsearch \ "$@" \ - <&- & + <<<"$KEYSTORE_PASSWORD" & retval=$? pid=$! [ $retval -eq 0 ] || exit $retval diff --git a/distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java b/distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java index 45284fd8ee2c2..63e62be78c1ad 100644 --- a/distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java +++ b/distribution/tools/keystore-cli/src/main/java/org/elasticsearch/common/settings/AddStringKeyStoreCommand.java @@ -69,8 +69,7 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment final char[] value; if (options.has(stdinOption)) { - BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8)); - value = stdinReader.readLine().toCharArray(); + value = terminal.readSecret(""); } else { value = terminal.readSecret("Enter value for " + setting + ": "); } diff --git a/distribution/tools/keystore-cli/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java b/distribution/tools/keystore-cli/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java index 7978940412d10..2ebb7cbde689a 100644 --- a/distribution/tools/keystore-cli/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java +++ b/distribution/tools/keystore-cli/src/test/java/org/elasticsearch/common/settings/AddStringKeyStoreCommandTests.java @@ -21,11 +21,14 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Map; import org.elasticsearch.cli.Command; import org.elasticsearch.cli.ExitCodes; +import org.elasticsearch.cli.Terminal; +import org.elasticsearch.cli.TestSystemTerminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.env.Environment; @@ -140,7 +143,7 @@ public void testStdinShort() throws Exception { String password = "keystorepassword"; KeyStoreWrapper.create().save(env.configFile(), password.toCharArray()); terminal.addSecretInput(password); - setInput("secret value 1"); + terminal.addSecretInput("secret value 1"); execute("-x", "foo"); assertSecureString("foo", "secret value 1", password); } @@ -149,11 +152,20 @@ public void testStdinLong() throws Exception { String password = "keystorepassword"; KeyStoreWrapper.create().save(env.configFile(), password.toCharArray()); terminal.addSecretInput(password); - setInput("secret value 2"); + terminal.addSecretInput("secret value 2"); execute("--stdin", "foo"); assertSecureString("foo", "secret value 2", password); } + public void testStdinSystemTerminal() throws Exception { + String password = "keystorepassword"; + KeyStoreWrapper.create().save(env.configFile(), password.toCharArray()); + String input = password + "\nbar\n"; + InputStream in = new ByteArrayInputStream(input.getBytes(Charset.defaultCharset())); + Terminal term = new TestSystemTerminal(in); + execute(term, "--stdin", "foo"); + } + public void testMissingSettingName() throws Exception { String password = "keystorepassword"; createKeystore(password); diff --git a/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java b/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java index a0ebff5d67041..6259d1459a07e 100644 --- a/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java +++ b/libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java @@ -22,6 +22,7 @@ import java.io.BufferedReader; import java.io.Console; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.nio.charset.Charset; @@ -59,6 +60,11 @@ protected Terminal(String lineSeparator) { this.lineSeparator = lineSeparator; } + /** Visible for testing in subclasses: set an input other than standard input */ + protected void setInput(InputStream inputStream) throws IOException { + throw new AssertionError("unsupported operation"); + } + /** Sets the verbosity of the terminal. */ public void setVerbosity(Verbosity verbosity) { this.verbosity = verbosity; @@ -149,6 +155,8 @@ private static class SystemTerminal extends Terminal { private static final PrintWriter WRITER = newWriter(); + private BufferedReader reader; + SystemTerminal() { super(System.lineSeparator()); } @@ -158,6 +166,22 @@ private static PrintWriter newWriter() { return new PrintWriter(System.out); } + private BufferedReader getReader() { + if (reader == null) { + reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())); + } + return reader; + } + + /** Visible for testing the system terminal with custom input */ + @Override + protected void setInput(InputStream inputStream) throws IOException { + if (reader != null) { + reader.close(); + } + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); + } + @Override public PrintWriter getWriter() { return WRITER; @@ -166,9 +190,8 @@ public PrintWriter getWriter() { @Override public String readText(String text) { getWriter().print(text); - BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())); try { - final String line = reader.readLine(); + final String line = getReader().readLine(); if (line == null) { throw new IllegalStateException("unable to read from standard input; is standard input open and a tty attached?"); } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index a4a2696fdad73..a7a20629a2741 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -26,6 +26,7 @@ import org.apache.logging.log4j.core.appender.ConsoleAppender; import org.apache.logging.log4j.core.config.Configurator; import org.apache.lucene.util.Constants; +import org.elasticsearch.cli.Terminal; import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.StringHelper; import org.elasticsearch.ElasticsearchException; @@ -234,16 +235,27 @@ static SecureSettings loadSecureSettings(Environment initialEnv) throws Bootstra throw new BootstrapException(e); } + char[] password; + if (keystore != null && keystore.hasPassword()) { + password = Terminal.DEFAULT.readSecret("Elasticsearch password? "); + } else { + // TODO[wrb]: is there a case w/o stdin? + password = new char[0]; + } + try { if (keystore == null) { final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); keyStoreWrapper.save(initialEnv.configFile(), new char[0]); return keyStoreWrapper; } else { - keystore.decrypt(new char[0] /* TODO: read password from stdin */); - KeyStoreWrapper.upgrade(keystore, initialEnv.configFile(), new char[0]); + keystore.decrypt(password); + KeyStoreWrapper.upgrade(keystore, initialEnv.configFile(), password); } } catch (Exception e) { + if (password.length != 0) { + throw new BootstrapException(new SecurityException("Incorrect keystore password")); + } throw new BootstrapException(e); } return keystore; diff --git a/server/src/test/java/org/elasticsearch/cli/TerminalTests.java b/server/src/test/java/org/elasticsearch/cli/TerminalTests.java index 3b409c2add636..486c2c550fd64 100644 --- a/server/src/test/java/org/elasticsearch/cli/TerminalTests.java +++ b/server/src/test/java/org/elasticsearch/cli/TerminalTests.java @@ -21,6 +21,10 @@ import org.elasticsearch.test.ESTestCase; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.Charset; + public class TerminalTests extends ESTestCase { public void testVerbosity() throws Exception { MockTerminal terminal = new MockTerminal(); @@ -75,6 +79,13 @@ public void testPromptYesNoCase() throws Exception { assertFalse(terminal.promptYesNo("Answer?", true)); } + public void testReadTwiceFromSystemTerminal() throws Exception { + InputStream in = new ByteArrayInputStream("foo\nbar\n".getBytes(Charset.defaultCharset())); + Terminal terminal = new TestSystemTerminal(in); + assertEquals(terminal.readText("say foo"), "foo"); + assertEquals(terminal.readText("say bar"), "bar"); + } + private void assertPrinted(MockTerminal logTerminal, Terminal.Verbosity verbosity, String text) throws Exception { logTerminal.println(verbosity, text); String output = logTerminal.getOutput(); diff --git a/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java b/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java index e9c6a2eec9c31..741dd737a5e40 100644 --- a/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cli/CommandTestCase.java @@ -49,8 +49,12 @@ public void resetTerminal() { * The command created can be found in {@link #command}. */ public String execute(String... args) throws Exception { + execute(this.terminal, args); + return terminal.getOutput(); + } + + public void execute(Terminal terminal, String... args) throws Exception { command = newCommand(); command.mainWithoutErrorHandling(args, terminal); - return terminal.getOutput(); } } diff --git a/test/framework/src/main/java/org/elasticsearch/cli/TestSystemTerminal.java b/test/framework/src/main/java/org/elasticsearch/cli/TestSystemTerminal.java new file mode 100644 index 0000000000000..1b2142571dab0 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/cli/TestSystemTerminal.java @@ -0,0 +1,51 @@ +/* + * 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.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +public class TestSystemTerminal extends Terminal { + Terminal delegate = Terminal.DEFAULT; + + public TestSystemTerminal(InputStream inputStream) throws IOException { + super(System.lineSeparator()); + this.delegate.setInput(inputStream); + if (!"SystemTerminal".equals(this.delegate.getClass().getSimpleName())) { + throw new AssertionError("delegate must be a SystemTerminal"); + } + } + + @Override + public String readText(String prompt) { + return this.delegate.readText(prompt); + } + + @Override + public char[] readSecret(String prompt) { + return this.delegate.readSecret(prompt); + } + + @Override + public PrintWriter getWriter() { + return this.delegate.getWriter(); + } +}