Skip to content

Commit 0a0db4b

Browse files
Elasticsearch keystore passphrase for startup scripts (#44775)
This commit allows a user to provide a keystore password on Elasticsearch startup, but only prompts when the keystore exists and is encrypted. The entrypoint in Java code is standard input. When the Bootstrap class is checking for secure keystore settings, it checks whether or not the keystore is encrypted. If so, we read one line from standard input and use this as the password. For simplicity's sake, we allow a maximum passphrase length of 128 characters. (This is an arbitrary limit and could be increased or eliminated. It is also enforced in the keystore tools, so that a user can't create a password that's too long to enter at startup.) In order to provide a password on standard input, we have to account for four different ways of starting Elasticsearch: the bash startup script, the Windows batch startup script, systemd startup, and docker startup. We use wrapper scripts to reduce systemd and docker to the bash case: in both cases, a wrapper script can read a passphrase from the filesystem and pass it to the bash script. In order to simplify testing the need for a passphrase, I have added a has-passwd command to the keystore tool. This command can run silently, and exit with status 0 when the keystore has a password. It exits with status 1 if the keystore doesn't exist or exists and is unencrypted. A good deal of the code-change in this commit has to do with refactoring packaging tests to cleanly use the same tests for both the "archive" and the "package" cases. This required not only moving tests around, but also adding some convenience methods for an abstraction layer over distribution-specific commands. I will write some user-facing documentation for these changes in a follow-up commit.
1 parent 0944b31 commit 0a0db4b

File tree

32 files changed

+1126
-179
lines changed

32 files changed

+1126
-179
lines changed

Vagrantfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ JAVA
471471
ensure curl
472472
ensure unzip
473473
ensure rsync
474+
ensure expect
474475
475476
installed bats || {
476477
# Bats lives in a git repository....

distribution/docker/src/docker/bin/docker-entrypoint.sh

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,18 @@ if [[ -f bin/elasticsearch-users ]]; then
8484
# honor the variable if it's present.
8585
if [[ -n "$ELASTIC_PASSWORD" ]]; then
8686
[[ -f /usr/share/elasticsearch/config/elasticsearch.keystore ]] || (run_as_other_user_if_needed elasticsearch-keystore create)
87-
if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then
88-
(run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password')
87+
if ! (run_as_other_user_if_needed elasticsearch-keystore has-passwd --silent) ; then
88+
# keystore is unencrypted
89+
if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then
90+
(run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password')
91+
fi
92+
else
93+
# keystore requires password
94+
if ! (run_as_other_user_if_needed echo "$KEYSTORE_PASSWORD" \
95+
| elasticsearch-keystore list | grep -q '^bootstrap.password$') ; then
96+
COMMANDS="$(printf "%s\n%s" "$KEYSTORE_PASSWORD" "$ELASTIC_PASSWORD")"
97+
(run_as_other_user_if_needed echo "$COMMANDS" | elasticsearch-keystore add -x 'bootstrap.password')
98+
fi
8999
fi
90100
fi
91101
fi
@@ -97,4 +107,4 @@ if [[ "$(id -u)" == "0" ]]; then
97107
fi
98108
fi
99109

100-
run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch "${es_opts[@]}"
110+
run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch "${es_opts[@]}" <<<"$KEYSTORE_PASSWORD"

distribution/packages/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ Closure commonPackageConfig(String type, boolean oss, boolean jdk) {
234234
from "${packagingFiles}/systemd/sysctl/elasticsearch.conf"
235235
fileMode 0644
236236
}
237+
into('/usr/share/elasticsearch/bin') {
238+
from "${packagingFiles}/systemd/systemd-entrypoint"
239+
fileMode 0755
240+
}
237241

238242
// ========= sysV init =========
239243
configurationFile '/etc/init.d/elasticsearch'

distribution/packages/src/common/scripts/posttrans

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ if [ ! -f /etc/elasticsearch/elasticsearch.keystore ]; then
44
chmod 660 /etc/elasticsearch/elasticsearch.keystore
55
md5sum /etc/elasticsearch/elasticsearch.keystore > /etc/elasticsearch/.elasticsearch.keystore.initial_md5sum
66
else
7-
/usr/share/elasticsearch/bin/elasticsearch-keystore upgrade
7+
if /usr/share/elasticsearch/bin/elasticsearch-keystore has-passwd --silent ; then
8+
echo "### Warning: unable to upgrade encrypted keystore" 1>&2
9+
echo " Please run elasticsearch-keystore upgrade and enter password" 1>&2
10+
else
11+
/usr/share/elasticsearch/bin/elasticsearch-keystore upgrade
12+
fi
813
fi
914

1015
${scripts.footer}

distribution/packages/src/common/systemd/elasticsearch.service

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ WorkingDirectory=/usr/share/elasticsearch
1919
User=elasticsearch
2020
Group=elasticsearch
2121

22-
ExecStart=/usr/share/elasticsearch/bin/elasticsearch -p ${PID_DIR}/elasticsearch.pid --quiet
22+
ExecStart=/usr/share/elasticsearch/bin/systemd-entrypoint -p ${PID_DIR}/elasticsearch.pid --quiet
2323

2424
# StandardOutput is configured to redirect to journalctl since
2525
# some error messages may be logged in standard output before
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
3+
# This wrapper script allows SystemD to feed a file containing a passphrase into
4+
# the main Elasticsearch startup script
5+
6+
if [ -n "$ES_KEYSTORE_PASSPHRASE_FILE" ] ; then
7+
exec /usr/share/elasticsearch/bin/elasticsearch "$@" < "$ES_KEYSTORE_PASSPHRASE_FILE"
8+
else
9+
exec /usr/share/elasticsearch/bin/elasticsearch "$@"
10+
fi

distribution/src/bin/elasticsearch

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ if [ -z "$ES_TMPDIR" ]; then
2020
ES_TMPDIR=`"$JAVA" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory`
2121
fi
2222

23+
# get keystore password before setting java options to avoid
24+
# conflicting GC configurations for the keystore tools
25+
unset KEYSTORE_PASSWORD
26+
KEYSTORE_PASSWORD=
27+
if ! echo $* | grep -E -q '(^-h |-h$| -h |--help$|--help |^-V |-V$| -V |--version$|--version )' \
28+
&& "`dirname "$0"`"/elasticsearch-keystore has-passwd --silent
29+
then
30+
if ! read -s -r -p "Elasticsearch keystore password: " KEYSTORE_PASSWORD ; then
31+
echo "Failed to read keystore password on console" 1>&2
32+
exit 1
33+
fi
34+
fi
35+
2336
ES_JVM_OPTIONS="$ES_PATH_CONF"/jvm.options
2437
ES_JAVA_OPTS=`export ES_TMPDIR; "$JAVA" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.JvmOptionsParser "$ES_JVM_OPTIONS"`
2538

@@ -35,7 +48,7 @@ if ! echo $* | grep -E '(^-d |-d$| -d |--daemonize$|--daemonize )' > /dev/null;
3548
-Des.bundled_jdk="$ES_BUNDLED_JDK" \
3649
-cp "$ES_CLASSPATH" \
3750
org.elasticsearch.bootstrap.Elasticsearch \
38-
"$@"
51+
"$@" <<<"$KEYSTORE_PASSWORD"
3952
else
4053
exec \
4154
"$JAVA" \
@@ -48,7 +61,7 @@ else
4861
-cp "$ES_CLASSPATH" \
4962
org.elasticsearch.bootstrap.Elasticsearch \
5063
"$@" \
51-
<&- &
64+
<<<"$KEYSTORE_PASSWORD" &
5265
retval=$?
5366
pid=$!
5467
[ $retval -eq 0 ] || exit $retval

distribution/src/bin/elasticsearch-cli.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ set ES_JAVA_OPTS=-Xms4m -Xmx64m -XX:+UseSerialGC %ES_JAVA_OPTS%
2525
-cp "%ES_CLASSPATH%" ^
2626
"%ES_MAIN_CLASS%" ^
2727
%*
28-
28+
2929
exit /b %ERRORLEVEL%

distribution/src/bin/elasticsearch.bat

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ setlocal enabledelayedexpansion
44
setlocal enableextensions
55

66
SET params='%*'
7+
SET checkpassword=Y
78

89
:loop
910
FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO (
@@ -18,6 +19,20 @@ FOR /F "usebackq tokens=1* delims= " %%A IN (!params!) DO (
1819
SET silent=Y
1920
)
2021

22+
IF "!current!" == "-h" (
23+
SET checkpassword=N
24+
)
25+
IF "!current!" == "--help" (
26+
SET checkpassword=N
27+
)
28+
29+
IF "!current!" == "-V" (
30+
SET checkpassword=N
31+
)
32+
IF "!current!" == "--version" (
33+
SET checkpassword=N
34+
)
35+
2136
IF "!silent!" == "Y" (
2237
SET nopauseonerror=Y
2338
) ELSE (
@@ -41,6 +56,18 @@ IF ERRORLEVEL 1 (
4156
EXIT /B %ERRORLEVEL%
4257
)
4358

59+
SET KEYSTORE_PASSWORD=
60+
IF "%checkpassword%"=="Y" (
61+
CALL "%~dp0elasticsearch-keystore.bat" has-passwd --silent
62+
IF !ERRORLEVEL! EQU 0 (
63+
SET /P KEYSTORE_PASSWORD=Elasticsearch keystore password:
64+
IF !ERRORLEVEL! NEQ 0 (
65+
ECHO Failed to read keystore password on standard input
66+
EXIT /B !ERRORLEVEL!
67+
)
68+
)
69+
)
70+
4471
if not defined ES_TMPDIR (
4572
for /f "tokens=* usebackq" %%a in (`CALL %JAVA% -cp "!ES_CLASSPATH!" "org.elasticsearch.tools.launchers.TempDirectory"`) do set ES_TMPDIR=%%a
4673
)
@@ -54,7 +81,20 @@ if "%MAYBE_JVM_OPTIONS_PARSER_FAILED%" == "jvm_options_parser_failed" (
5481
exit /b 1
5582
)
5683

57-
%JAVA% %ES_JAVA_OPTS% -Delasticsearch -Des.path.home="%ES_HOME%" -Des.path.conf="%ES_PATH_CONF%" -Des.distribution.flavor="%ES_DISTRIBUTION_FLAVOR%" -Des.distribution.type="%ES_DISTRIBUTION_TYPE%" -Des.bundled_jdk="%ES_BUNDLED_JDK%" -cp "%ES_CLASSPATH%" "org.elasticsearch.bootstrap.Elasticsearch" !newparams!
84+
rem windows batch pipe will choke on special characters in strings
85+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^^=^^^^!
86+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^&=^^^&!
87+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^|=^^^|!
88+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^<=^^^<!
89+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^>=^^^>!
90+
SET KEYSTORE_PASSWORD=!KEYSTORE_PASSWORD:^\=^^^\!
91+
92+
ECHO.!KEYSTORE_PASSWORD!| %JAVA% %ES_JAVA_OPTS% -Delasticsearch ^
93+
-Des.path.home="%ES_HOME%" -Des.path.conf="%ES_PATH_CONF%" ^
94+
-Des.distribution.flavor="%ES_DISTRIBUTION_FLAVOR%" ^
95+
-Des.distribution.type="%ES_DISTRIBUTION_TYPE%" ^
96+
-Des.bundled_jdk="%ES_BUNDLED_JDK%" ^
97+
-cp "%ES_CLASSPATH%" "org.elasticsearch.bootstrap.Elasticsearch" !newparams!
5898

5999
endlocal
60100
endlocal
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.KeyStoreAwareCommand;
24+
import org.elasticsearch.cli.Terminal;
25+
import org.elasticsearch.cli.UserException;
26+
import org.elasticsearch.env.Environment;
27+
28+
import java.nio.file.Path;
29+
30+
public class HasPasswordKeyStoreCommand extends KeyStoreAwareCommand {
31+
32+
static final int NO_PASSWORD_EXIT_CODE = 1;
33+
34+
HasPasswordKeyStoreCommand() {
35+
super("Succeeds if the keystore exists and is password-protected, " +
36+
"fails with exit code " + NO_PASSWORD_EXIT_CODE + " otherwise.");
37+
}
38+
39+
@Override
40+
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
41+
final Path configFile = env.configFile();
42+
final KeyStoreWrapper keyStore = KeyStoreWrapper.load(configFile);
43+
44+
// We handle error printing here so we can respect the "--silent" flag
45+
// We have to throw an exception to get a nonzero exit code
46+
if (keyStore == null) {
47+
terminal.errorPrintln(Terminal.Verbosity.NORMAL, "ERROR: Elasticsearch keystore not found");
48+
throw new UserException(NO_PASSWORD_EXIT_CODE, null);
49+
}
50+
if (keyStore.hasPassword() == false) {
51+
terminal.errorPrintln(Terminal.Verbosity.NORMAL, "ERROR: Keystore is not password-protected");
52+
throw new UserException(NO_PASSWORD_EXIT_CODE, null);
53+
}
54+
55+
terminal.println(Terminal.Verbosity.NORMAL, "Keystore is password-protected");
56+
}
57+
}

0 commit comments

Comments
 (0)