Skip to content

Commit d235e48

Browse files
authored
Merge pull request #22335 from rjernst/keystore
Settings: Add infrastructure for elasticsearch keystore
2 parents b9c2c2f + cd6e3f4 commit d235e48

28 files changed

+1538
-38
lines changed

core/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ dependencies {
9494
exclude group: 'org.elasticsearch', module: 'elasticsearch'
9595
}
9696
}
97+
testCompile 'com.google.jimfs:jimfs:1.1'
98+
testCompile 'com.google.guava:guava:18.0'
9799
}
98100

99101
if (isEclipse) {

core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,16 @@
3030
import org.apache.lucene.util.StringHelper;
3131
import org.elasticsearch.ElasticsearchException;
3232
import org.elasticsearch.Version;
33-
import org.elasticsearch.cli.ExitCodes;
3433
import org.elasticsearch.cli.Terminal;
3534
import org.elasticsearch.cli.UserException;
3635
import org.elasticsearch.common.PidFile;
3736
import org.elasticsearch.common.SuppressForbidden;
3837
import org.elasticsearch.common.inject.CreationException;
39-
import org.elasticsearch.common.logging.DeprecationLogger;
4038
import org.elasticsearch.common.logging.ESLoggerFactory;
4139
import org.elasticsearch.common.logging.LogConfigurator;
4240
import org.elasticsearch.common.logging.Loggers;
4341
import org.elasticsearch.common.network.IfConfig;
42+
import org.elasticsearch.common.settings.KeyStoreWrapper;
4443
import org.elasticsearch.common.settings.Settings;
4544
import org.elasticsearch.common.transport.BoundTransportAddress;
4645
import org.elasticsearch.env.Environment;
@@ -228,13 +227,36 @@ protected void validateNodeBeforeAcceptingRequests(
228227
};
229228
}
230229

