Settings: Add infrastructure for elasticsearch keystore

This change is the first towards providing the ability to store
sensitive settings in elasticsearch. It adds the
`elasticsearch-keystore` tool, which allows managing a java keystore.
The keystore is loaded upon node startup in Elasticsearch, and used by
the Setting infrastructure when a setting is configured as secure.

There are a lot of caveats to this PR. The most important is it only
provides the tool and setting infrastructure for secure strings. It does
not yet provide for keystore passwords, keypairs, certificates, or even
convert any existing string settings to secure string settings. Those
will all come in follow up PRs. But this PR was already too big, so this
at least gets a basic version of the infrastructure in.

The two main things to look at.  The first is the `SecureSetting` class,
which extends `Setting`, but removes the assumption for the raw value of the
setting to be a string. SecureSetting provides, for now, a single
helper, `stringSetting()` to create a SecureSetting which will return a
SecureString (which is like String, but is closeable, so that the
underlying character array can be cleared). The second is the
`KeyStoreWrapper` class, which wraps the java `KeyStore` to provide a
simpler api (we do not need the entire keystore api) and also extend
the serialized format to add metadata needed for loading the keystore
with no assumptions about keystore type (so that we can change this in
the future) as well as whether the keystore has a password (so that we
can know whether prompting is necessary when we add support for keystore
passwords).
This commit is contained in:
Ryan Ernst 2016-12-22 16:28:34 -08:00
parent 9cd9576b84
commit fb690ef748
26 changed files with 1419 additions and 38 deletions

View File

