Skip to content
8 changes: 8 additions & 0 deletions distribution/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> result = [:]
expansions = expansions.each { key, value ->
Expand Down
18 changes: 15 additions & 3 deletions distribution/docker/src/docker/bin/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
30 changes: 28 additions & 2 deletions distribution/src/bin/elasticsearch
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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" \
Expand All @@ -49,7 +75,7 @@ else
-cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch \
"$@" \
<&- &
<<<"$KEYSTORE_PASSWORD" &
retval=$?
pid=$!
[ $retval -eq 0 ] || exit $retval
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ": ");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
27 changes: 25 additions & 2 deletions libs/cli/src/main/java/org/elasticsearch/cli/Terminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -149,6 +155,8 @@ private static class SystemTerminal extends Terminal {

private static final PrintWriter WRITER = newWriter();

private BufferedReader reader;

SystemTerminal() {
super(System.lineSeparator());
}
Expand All @@ -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;
Expand All @@ -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?");
}
Expand Down
16 changes: 14 additions & 2 deletions server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions server/src/test/java/org/elasticsearch/cli/TerminalTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}