Skip to content

Commit f6191d0

Browse files
committed
add ability to prompt for selected settings on startup
Some settings may be considered sensitive, such as passwords, and storing them in the configuration file on disk is not good from a security perspective. This change allows settings to have a special value, `${prompt::text}` or `${prompt::secret}`, to indicate that elasticsearch should prompt the user for the actual value on startup. This only works when started in the foreground. In cases where elasticsearch is started as a service or in the background, an exception will be thrown. Closes #10838
1 parent a4e0ecc commit f6191d0

File tree

18 files changed

+288
-29
lines changed

18 files changed

+288
-29
lines changed

docs/reference/setup/configuration.asciidoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,29 @@ file which will resolve to an environment setting, for example:
264264
}
265265
--------------------------------------------------
266266

267+
Additionally, for settings that you do not wish to store in the configuration
268+
file, you can use the value `${prompt::text}` or `${prompt::secret}` and start
269+
Elasticsearch in the foreground. `${prompt::secret}` has echoing disabled so
270+
that the value entered will not be shown in your terminal; `${prompt::text}`
271+
will allow you to see the value as you type it in. For example:
272+
273+
[source,yaml]
274+
--------------------------------------------------
275+
node:
276+
name: ${prompt::text}
277+
--------------------------------------------------
278+
279+
On execution of the `elasticsearch` command, you will be prompted to enter
280+
the actual value like so:
281+
282+
[source,sh]
283+
--------------------------------------------------
284+
Enter value for [node.name]:
285+
--------------------------------------------------
286+
287+
NOTE: Elasticsearch will not start if `${prompt::text}` or `${prompt::secret}`
288+
is used in the settings and the process is run as a service or in the background.
289+
267290
The location of the configuration file can be set externally using a
268291
system property:
269292

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.Version;
2626
import org.elasticsearch.common.PidFile;
2727
import org.elasticsearch.common.SuppressForbidden;
28+
import org.elasticsearch.common.cli.Terminal;
2829
import org.elasticsearch.common.collect.Tuple;
2930
import org.elasticsearch.common.inject.CreationException;
3031
import org.elasticsearch.common.inject.spi.Message;
@@ -163,8 +164,16 @@ public void run() {
163164

164165
// install SM after natives, shutdown hooks, etc.
165166
setupSecurity(settings, environment);
166-
167-
NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(settings).loadConfigSettings(false);
167+
168+
// We do not need to reload system properties here as we have already applied them in building the settings and
169+
// reloading could cause multiple prompts to the user for values if a system property was specified with a prompt
170+
// placeholder
171+
Settings nodeSettings = Settings.settingsBuilder()
172+
.put(settings)
173+
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true)
174+
.build();
175+
176+
NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(nodeSettings).loadConfigSettings(false);
168177
node = nodeBuilder.build();
169178
}
170179

@@ -195,8 +204,9 @@ private static void setupLogging(Settings settings, Environment environment) {
195204
}
196205
}
197206

198-
private static Tuple<Settings, Environment> initialSettings() {
199-
return InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
207+
private static Tuple<Settings, Environment> initialSettings(boolean foreground) {
208+
Terminal terminal = foreground ? Terminal.DEFAULT : null;
209+
return InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, terminal);
200210
}
201211