@ -94,6 +94,8 @@ dependencies {
exclude group: 'org.elasticsearch', module: 'elasticsearch'
}
}
testCompile 'com.google.jimfs:jimfs:1.1'
testCompile 'com.google.guava:guava:18.0'
}
if (isEclipse) {

View File

@ -30,16 +30,15 @@ import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.StringHelper;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.PidFile;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.inject.CreationException;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.logging.LogConfigurator;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.BoundTransportAddress;
import org.elasticsearch.env.Environment;
@ -224,13 +223,34 @@ final class Bootstrap {
};
}
private static Environment initialEnvironment(boolean foreground, Path pidFile, Settings initialSettings) {
private static KeyStoreWrapper loadKeyStore(Environment env0) throws BootstrapException {
final KeyStoreWrapper keystore;
try {
keystore = KeyStoreWrapper.loadMetadata(env0.configFile());
} catch (IOException e) {
throw new BootstrapException(e);
}
if (keystore == null) {
return null; // no keystore
}
try {
keystore.loadKeystore(new char[0] /* TODO: read password from stdin */);
} catch (Exception e) {
throw new BootstrapException(e);
}
return keystore;
}
private static Environment initialEnvironment(boolean foreground, Path pidFile,
KeyStoreWrapper keystore, Settings initialSettings) {
Terminal terminal = foreground ? Terminal.DEFAULT : null;
Settings.Builder builder = Settings.builder();
if (pidFile != null) {
builder.put(Environment.PIDFILE_SETTING.getKey(), pidFile);
}
builder.put(initialSettings);
builder.setKeyStore(keystore);
return InternalSettingsPreparer.prepareEnvironment(builder.build(), terminal, Collections.emptyMap());
}
@ -261,7 +281,7 @@ final class Bootstrap {
final boolean foreground,
final Path pidFile,
final boolean quiet,
final Settings initialSettings) throws BootstrapException, NodeValidationException, UserException {
final Environment env0) throws BootstrapException, NodeValidationException, UserException {
// Set the system property before anything has a chance to trigger its use
initLoggerPrefix();
@ -271,7 +291,8 @@ final class Bootstrap {
INSTANCE = new Bootstrap();
Environment environment = initialEnvironment(foreground, pidFile, initialSettings);
final KeyStoreWrapper keystore = loadKeyStore(env0);
Environment environment = initialEnvironment(foreground, pidFile, keystore, env0.settings());
try {
LogConfigurator.configure(environment);
} catch (IOException e) {
@ -309,6 +330,13 @@ final class Bootstrap {
INSTANCE.setup(true, environment);
try {
// any secure settings must be read during node construction
IOUtils.close(keystore);
} catch (IOException e) {
throw new BootstrapException(e);
}
INSTANCE.start();
if (closeStandardStreams) {

View File

@ -26,7 +26,7 @@ import java.nio.file.Path;
* during bootstrap should explicitly declare the checked exceptions that they can throw, rather
* than declaring the top-level checked exception {@link Exception}. This exception exists to wrap
* these checked exceptions so that
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.common.settings.Settings)}
* {@link Bootstrap#init(boolean, Path, boolean, org.elasticsearch.env.Environment)}
* does not have to declare all of these checked exceptions.
*/
class BootstrapException extends Exception {

View File

@ -111,16 +111,16 @@ class Elasticsearch extends EnvironmentAwareCommand {
final boolean quiet = options.has(quietOption);
try {
init(daemonize, pidFile, quiet, env.settings());
init(daemonize, pidFile, quiet, env);
} catch (NodeValidationException e) {
throw new UserException(ExitCodes.CONFIG, e.getMessage());
}
}
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings)
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment env0)
throws NodeValidationException, UserException {
try {
Bootstrap.init(!daemonize, pidFile, quiet, initialSettings);
Bootstrap.init(!daemonize, pidFile, quiet, env0);
} catch (BootstrapException | RuntimeException e) {
// format exceptions to the console in a special way
// to avoid 2MB stacktraces from guice, etc.

View File

@ -0,0 +1,86 @@
/*
* 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.common.settings;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli which adds a string setting.
*/
class AddStringKeyStoreCommand extends EnvironmentAwareCommand {
private final OptionSpec<Void> stdinOption;
private final OptionSpec<Void> forceOption;
private final OptionSpec<String> arguments;
AddStringKeyStoreCommand() {
super("Add a string setting to the keystore");
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin");
this.forceOption = parser.acceptsAll(Arrays.asList("f", "force"), "Overwrite existing setting without prompting");
this.arguments = parser.nonOptions("setting name");
}
// pkg private so tests can manipulate
InputStream getStdin() {
return System.in;
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.loadMetadata(env.configFile());
if (keystore == null) {
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
}
keystore.loadKeystore(new char[0] /* TODO: prompt for password when they are supported */);
String setting = arguments.value(options);
if (keystore.getSettings().contains(setting) && options.has(forceOption) == false) {
String answer = terminal.readText("Setting " + setting + " already exists. Overwrite? [y/N]");
if (answer.equals("y") == false) {
terminal.println("Exiting without modifying keystore.");
return;
}
}
final char[] value;
if (options.has(stdinOption)) {
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
value = stdinReader.readLine().toCharArray();
} else {
value = terminal.readSecret("Enter value for " + setting + ": ");
}
keystore.setStringSetting(setting, value);
keystore.save(env.configFile());
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.common.settings;
import java.nio.file.Files;
import java.nio.file.Path;
import joptsimple.OptionSet;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli to create a new keystore.
*/
class CreateKeyStoreCommand extends EnvironmentAwareCommand {
CreateKeyStoreCommand() {
super("Creates a new elasticsearch keystore");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
if (Files.exists(keystoreFile)) {
String answer = terminal.readText("An elasticsearch keystore already exists. Overwrite? [y/N] ");
if (answer.equals("y") == false) {
terminal.println("Exiting without creating keystore.");
return;
}
}
char[] password = new char[0];// terminal.readSecret("Enter passphrase (empty for no passphrase): ");
/* TODO: uncomment when entering passwords on startup is supported
char[] passwordRepeat = terminal.readSecret("Enter same passphrase again: ");
if (Arrays.equals(password, passwordRepeat) == false) {
throw new UserException(ExitCodes.DATA_ERROR, "Passphrases are not equal, exiting.");
}*/
KeyStoreWrapper keystore = KeyStoreWrapper.create(password);
keystore.save(env.configFile());
terminal.println("Created elasticsearch keystore in " + env.configFile());
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.common.settings;
import org.elasticsearch.cli.MultiCommand;
import org.elasticsearch.cli.Terminal;
/**
* A cli tool for managing secrets in the elasticsearch keystore.
*/
public class KeyStoreCli extends MultiCommand {
private KeyStoreCli() {
super("A tool for managing settings stored in the elasticsearch keystore");
subcommands.put("create", new CreateKeyStoreCommand());
subcommands.put("list", new ListKeyStoreCommand());
subcommands.put("add", new AddStringKeyStoreCommand());
subcommands.put("remove", new RemoveSettingKeyStoreCommand());
}
public static void main(String[] args) throws Exception {
exit(new KeyStoreCli().main(args, Terminal.DEFAULT));
}
}

View File

@ -0,0 +1,218 @@
/*
* 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.common.settings;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.security.auth.DestroyFailedException;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import org.apache.lucene.util.SetOnce;
/**
* A wrapper around a Java KeyStore which provides supplements the keystore with extra metadata.
*
* Loading a keystore has 2 phases. First, call {@link #loadMetadata(Path)}. Then call
* {@link #loadKeystore(char[])} with the keystore password, or an empty char array if
* {@link #hasPassword()} is {@code false}.
*/
public class KeyStoreWrapper implements Closeable {
/** The version of the metadata written before the keystore data. */
private static final int FORMAT_VERSION = 1;
/** The keystore type for a newly created keystore. */
private static final String NEW_KEYSTORE_TYPE = "PKCS12";
/** A factory necessary for constructing instances of secrets in a {@link KeyStore}. */
private static final SecretKeyFactory passwordFactory;
static {
try {
passwordFactory = SecretKeyFactory.getInstance("PBE");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/** True iff the keystore has a password needed to read. */
private final boolean hasPassword;
/** The type of the keystore, as passed to {@link java.security.KeyStore#getInstance(String)} */
private final String type;
/** A stream of the actual keystore data. */
private final InputStream input;
/** The loaded keystore. See {@link #loadKeystore(char[])}. */
private final SetOnce<KeyStore> keystore = new SetOnce<>();
/** The password for the keystore. See {@link #loadKeystore(char[])}. */
private final SetOnce<KeyStore.PasswordProtection> keystorePassword = new SetOnce<>();
/** The setting names contained in the loaded keystore. */
private final Set<String> settingNames = new HashSet<>();
private KeyStoreWrapper(boolean hasPassword, String type, InputStream input) {
this.hasPassword = hasPassword;
this.type = type;
this.input = input;
}
/** Returns a path representing the ES keystore in the given config dir. */
static Path keystorePath(Path configDir) {
return configDir.resolve("elasticsearch.keystore");
}
/** Constructs a new keystore with the given password. */
static KeyStoreWrapper create(char[] password) throws Exception {
KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, null);
KeyStore keyStore = KeyStore.getInstance(NEW_KEYSTORE_TYPE);
keyStore.load(null, null);
wrapper.keystore.set(keyStore);
wrapper.keystorePassword.set(new KeyStore.PasswordProtection(password));
return wrapper;
}
/**
* Loads information about the Elasticsearch keystore from the provided config directory.
*
* {@link #loadKeystore(char[])} must be called before reading or writing any entries.
* Returns {@code null} if no keystore exists.
*/
public static KeyStoreWrapper loadMetadata(Path configDir) throws IOException {
Path keystoreFile = keystorePath(configDir);
if (Files.exists(keystoreFile) == false) {
return null;
}
DataInputStream inputStream = new DataInputStream(Files.newInputStream(keystoreFile));
int format = inputStream.readInt();
if (format != FORMAT_VERSION) {
throw new IllegalStateException("Unknown keystore metadata format [" + format + "]");
}
boolean hasPassword = inputStream.readBoolean();
String type = inputStream.readUTF();
return new KeyStoreWrapper(hasPassword, type, inputStream);
}
/** Returns true iff {@link #loadKeystore(char[])} has been called. */
public boolean isLoaded() {
return keystore.get() != null;
}
/** Return true iff calling {@link #loadKeystore(char[])} requires a non-empty password. */
public boolean hasPassword() {
return hasPassword;
}
/** Loads the keystore this metadata wraps. This may only be called once. */
public void loadKeystore(char[] password) throws GeneralSecurityException, IOException {
this.keystore.set(KeyStore.getInstance(type));
try (InputStream in = input) {
keystore.get().load(in, password);
}
this.keystorePassword.set(new KeyStore.PasswordProtection(password));
Arrays.fill(password, '\0');
// convert keystore aliases enum into a set for easy lookup
Enumeration<String> aliases = keystore.get().aliases();
while (aliases.hasMoreElements()) {
settingNames.add(aliases.nextElement());
}
}
/** Write the keystore to the given config directory. */
void save(Path configDir) throws Exception {
Path keystoreFile = keystorePath(configDir);
try (DataOutputStream outputStream = new DataOutputStream(Files.newOutputStream(keystoreFile))) {
outputStream.writeInt(FORMAT_VERSION);
char[] password = this.keystorePassword.get().getPassword();
outputStream.writeBoolean(password.length != 0);
outputStream.writeUTF(type);
keystore.get().store(outputStream, password);
}
PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreFile, PosixFileAttributeView.class);
if (attrs != null) {
// don't rely on umask: ensure the keystore has minimal permissions
attrs.setPermissions(PosixFilePermissions.fromString("rw-------"));
}
}
/** Returns the names of all settings in this keystore. */
public Set<String> getSettings() {
return settingNames;
}
/** Retrieve a string setting. The {@link SecureString} should be closed once it is used. */
SecureString getStringSetting(String setting) throws GeneralSecurityException {
KeyStore.Entry entry = keystore.get().getEntry(setting, keystorePassword.get());
if (entry instanceof KeyStore.SecretKeyEntry == false) {
throw new IllegalStateException("Secret setting " + setting + " is not a string");
}
// TODO: only allow getting a setting once?
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)entry;
PBEKeySpec keySpec = (PBEKeySpec)passwordFactory.getKeySpec(secretKeyEntry.getSecretKey(), PBEKeySpec.class);
SecureString value = new SecureString(keySpec.getPassword());
keySpec.clearPassword();
return value;
}
/** Set a string setting. */
void setStringSetting(String setting, char[] value) throws GeneralSecurityException {
SecretKey secretKey = passwordFactory.generateSecret(new PBEKeySpec(value));
keystore.get().setEntry(setting, new KeyStore.SecretKeyEntry(secretKey), keystorePassword.get());
settingNames.add(setting);
}
/** Remove the given setting from the keystore. */
void remove(String setting) throws KeyStoreException {
keystore.get().deleteEntry(setting);
settingNames.remove(setting);
}
@Override
public void close() throws IOException {
try {
if (keystorePassword.get() != null) {
keystorePassword.get().destroy();
}
} catch (DestroyFailedException e) {
throw new IOException(e);
}
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.common.settings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import joptsimple.OptionSet;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli to list all settings in the keystore.
*/
class ListKeyStoreCommand extends EnvironmentAwareCommand {
ListKeyStoreCommand() {
super("List entries in the keystore");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.loadMetadata(env.configFile());
if (keystore == null) {
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
}
keystore.loadKeystore(new char[0] /* TODO: prompt for password when they are supported */);
List<String> sortedEntries = new ArrayList<>(keystore.getSettings());
Collections.sort(sortedEntries);
for (String entry : sortedEntries) {
terminal.println(entry);
}
// TODO:
// 2. delete command
// 3. tests for delete
// 4. shell script
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.common.settings;
import java.util.List;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
/**
* A subcommand for the keystore cli to remove a setting.
*/
class RemoveSettingKeyStoreCommand extends EnvironmentAwareCommand {
private final OptionSpec<String> arguments;
RemoveSettingKeyStoreCommand() {
super("Remove a setting from the keystore");
arguments = parser.nonOptions("setting names");
}
@Override
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
List<String> settings = arguments.values(options);
if (settings.isEmpty()) {
throw new UserException(ExitCodes.USAGE, "Must supply at least one setting to remove");
}
KeyStoreWrapper keystore = KeyStoreWrapper.loadMetadata(env.configFile());
if (keystore == null) {
throw new UserException(ExitCodes.DATA_ERROR, "Elasticsearch keystore not found. Use 'create' command to create one.");
}
keystore.loadKeystore(new char[0] /* TODO: prompt for password when they are supported */);
for (String setting : arguments.values(options)) {
if (keystore.getSettings().contains(setting) == false) {
throw new UserException(ExitCodes.CONFIG, "Setting [" + setting + "] does not exist in the keystore.");
}
keystore.remove(setting);
}
keystore.save(env.configFile());
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.common.settings;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* A secure setting.
*
* This class allows access to settings from the Elasticsearch keystore.
*/
public abstract class SecureSetting<T> extends Setting<T> {
private static final Set<Property> ALLOWED_PROPERTIES = new HashSet<>(
Arrays.asList(Property.Deprecated, Property.Shared)
);
private SecureSetting(String key, Setting.Property... properties) {
super(key, (String)null, null, properties);
assert assertAllowedProperties(properties);
}
private boolean assertAllowedProperties(Setting.Property... properties) {
for (Setting.Property property : properties) {
if (ALLOWED_PROPERTIES.contains(property) == false) {
return false;
}
}
return true;
}
@Override
public String getDefaultRaw(Settings settings) {
throw new UnsupportedOperationException("secure settings are not strings");
}
@Override
public T getDefault(Settings settings) {
throw new UnsupportedOperationException("secure settings are not strings");
}
@Override
public String getRaw(Settings settings) {
throw new UnsupportedOperationException("secure settings are not strings");
}
@Override
public T get(Settings settings) {
checkDeprecation(settings);
final KeyStoreWrapper keystore = Objects.requireNonNull(settings.getKeyStore());
if (keystore.getSettings().contains(getKey()) == false) {
return getFallback(settings);
}
try {
return getSecret(keystore);
} catch (GeneralSecurityException e) {
throw new RuntimeException("failed to read secure setting " + getKey(), e);
}
}
/** Returns the secret setting from the keyStoreReader store. */
abstract T getSecret(KeyStoreWrapper keystore) throws GeneralSecurityException;
/** Returns the value from a fallback setting. Returns null if no fallback exists. */
abstract T getFallback(Settings settings);
// TODO: override toXContent
/**
* A setting which contains a sensitive string.
*
* This may be any sensitive string, e.g. a username, a password, an auth token, etc.
*/
public static SecureSetting<SecureString> stringSetting(String name, Setting<String> fallback, Property... properties) {
return new SecureSetting<SecureString>(name, properties) {
@Override
protected SecureString getSecret(KeyStoreWrapper keystore) throws GeneralSecurityException {
return keystore.getStringSetting(getKey());
}
@Override
SecureString getFallback(Settings settings) {
if (fallback != null) {
return new SecureString(fallback.get(settings).toCharArray());
}
return null;
}
};
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.common.settings;
import java.util.Arrays;
import java.util.Objects;
/**
* A String implementations which allows clearing the underlying char array.
*/
public final class SecureString implements CharSequence, AutoCloseable {
private char[] chars;
/**
* Constructs a new SecureString which controls the passed in char array.
*
* Note: When this instance is closed, the array will be zeroed out.
*/
public SecureString(char[] chars) {
this.chars = Objects.requireNonNull(chars);
}
/** Constant time equality to avoid potential timing attacks. */
@Override
public boolean equals(Object o) {
ensureNotClosed();
if (this == o) return true;
if (o == null || o instanceof CharSequence == false) return false;
CharSequence that = (CharSequence) o;
if (chars.length != that.length()) {
return false;
}
int equals = 0;
for (int i = 0; i < chars.length; i++) {
equals |= chars[i] ^ that.charAt(i);
}
return equals == 0;
}
@Override
public int hashCode() {
return Arrays.hashCode(chars);
}
@Override
public int length() {
ensureNotClosed();
return chars.length;
}
@Override
public char charAt(int index) {
ensureNotClosed();
return chars[index];
}
@Override
public SecureString subSequence(int start, int end) {
throw new UnsupportedOperationException("Cannot get subsequence of SecureString");
}
/**
* Convert to a {@link String}. This only be used with APIs that do not take {@link CharSequence}.
*/
@Override
public String toString() {
return new String(chars);
}
/**
* Closes the string by clearing the underlying char array.
*/
@Override
public void close() {
Arrays.fill(chars, '\0');
chars = null;
}
/** Throw an exception if this string has been closed, indicating something is trying to access the data after being closed. */
private void ensureNotClosed() {
if (chars == null) {
throw new IllegalStateException("SecureString has already been closed");
}
}
}

View File

@ -274,7 +274,7 @@ public class Setting<T> extends ToXContentToBytes {
* Returns the default value string representation for this setting.
* @param settings a settings object for settings that has a default value depending on another setting if available
*/
public final String getDefaultRaw(Settings settings) {
public String getDefaultRaw(Settings settings) {
return defaultValue.apply(settings);
}
@ -282,7 +282,7 @@ public class Setting<T> extends ToXContentToBytes {
* Returns the default value for this setting.
* @param settings a settings object for settings that has a default value depending on another setting if available
*/
public final T getDefault(Settings settings) {
public T getDefault(Settings settings) {
return parser.apply(getDefaultRaw(settings));
}
@ -290,7 +290,7 @@ public class Setting<T> extends ToXContentToBytes {
* Returns <code>true</code> iff this setting is present in the given settings object. Otherwise <code>false</code>
*/
public boolean exists(Settings settings) {
return settings.get(getKey()) != null;
return settings.contains(getKey());
}
/**
@ -330,14 +330,19 @@ public class Setting<T> extends ToXContentToBytes {
* instead. This is useful if the value can't be parsed due to an invalid value to access the actual value.
*/
public String getRaw(Settings settings) {
checkDeprecation(settings);
return settings.get(getKey(), defaultValue.apply(settings));
}
/** Logs a deprecation warning if the setting is deprecated and used. */
protected void checkDeprecation(Settings settings) {
// They're using the setting, so we need to tell them to stop
if (this.isDeprecated() && this.exists(settings)) {
// It would be convenient to show its replacement key, but replacement is often not so simple
final DeprecationLogger deprecationLogger = new DeprecationLogger(Loggers.getLogger(getClass()));
deprecationLogger.deprecated("[{}] setting was deprecated in Elasticsearch and it will be removed in a future release! " +
"See the breaking changes lists in the documentation for details", getKey());
"See the breaking changes lists in the documentation for details", getKey());
}
return settings.get(getKey(), defaultValue.apply(settings));
}
/**

View File

@ -76,10 +76,29 @@ public final class Settings implements ToXContent {
public static final Settings EMPTY = new Builder().build();
private static final Pattern ARRAY_PATTERN = Pattern.compile("(.*)\\.\\d+$");
/** The raw settings from the full key to raw string value. */
private Map<String, String> settings;
Settings(Map<String, String> settings) {
this.settings = Collections.unmodifiableMap(settings);
/** The keystore storage associated with these settings. */
private KeyStoreWrapper keystore;
Settings(Map<String, String> settings, KeyStoreWrapper keystore) {
// we use a sorted map for consistent serialization when using getAsMap()
this.settings = Collections.unmodifiableSortedMap(new TreeMap<>(settings));
this.keystore = keystore;
}
/**
* Retrieve the keystore that contains secure settings.
*/
KeyStoreWrapper getKeyStore() {
// pkg private so it can only be accessed by local subclasses of SecureSetting
return keystore;
}
/** Returns true if the setting exists, false otherwise. */
public boolean contains(String key) {
return settings.containsKey(key) || keystore != null && keystore.getSettings().contains(key);
}
/**
@ -185,16 +204,18 @@ public final class Settings implements ToXContent {
/**
* A settings that are filtered (and key is removed) with the specified prefix.
* Secure settings may not be access through the prefixed settings.
*/
public Settings getByPrefix(String prefix) {
return new Settings(new FilteredMap(this.settings, (k) -> k.startsWith(prefix), prefix));
return new Settings(new FilteredMap(this.settings, (k) -> k.startsWith(prefix), prefix), null);
}
/**
* Returns a new settings object that contains all setting of the current one filtered by the given settings key predicate.
* Secure settings may not be accessed through a filter.
*/
public Settings filter(Predicate<String> predicate) {
return new Settings(new FilteredMap(this.settings, predicate, null));
return new Settings(new FilteredMap(this.settings, predicate, null), null);
}
/**
@ -456,7 +477,7 @@ public final class Settings implements ToXContent {
}
Map<String, Settings> retVal = new LinkedHashMap<>();
for (Map.Entry<String, Map<String, String>> entry : map.entrySet()) {
retVal.put(entry.getKey(), new Settings(Collections.unmodifiableMap(entry.getValue())));
retVal.put(entry.getKey(), new Settings(Collections.unmodifiableMap(entry.getValue()), keystore));
}
return Collections.unmodifiableMap(retVal);
}
@ -591,6 +612,8 @@ public final class Settings implements ToXContent {
// we use a sorted map for consistent serialization when using getAsMap()
private final Map<String, String> map = new TreeMap<>();
private KeyStoreWrapper keystore;
private Builder() {
}
@ -613,6 +636,13 @@ public final class Settings implements ToXContent {
return map.get(key);
}
/** Sets the secret store for these settings. */
public void setKeyStore(KeyStoreWrapper keystore) {
assert this.keystore == null; // only set once!
assert keystore.isLoaded();
this.keystore = Objects.requireNonNull(keystore);
}
/**
* Puts tuples of key value pairs of settings. Simplified version instead of repeating calling
* put for each one.
@ -1019,7 +1049,7 @@ public final class Settings implements ToXContent {
* set on this builder.
*/
public Settings build() {
return new Settings(map);
return new Settings(map, keystore);
}
}

View File

@ -19,14 +19,6 @@
package org.elasticsearch.node.internal;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.env.Environment;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -39,6 +31,14 @@ import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.env.Environment;
import static org.elasticsearch.common.Strings.cleanPath;
public class InternalSettingsPreparer {

View File

@ -152,8 +152,8 @@ public class ElasticsearchCliTests extends ESElasticsearchCliTestCase {
ExitCodes.OK,
true,
output -> {},
(foreground, pidFile, quiet, esSettings) -> {
Map<String, String> settings = esSettings.getAsMap();
(foreground, pidFile, quiet, env) -> {
Map<String, String> settings = env.settings().getAsMap();
assertThat(settings, hasEntry("foo", "bar"));
assertThat(settings, hasEntry("baz", "qux"));
},

View File

@ -0,0 +1,125 @@
/*
* 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.common.settings;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
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.UserException;
import org.elasticsearch.env.Environment;
import static org.hamcrest.Matchers.containsString;
public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
InputStream input;
@Override
protected Command newCommand() {
return new AddStringKeyStoreCommand() {
@Override
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
return env;
}
@Override
InputStream getStdin() {
return input;
}
};
}
public void testMissing() throws Exception {
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
assertThat(e.getMessage(), containsString("keystore not found"));
}
public void testOverwritePromptDefault() throws Exception {
createKeystore("", "foo", "bar");
terminal.addTextInput("");
execute("foo");
assertSecureString("foo", "bar");
}
public void testOverwritePromptExplicitNo() throws Exception {
createKeystore("", "foo", "bar");
terminal.addTextInput("n"); // explicit no
execute("foo");
assertSecureString("foo", "bar");
}
public void testOverwritePromptExplicitYes() throws Exception {
createKeystore("", "foo", "bar");
terminal.addTextInput("y");
terminal.addSecretInput("newvalue");
execute("foo");
assertSecureString("foo", "newvalue");
}
public void testOverwriteForceShort() throws Exception {
createKeystore("", "foo", "bar");
terminal.addSecretInput("newvalue");
execute("-f", "foo"); // force
assertSecureString("foo", "newvalue");
}
public void testOverwriteForceLong() throws Exception {
createKeystore("", "foo", "bar");
terminal.addSecretInput("and yet another secret value");
execute("--force", "foo"); // force
assertSecureString("foo", "and yet another secret value");
}
public void testForceNonExistent() throws Exception {
createKeystore("");
terminal.addSecretInput("value");
execute("--force", "foo"); // force
assertSecureString("foo", "value");
}
public void testPromptForValue() throws Exception {
KeyStoreWrapper.create(new char[0]).save(env.configFile());
terminal.addSecretInput("secret value");
execute("foo");
assertSecureString("foo", "secret value");
}
public void testStdinShort() throws Exception {
KeyStoreWrapper.create(new char[0]).save(env.configFile());
setInput("secret value 1");
execute("-x", "foo");
assertSecureString("foo", "secret value 1");
}
public void testStdinLong() throws Exception {
KeyStoreWrapper.create(new char[0]).save(env.configFile());
setInput("secret value 2");
execute("--stdin", "foo");
assertSecureString("foo", "secret value 2");
}
void setInput(String inputStr) {
input = new ByteArrayInputStream(inputStr.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.common.settings;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.env.Environment;
public class CreateKeyStoreCommandTests extends KeyStoreCommandTestCase {
@Override
protected Command newCommand() {
return new CreateKeyStoreCommand() {
@Override
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
return env;
}
};
}
public void testPosix() throws Exception {
execute();
Path configDir = env.configFile();
assertNotNull(KeyStoreWrapper.loadMetadata(configDir));
}
public void testNotPosix() throws Exception {
setupEnv(false);
execute();
Path configDir = env.configFile();
assertNotNull(KeyStoreWrapper.loadMetadata(configDir));
}
public void testOverwrite() throws Exception {
Path keystoreFile = KeyStoreWrapper.keystorePath(env.configFile());
byte[] content = "not a keystore".getBytes();
Files.write(keystoreFile, content);
terminal.addTextInput(""); // default is no
execute();
assertArrayEquals(content, Files.readAllBytes(keystoreFile));
terminal.addTextInput("n"); // explicit no
execute();
assertArrayEquals(content, Files.readAllBytes(keystoreFile));
terminal.addTextInput("y");
execute();
assertNotNull(KeyStoreWrapper.loadMetadata(env.configFile()));
}
}

View File

@ -0,0 +1,98 @@
/*
* 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.common.settings;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.cli.CommandTestCase;
import org.elasticsearch.common.io.PathUtilsForTesting;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
/**
* Base test case for manipulating the ES keystore.
*/
@LuceneTestCase.SuppressFileSystems("*") // we do our own mocking
public abstract class KeyStoreCommandTestCase extends CommandTestCase {
Environment env;
List<FileSystem> fileSystems = new ArrayList<>();
@After
public void closeMockFileSystems() throws IOException {
IOUtils.close(fileSystems);
}
@Before
public void setupEnv() throws IOException {
setupEnv(true); // default to posix, but tests may call setupEnv(false) to overwrite
}
void setupEnv(boolean posix) throws IOException {
final Configuration configuration;
if (posix) {
configuration = Configuration.unix().toBuilder().setAttributeViews("basic", "owner", "posix", "unix").build();
} else {
configuration = Configuration.unix();
}
FileSystem fs = Jimfs.newFileSystem(configuration);
fileSystems.add(fs);
PathUtilsForTesting.installMock(fs); // restored by restoreFileSystem in ESTestCase
Path home = fs.getPath("/", "test-home");
Files.createDirectories(home.resolve("config"));
env = new Environment(Settings.builder().put("path.home", home).build());
}
KeyStoreWrapper createKeystore(String password, String... settings) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.create(password.toCharArray());
assertEquals(0, settings.length % 2);
for (int i = 0; i < settings.length; i += 2) {
keystore.setStringSetting(settings[i], settings[i + 1].toCharArray());
}
keystore.save(env.configFile());
return keystore;
}
KeyStoreWrapper loadKeystore(String password) throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.loadMetadata(env.configFile());
keystore.loadKeystore(password.toCharArray());
return keystore;
}
void assertSecureString(String setting, String value) throws Exception {
assertSecureString(loadKeystore(""), setting, value);
}
void assertSecureString(KeyStoreWrapper keystore, String setting, String value) throws Exception {
assertEquals(value, keystore.getStringSetting(setting).toString());
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.common.settings;
import java.util.Map;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
import static org.hamcrest.Matchers.containsString;
public class ListKeyStoreCommandTests extends KeyStoreCommandTestCase {
@Override
protected Command newCommand() {
return new ListKeyStoreCommand() {
@Override
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
return env;
}
};
}
public void testMissing() throws Exception {
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
assertThat(e.getMessage(), containsString("keystore not found"));
}
public void testEmpty() throws Exception {
createKeystore("");
execute();
assertTrue(terminal.getOutput(), terminal.getOutput().isEmpty());
}
public void testOne() throws Exception {
createKeystore("", "foo", "bar");
execute();
assertEquals("foo\n", terminal.getOutput());
}
public void testMultiple() throws Exception {
createKeystore("", "foo", "1", "baz", "2", "bar", "3");
execute();
assertEquals("bar\nbaz\nfoo\n", terminal.getOutput()); // sorted
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.common.settings;
import java.util.Map;
import java.util.Set;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
import static org.hamcrest.Matchers.containsString;
public class RemoveSettingKeyStoreCommandTests extends KeyStoreCommandTestCase {
@Override
protected Command newCommand() {
return new RemoveSettingKeyStoreCommand() {
@Override
protected Environment createEnv(Terminal terminal, Map<String, String> settings) {
return env;
}
};
}
public void testMissing() throws Exception {
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.DATA_ERROR, e.exitCode);
assertThat(e.getMessage(), containsString("keystore not found"));
}
public void testNoSettings() throws Exception {
createKeystore("");
UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("Must supply at least one setting"));
}
public void testNonExistentSetting() throws Exception {
createKeystore("");
UserException e = expectThrows(UserException.class, () -> execute("foo"));
assertEquals(ExitCodes.CONFIG, e.exitCode);
assertThat(e.getMessage(), containsString("[foo] does not exist"));
}
public void testOne() throws Exception {
createKeystore("", "foo", "bar");
execute("foo");
assertFalse(loadKeystore("").getSettings().contains("foo"));
}
public void testMany() throws Exception {
createKeystore("", "foo", "1", "bar", "2", "baz", "3");
execute("foo", "baz");
Set<String> settings = loadKeystore("").getSettings();
assertFalse(settings.contains("foo"));
assertFalse(settings.contains("baz"));
assertTrue(settings.contains("bar"));
assertEquals(1, settings.size());
}
}

View File

@ -0,0 +1,91 @@
#!/bin/bash
CDPATH=""
SCRIPT="$0"
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
# Drop everything prior to ->
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done
# determine elasticsearch home
ES_HOME=`dirname "$SCRIPT"`/..
# make ELASTICSEARCH_HOME absolute
ES_HOME=`cd "$ES_HOME"; pwd`
# Sets the default values for elasticsearch variables used in this script
if [ -z "$CONF_DIR" ]; then
CONF_DIR="${path.conf}"
fi
# The default env file is defined at building/packaging time.
# For a ${project.name} package, the value is "${path.env}".
ES_ENV_FILE="${path.env}"
# If an include is specified with the ES_INCLUDE environment variable, use it
if [ -n "$ES_INCLUDE" ]; then
ES_ENV_FILE="$ES_INCLUDE"
fi
# Source the environment file
if [ -n "$ES_ENV_FILE" ]; then
# If the ES_ENV_FILE is not found, try to resolve the path
# against the ES_HOME directory
if [ ! -f "$ES_ENV_FILE" ]; then
ES_ENV_FILE="$ELASTIC_HOME/$ES_ENV_FILE"
fi
. "$ES_ENV_FILE"
if [ $? -ne 0 ]; then
echo "Unable to source environment file: $ES_ENV_FILE" >&2
exit 1
fi
fi
# don't let JAVA_TOOL_OPTIONS slip in (e.g. crazy agents in ubuntu)
# works around https://bugs.launchpad.net/ubuntu/+source/jayatana/+bug/1441487
if [ "x$JAVA_TOOL_OPTIONS" != "x" ]; then
echo "Warning: Ignoring JAVA_TOOL_OPTIONS=$JAVA_TOOL_OPTIONS"
unset JAVA_TOOL_OPTIONS
fi
# CONF_FILE setting was removed
if [ ! -z "$CONF_FILE" ]; then
echo "CONF_FILE setting is no longer supported. elasticsearch.yml must be placed in the config directory and cannot be renamed."
exit 1
fi
if [ -x "$JAVA_HOME/bin/java" ]; then
JAVA=$JAVA_HOME/bin/java
else
JAVA=`which java`
fi
if [ ! -x "$JAVA" ]; then
echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME"
exit 1
fi
# full hostname passed through cut for portability on systems that do not support hostname -s
# export on separate line for shells that do not support combining definition and export
HOSTNAME=`hostname | cut -d. -f1`
export HOSTNAME
declare -a args=("$@")
path_props=(-Des.path.home="$ES_HOME")
if [ -e "$CONF_DIR" ]; then
path_props=("${path_props[@]}" -Des.path.conf="$CONF_DIR")
fi
exec "$JAVA" $ES_JAVA_OPTS -Delasticsearch "${path_props[@]}" -cp "$ES_HOME/lib/*" org.elasticsearch.common.settings.KeyStoreCli "${args[@]}"

View File

@ -0,0 +1,30 @@
@echo off
SETLOCAL enabledelayedexpansion
IF DEFINED JAVA_HOME (
set JAVA=%JAVA_HOME%\bin\java.exe
) ELSE (
FOR %%I IN (java.exe) DO set JAVA=%%~$PATH:I
)
IF NOT EXIST "%JAVA%" (
ECHO Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME 1>&2
EXIT /B 1
)
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set ES_HOME=%%~dpfI
TITLE Elasticsearch Plugin Manager ${project.version}
SET path_props=-Des.path.home="%ES_HOME%"
IF DEFINED CONF_DIR (
SET path_props=!path_props! -Des.path.conf="%CONF_DIR%"
)
SET args=%*
SET HOSTNAME=%COMPUTERNAME%
"%JAVA%" %ES_JAVA_OPTS% !path_props! -cp "%ES_HOME%/lib/*;" "org.elasticsearch.common.settings.KeyStoreCli" !args!
ENDLOCAL

View File

@ -93,9 +93,9 @@ https://hc.apache.org/httpcomponents-asyncclient-dev/httpasyncclient/apidocs/org
[source,java]
--------------------------------------------------
KeyStore keyStore = KeyStore.getInstance("jks");
KeyStore keystore = KeyStore.getInstance("jks");
try (InputStream is = Files.newInputStream(keyStorePath)) {
keyStore.load(is, keyStorePass.toCharArray());
keystore.load(is, keyStorePass.toCharArray());
}
RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {

View File

@ -41,8 +41,7 @@ public class EvilElasticsearchCliTests extends ESElasticsearchCliTestCase {
true,
output -> {},
(foreground, pidFile, quiet, esSettings) -> {
Map<String, String> settings = esSettings.getAsMap();
settings.keySet().forEach(System.out::println);
Map<String, String> settings = esSettings.settings().getAsMap();
assertThat(settings.size(), equalTo(2));
assertThat(settings, hasEntry("path.home", value));
assertThat(settings, hasKey("path.logs")); // added by env initialization
@ -55,7 +54,7 @@ public class EvilElasticsearchCliTests extends ESElasticsearchCliTestCase {
true,
output -> {},
(foreground, pidFile, quiet, esSettings) -> {
Map<String, String> settings = esSettings.getAsMap();
Map<String, String> settings = esSettings.settings().getAsMap();
assertThat(settings.size(), equalTo(2));
assertThat(settings, hasEntry("path.home", commandLineValue));
assertThat(settings, hasKey("path.logs")); // added by env initialization

View File

@ -35,7 +35,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
abstract class ESElasticsearchCliTestCase extends ESTestCase {
interface InitConsumer {
void accept(final boolean foreground, final Path pidFile, final boolean quiet, final Settings initialSettings);
void accept(final boolean foreground, final Path pidFile, final boolean quiet, final Environment env0);
}
void runTest(
@ -57,9 +57,9 @@ abstract class ESElasticsearchCliTestCase extends ESTestCase {
return new Environment(realSettings);
}
@Override
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings) {
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment env0) {
init.set(true);
initConsumer.accept(!daemonize, pidFile, quiet, initialSettings);
initConsumer.accept(!daemonize, pidFile, quiet, env0);
}
@Override