Skip to content
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

WIP: Elasticsearch keystore passphrase for startup scripts #1

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();
}
}