202212
private void start() {
@@ -227,7 +237,7 @@ public static void main(String[] args) {
227237
Settings settings = null;
228238
Environment environment = null;
229239
try {
230-
Tuple<Settings, Environment> tuple = initialSettings();
240+
Tuple<Settings, Environment> tuple = initialSettings(foreground);
231241
settings = tuple.v1();
232242
environment = tuple.v2();
233243

src/main/java/org/elasticsearch/common/cli/CliTool.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ protected CliTool(CliToolConfig config, Terminal terminal) {
9393
Preconditions.checkArgument(config.cmds().size() != 0, "At least one command must be configured");
9494
this.config = config;
9595
this.terminal = terminal;
96-
Tuple<Settings, Environment> tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
96+
Tuple<Settings, Environment> tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, terminal);
9797
settings = tuple.v1();
9898
env = tuple.v2();
9999
}

src/main/java/org/elasticsearch/common/settings/Settings.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,7 @@ public Builder putProperties(String prefix, Properties properties, String[] igno
12111211
* tries and resolve it against an environment variable ({@link System#getenv(String)}), and last, tries
12121212
* and replace it with another setting already set on this builder.
12131213
*/
1214-
public Builder replacePropertyPlaceholders() {
1214+
public Builder replacePropertyPlaceholders(String... ignoredValues) {
12151215
PropertyPlaceholder propertyPlaceholder = new PropertyPlaceholder("${", "}", false);
12161216
PropertyPlaceholder.PlaceholderResolver placeholderResolver = new PropertyPlaceholder.PlaceholderResolver() {
12171217
@Override
@@ -1241,7 +1241,19 @@ public boolean shouldIgnoreMissing(String placeholderName) {
12411241
}
12421242
};
12431243
for (Map.Entry<String, String> entry : Maps.newHashMap(map).entrySet()) {
1244-
String value = propertyPlaceholder.replacePlaceholders(entry.getValue(), placeholderResolver);
1244+
String possiblePlaceholder = entry.getValue();
1245+
boolean ignored = false;
1246+
for (String ignoredValue : ignoredValues) {
1247+
if (ignoredValue.equals(possiblePlaceholder)) {
1248+
ignored = true;
1249+
break;
1250+
}
1251+
}
1252+
if (ignored) {
1253+
continue;
1254+
}
1255+
1256+
String value = propertyPlaceholder.replacePlaceholders(possiblePlaceholder, placeholderResolver);
12451257
// if the values exists and has length, we should maintain it in the map
12461258
// otherwise, the replace process resolved into removing it
12471259
if (Strings.hasLength(value)) {

src/main/java/org/elasticsearch/node/internal/InternalSettingsPreparer.java

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
package org.elasticsearch.node.internal;
2121

2222
import com.google.common.collect.ImmutableList;
23+
import com.google.common.collect.UnmodifiableIterator;
2324
import org.elasticsearch.cluster.ClusterName;
2425
import org.elasticsearch.common.Names;
2526
import org.elasticsearch.common.Strings;
27+
import org.elasticsearch.common.cli.Terminal;
2628
import org.elasticsearch.common.collect.Tuple;
2729
import org.elasticsearch.common.settings.Settings;
2830
import org.elasticsearch.env.Environment;
@@ -41,10 +43,38 @@ public class InternalSettingsPreparer {
4143

4244
static final List<String> ALLOWED_SUFFIXES = ImmutableList.of(".yml", ".yaml", ".json", ".properties");
4345

46+
public static final String SECRET_PROMPT_VALUE = "${prompt::secret}";
47+
public static final String TEXT_PROMPT_VALUE = "${prompt::text}";
48+
public static final String IGNORE_SYSTEM_PROPERTIES_SETTING = "config.ignore_system_properties";
49+
50+
/**
51+
* Prepares the settings by gathering all elasticsearch system properties, optionally loading the configuration settings,
52+
* and then replacing all property placeholders. This method will not work with settings that have <code>__prompt__</code>
53+
* as their value unless they have been resolved previously.
54+
* @param pSettings The initial settings to use
55+
* @param loadConfigSettings flag to indicate whether to load settings from the configuration directory/file
56+
* @return the {@link Settings} and {@link Environment} as a {@link Tuple}
57+
*/
4458
public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, boolean loadConfigSettings) {
59+
return prepareSettings(pSettings, loadConfigSettings, null);
60+
}
61+
62+
/**
63+
* Prepares the settings by gathering all elasticsearch system properties, optionally loading the configuration settings,
64+
* and then replacing all property placeholders. If a {@link Terminal} is provided and configuration settings are loaded,
65+
* settings with the <code>__prompt__</code> value will result in a prompt for the setting to the user.
66+
* @param pSettings The initial settings to use
67+
* @param loadConfigSettings flag to indicate whether to load settings from the configuration directory/file
68+
* @param terminal the Terminal to use for input/output
69+
* @return the {@link Settings} and {@link Environment} as a {@link Tuple}
70+
*/
71+
public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, boolean loadConfigSettings, Terminal terminal) {
4572
// ignore this prefixes when getting properties from es. and elasticsearch.
4673
String[] ignorePrefixes = new String[]{"es.default.", "elasticsearch.default."};
47-
boolean useSystemProperties = !pSettings.getAsBoolean("config.ignore_system_properties", false);
74+
// ignore the special prompt placeholders since they have the same format as property placeholders and will be resolved
75+
// as having a default value because of the ':' in the format
76+
String[] ignoredPlaceholders = new String[] { SECRET_PROMPT_VALUE, TEXT_PROMPT_VALUE };
77+
boolean useSystemProperties = !pSettings.getAsBoolean(IGNORE_SYSTEM_PROPERTIES_SETTING, false);
4878
// just create enough settings to build the environment
4979
Settings.Builder settingsBuilder = settingsBuilder().put(pSettings);
5080
if (useSystemProperties) {
@@ -53,7 +83,7 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
5383
.putProperties("elasticsearch.", System.getProperties(), ignorePrefixes)
5484
.putProperties("es.", System.getProperties(), ignorePrefixes);
5585
}
56-
settingsBuilder.replacePropertyPlaceholders();
86+
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);
5787

5888
Environment environment = new Environment(settingsBuilder.build());
5989

@@ -91,17 +121,17 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
91121
settingsBuilder.putProperties("elasticsearch.", System.getProperties(), ignorePrefixes)
92122
.putProperties("es.", System.getProperties(), ignorePrefixes);
93123
}
94-
settingsBuilder.replacePropertyPlaceholders();
124+
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);
95125

96126
// allow to force set properties based on configuration of the settings provided
97127
for (Map.Entry<String, String> entry : pSettings.getAsMap().entrySet()) {
98128
String setting = entry.getKey();
99129
if (setting.startsWith("force.")) {
100130
settingsBuilder.remove(setting);
101-
settingsBuilder.put(setting.substring(".force".length()), entry.getValue());
131+
settingsBuilder.put(setting.substring("force.".length()), entry.getValue());
102132
}
103133
}
104-
settingsBuilder.replacePropertyPlaceholders();
134+
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);
105135

106136
// generate the name
107137
if (settingsBuilder.get("name") == null) {
@@ -123,7 +153,7 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
123153
settingsBuilder.put(ClusterName.SETTING, ClusterName.DEFAULT.value());
124154
}
125155

126-
Settings v1 = settingsBuilder.build();
156+
Settings v1 = replacePromptPlaceholders(settingsBuilder.build(), terminal);
127157
environment = new Environment(v1);
128158

129159
// put back the env settings
@@ -135,4 +165,45 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
135165

136166
return new Tuple<>(v1, environment);
137167
}
168+
169+
static Settings replacePromptPlaceholders(Settings settings, Terminal terminal) {
170+
UnmodifiableIterator<Map.Entry<String, String>> iter = settings.getAsMap().entrySet().iterator();
171+
Settings.Builder builder = Settings.builder();
172+
173+
while (iter.hasNext()) {
174+
Map.Entry<String, String> entry = iter.next();
175+
String value = entry.getValue();
176+
String key = entry.getKey();
177+
switch (value) {
178+
case SECRET_PROMPT_VALUE:
179+
String secretValue = promptForValue(key, terminal, true);
180+
if (Strings.hasLength(secretValue)) {
181+
builder.put(key, secretValue);
182+
}
183+
break;
184+
case TEXT_PROMPT_VALUE:
185+
String textValue = promptForValue(key, terminal, false);
186+
if (Strings.hasLength(textValue)) {
187+
builder.put(key, textValue);
188+
}
189+
break;
190+
default:
191+
builder.put(key, value);
192+
break;
193+
}
194+
}
195+
196+
return builder.build();
197+
}
198+
199+
static String promptForValue(String key, Terminal terminal, boolean secret) {
200+
if (terminal == null) {
201+
throw new UnsupportedOperationException("found property [" + key + "] with value [" + (secret ? SECRET_PROMPT_VALUE : TEXT_PROMPT_VALUE) +"]. prompting for property values is only supported when running elasticsearch in the foreground");
202+
}
203+
204+
if (secret) {
205+
return new String(terminal.readSecret("Enter value for [%s]: ", key));
206+
}
207+
return terminal.readText("Enter value for [%s]: ", key);
208+
}
138209
}

src/main/java/org/elasticsearch/plugins/PluginManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.lucene.util.IOUtils;
2626
import org.elasticsearch.*;
2727
import org.elasticsearch.common.SuppressForbidden;
28+
import org.elasticsearch.common.cli.Terminal;
2829
import org.elasticsearch.common.collect.Tuple;
2930
import org.elasticsearch.common.http.client.HttpDownloadHelper;
3031
import org.elasticsearch.common.io.FileSystemUtils;
@@ -338,7 +339,7 @@ public void listInstalledPlugins() throws IOException {
338339
private static final int EXIT_CODE_ERROR = 70;
339340

340341
public static void main(String[] args) {
341-
Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
342+
Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, Terminal.DEFAULT);
342343

343344
try {
344345
Files.createDirectories(initialSettings.v2().pluginsFile());

src/main/java/org/elasticsearch/tribe/TribeService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.elasticsearch.gateway.GatewayService;
4646
import org.elasticsearch.node.Node;
4747
import org.elasticsearch.node.NodeBuilder;
48+
import org.elasticsearch.node.internal.InternalSettingsPreparer;
4849
import org.elasticsearch.rest.RestStatus;
4950

5051
import java.util.EnumSet;
@@ -128,7 +129,7 @@ public TribeService(Settings settings, ClusterService clusterService, DiscoveryS
128129
sb.put("node.name", settings.get("name") + "/" + entry.getKey());
129130
sb.put("path.home", settings.get("path.home")); // pass through ES home dir
130131
sb.put(TRIBE_NAME, entry.getKey());
131-
sb.put("config.ignore_system_properties", true);
132+
sb.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true);
132133
if (sb.get("http.enabled") == null) {
133134
sb.put("http.enabled", false);
134135
}

src/test/java/org/elasticsearch/client/transport/TransportClientRetryTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.cluster.ClusterState;
2929
import org.elasticsearch.common.settings.Settings;
3030
import org.elasticsearch.common.transport.TransportAddress;
31+
import org.elasticsearch.node.internal.InternalSettingsPreparer;
3132
import org.elasticsearch.plugins.PluginsService;
3233
import org.elasticsearch.test.ElasticsearchIntegrationTest;
3334
import org.elasticsearch.test.InternalTestCluster;
@@ -61,7 +62,7 @@ public void testRetry() throws IOException, ExecutionException, InterruptedExcep
6162
.put("node.mode", InternalTestCluster.nodeMode())
6263
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
6364
.put(ClusterName.SETTING, internalCluster().getClusterName())
64-
.put("config.ignore_system_properties", true)
65+
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true)
6566
.put("path.home", createTempDir());
6667

6768
try (TransportClient transportClient = TransportClient.builder().settings(builder.build()).build()) {

src/test/java/org/elasticsearch/client/transport/TransportClientTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.common.settings.Settings;
2626
import org.elasticsearch.common.transport.TransportAddress;
2727
import org.elasticsearch.node.Node;
28+
import org.elasticsearch.node.internal.InternalSettingsPreparer;
2829
import org.elasticsearch.test.ElasticsearchIntegrationTest;
2930
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
3031
import org.elasticsearch.transport.TransportService;
@@ -59,7 +60,7 @@ public void testNodeVersionIsUpdated() {
5960
.put("path.home", createTempDir())
6061
.put("node.name", "testNodeVersionIsUpdated")
6162
.put("http.enabled", false)
62-
.put("config.ignore_system_properties", true) // make sure we get what we set :)
63+
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true) // make sure we get what we set :)
6364
.build()).clusterName("foobar").build();
6465
node.start();
6566
try {

src/test/java/org/elasticsearch/common/cli/CliToolTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import org.elasticsearch.common.Strings;
2626
import org.elasticsearch.common.settings.Settings;
2727
import org.elasticsearch.env.Environment;
28+
import org.elasticsearch.node.internal.InternalSettingsPreparer;
2829
import org.junit.Test;
2930

3031
import java.io.IOException;
3132
import java.util.Map;
33+
import java.util.concurrent.atomic.AtomicInteger;
3234
import java.util.concurrent.atomic.AtomicReference;
3335

3436
import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
@@ -271,6 +273,49 @@ public CliTool.ExitStatus execute(Settings settings, Environment env) {
271273
tool.parse("cmd", Strings.splitStringByCommaToArray("--help"));
272274
}
273275

276+
@Test
277+
public void testPromptForSetting() throws Exception {
278+
final AtomicInteger counter = new AtomicInteger();
279+
final AtomicReference<String> promptedSecretValue = new AtomicReference<>(null);
280+
final AtomicReference<String> promptedTextValue = new AtomicReference<>(null);
281+
final Terminal terminal = new MockTerminal() {
282+
@Override
283+
public char[] readSecret(String text, Object... args) {
284+
counter.incrementAndGet();
285+
assertThat(args, arrayContaining((Object) "foo.password"));
286+
return "changeit".toCharArray();
287+
}
288+
289+
@Override
290+
public String readText(String text, Object... args) {
291+
counter.incrementAndGet();
292+
assertThat(args, arrayContaining((Object) "replace"));
293+
return "replaced";
294+
}
295+
};
296+
final NamedCommand cmd = new NamedCommand("noop", terminal) {
297+
@Override
298+
public CliTool.ExitStatus execute(Settings settings, Environment env) {
299+
promptedSecretValue.set(settings.get("foo.password"));
300+
promptedTextValue.set(settings.get("replace"));
301+
return CliTool.ExitStatus.OK;
302+
}
303+
};
304+
305+
System.setProperty("es.foo.password", InternalSettingsPreparer.SECRET_PROMPT_VALUE);
306+
System.setProperty("es.replace", InternalSettingsPreparer.TEXT_PROMPT_VALUE);
307+
try {
308+
new SingleCmdTool("tool", terminal, cmd).execute();
309+
} finally {
310+
System.clearProperty("es.foo.password");
311+
System.clearProperty("es.replace");
312+
}
313+
314+
assertThat(counter.intValue(), is(2));
315+
assertThat(promptedSecretValue.get(), is("changeit"));
316+
assertThat(promptedTextValue.get(), is("replaced"));
317+
}
318+
274319
private void assertStatus(int status, CliTool.ExitStatus expectedStatus) {
275320
assertThat(status, is(expectedStatus.status()));
276321
}

0 commit comments

Comments
 (0)