231-
private static Environment initialEnvironment(boolean foreground, Path pidFile, Settings initialSettings) {
230+
private static KeyStoreWrapper loadKeyStore(Environment initialEnv) throws BootstrapException {
231+
final KeyStoreWrapper keystore;
232+
try {
233+
keystore = KeyStoreWrapper.load(initialEnv.configFile());
234+
} catch (IOException e) {
235+
throw new BootstrapException(e);
236+
}
237+
if (keystore == null) {
238+
return null; // no keystore
239+
}
240+
241+
try {
242+
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
243+
} catch (Exception e) {
244+
throw new BootstrapException(e);
245+
}
246+
return keystore;
247+
}
248+
249+
private static Environment createEnvironment(boolean foreground, Path pidFile,
250+
KeyStoreWrapper keystore, Settings initialSettings) {
232251
Terminal terminal = foreground ? Terminal.DEFAULT : null;
233252
Settings.Builder builder = Settings.builder();
234253
if (pidFile != null) {
235254
builder.put(Environment.PIDFILE_SETTING.getKey(), pidFile);
236255
}
237256
builder.put(initialSettings);
257+
if (keystore != null) {
258+
builder.setKeyStore(keystore);
259+
}
238260
return InternalSettingsPreparer.prepareEnvironment(builder.build(), terminal, Collections.emptyMap());
239261
}
240262

@@ -265,7 +287,7 @@ static void init(
265287
final boolean foreground,
266288
final Path pidFile,
267289
final boolean quiet,
268-
final Settings initialSettings) throws BootstrapException, NodeValidationException, UserException {
290+
final Environment initialEnv) throws BootstrapException, NodeValidationException, UserException {
269291
// Set the system property before anything has a chance to trigger its use
270292
initLoggerPrefix();
271293

@@ -275,7 +297,8 @@ static void init(
275297

276298
INSTANCE = new Bootstrap();
277299

278-
Environment environment = initialEnvironment(foreground, pidFile, initialSettings);
300+
final KeyStoreWrapper keystore = loadKeyStore(initialEnv);
301+
Environment environment = createEnvironment(foreground, pidFile, keystore, initialEnv.settings());
279302
try {
280303
LogConfigurator.configure(environment);
281304
} catch (IOException e) {
@@ -313,6 +336,13 @@ static void init(
313336

314337
INSTANCE.setup(true, environment);
315338

339+
try {
340+
// any secure settings must be read during node construction
341+
IOUtils.close(keystore);
342+
} catch (IOException e) {
343+
throw new BootstrapException(e);
344+
}
345+
316346
INSTANCE.start();
317347

318348
if (closeStandardStreams) {

core/src/main/java/org/elasticsearch/bootstrap/BootstrapException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* during bootstrap should explicitly declare the checked exceptions that they can throw, rather
2727
* than declaring the top-level checked exception {@link Exception}. This exception exists to wrap
2828
* these checked exceptions so that
29-
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.common.settings.Settings)}
29+
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.env.Environment)}
3030
* does not have to declare all of these checked exceptions.
3131
*/
3232
class BootstrapException extends Exception {

core/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,16 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
111111
final boolean quiet = options.has(quietOption);
112112

113113
try {
114-
init(daemonize, pidFile, quiet, env.settings());
114+
init(daemonize, pidFile, quiet, env);
115115
} catch (NodeValidationException e) {
116116
throw new UserException(ExitCodes.CONFIG, e.getMessage());
117117
}
118118
}
119119

120-
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings)
120+
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment initialEnv)
121121
throws NodeValidationException, UserException {
122122
try {
123-
Bootstrap.init(!daemonize, pidFile, quiet, initialSettings);
123+
Bootstrap.init(!daemonize, pidFile, quiet, initialEnv);
124124
} catch (BootstrapException | RuntimeException e) {
125125
// format exceptions to the console in a special way
126126
// to avoid 2MB stacktraces from guice, etc.

core/src/main/java/org/elasticsearch/cli/Terminal.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.InputStreamReader;
2828
import java.io.PrintWriter;
2929
import java.nio.charset.Charset;
30+
import java.util.Locale;
3031

3132
/**
3233
* A Terminal wraps access to reading input and writing output for a cli.
@@ -92,6 +93,26 @@ public final void print(Verbosity verbosity, String msg) {
9293
}
9394
}
9495

96+
/**
97+
* Prompt for a yes or no answer from the user. This method will loop until 'y' or 'n'
98+
* (or the default empty value) is entered.
99+
*/
100+
public final boolean promptYesNo(String prompt, boolean defaultYes) {
101+
String answerPrompt = defaultYes ? " [Y/n]" : " [y/N]";
102+
while (true) {
103+
String answer = readText(prompt + answerPrompt).toLowerCase(Locale.ROOT);
104+
if (answer.isEmpty()) {
105+
return defaultYes;
106+
}
107+
boolean answerYes = answer.equals("y");
108+
if (answerYes == false && answer.equals("n") == false) {
109+
println("Did not understand answer '" + answer + "'");
110+
continue;
111+
}
112+
return answerYes;
113+
}
114+
}
115+
95116
private static class ConsoleTerminal extends Terminal {
96117

97118
private static final Console CONSOLE = System.console();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 java.io.BufferedReader;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.Arrays;
27+
28+
import joptsimple.OptionSet;
29+
import joptsimple.OptionSpec;
30+
import org.elasticsearch.cli.EnvironmentAwareCommand;
31+
import org.elasticsearch.cli.ExitCodes;
32+
import org.elasticsearch.cli.Terminal;
33+
import org.elasticsearch.cli.UserException;
34+
import org.elasticsearch.env.Environment;
35+
36+
/**
37+
* A subcommand for the keystore cli which adds a string setting.
38+
*/
39+
class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
40+
41+
private final OptionSpec<Void> stdinOption;
42+
private final OptionSpec<Void> forceOption;
43+
private final OptionSpec<String> arguments;
44+
45+
AddStringKeyStoreCommand() {
46+
super("Add a string setting to the keystore");
47+
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin");
48+
this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
49+
this.arguments = parser.nonOptions("setting name");
50+
}
51+
52+
// pkg private so tests can manipulate
53+
InputStream getStdin() {
54+
return System.in;
55+
}
56+
57+
@Override
58+
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
59+
KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile());
60+
if (keystore == null) {
61+
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
62+
}
63+
64+
keystore.decrypt(new char[0] /* TODO: prompt for password when they are supported */);
65+
66+
String setting = arguments.value(options);
67+
if (keystore.getSettings().contains(setting) && options.has(forceOption) == false) {
68+
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
69+
terminal.println("Exiting without modifying keystore.");
70+
return;
71+
}
72+
}
73+
74+
final char[] value;
75+
if (options.has(stdinOption)) {
76+
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
77+
value = stdinReader.readLine().toCharArray();
78+
} else {
79+
value = terminal.readSecret("Enter value for " + setting + ": ");
80+
}
81+
82+
try {
83+
keystore.setStringSetting(setting, value);
84+
} catch (IllegalArgumentException e) {
85+
throw new UserException(ExitCodes.DATA_ERROR, "String value must contain only ASCII");
86+
}
87+
keystore.save(env.configFile());
88+
}
89+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 java.nio.file.Files;
23+
import java.nio.file.Path;
24+
25+
import joptsimple.OptionSet;
26+
import org.elasticsearch.cli.EnvironmentAwareCommand;
27+
import org.elasticsearch.cli.Terminal;
28+
import org.elasticsearch.env.Environment;
29+
30+
/**
31+
* A subcommand for the keystore cli to create a new keystore.
32+
*/
33+
class CreateKeyStoreCommand extends EnvironmentAwareCommand {
34+
35+
CreateKeyStoreCommand() {
36+
super("Creates a new elasticsearch keystore");
37+
}
38+
39+
@Override
40+
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
41+
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
42+
if (Files.exists(keystoreFile)) {
43+
if (terminal.promptYesNo("An elasticsearch keystore already exists. Overwrite?", false) == false) {
44+
terminal.println("Exiting without creating keystore.");
45+
return;
46+
}
47+
}
48+
49+
50+
char[] password = new char[0];// terminal.readSecret("Enter passphrase (empty for no passphrase): ");
51+
/* TODO: uncomment when entering passwords on startup is supported
52+
char[] passwordRepeat = terminal.readSecret("Enter same passphrase again: ");
53+
if (Arrays.equals(password, passwordRepeat) == false) {
54+
throw new UserException(ExitCodes.DATA_ERROR, "Passphrases are not equal, exiting.");
55+
}*/
56+
57+
KeyStoreWrapper keystore = KeyStoreWrapper.create(password);
58+
keystore.save(env.configFile());
59+
terminal.println("Created elasticsearch keystore in " + env.configFile());
60+
}
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 org.elasticsearch.cli.MultiCommand;
23+
import org.elasticsearch.cli.Terminal;
24+
25+
/**
26+
* A cli tool for managing secrets in the elasticsearch keystore.
27+
*/
28+
public class KeyStoreCli extends MultiCommand {
29+
30+
private KeyStoreCli() {
31+
super("A tool for managing settings stored in the elasticsearch keystore");
32+
subcommands.put("create", new CreateKeyStoreCommand());
33+
subcommands.put("list", new ListKeyStoreCommand());
34+
subcommands.put("add", new AddStringKeyStoreCommand());
35+
subcommands.put("remove", new RemoveSettingKeyStoreCommand());
36+
}
37+
38+
public static void main(String[] args) throws Exception {
39+
exit(new KeyStoreCli().main(args, Terminal.DEFAULT));
40+
}
41+
}

0 commit comments

Comments
 (0)