Settings: Add keystore.seed auto generated secure setting (#26149)

This commit adds a keystore.seed setting that is automatically
generated when the ES keystore is created. This setting may be used by
plugins as a secure, random value. This commit also auto creates the
keystore upon startup to ensure the new setting is always available.
This commit is contained in:
Ryan Ernst 2017-08-15 14:04:03 -07:00 committed by GitHub
parent dfe1bc6883
commit b2d6ff9116
7 changed files with 58 additions and 10 deletions

View File

@ -227,12 +227,14 @@ final class Bootstrap {
} catch (IOException e) {
throw new BootstrapException(e);
}
if (keystore == null) {
return null; // no keystore
}
try {
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
if (keystore == null) {
// create it, we always want one! we use an empty passphrase, but a user can change this later if they want.
KeyStoreWrapper.create(new char[0]);
} else {
keystore.decrypt(new char[0] /* TODO: read password from stdin */);
}
} catch (Exception e) {
throw new BootstrapException(e);
}

View File

@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import java.lang.reflect.Method;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.List;
import java.util.Random;
@ -109,6 +110,22 @@ public final class Randomness {
}
}
/**
* Provides a secure source of randomness.
*
* This acts exactly similar to {@link #get()}, but returning a new {@link SecureRandom}.
*/
public static SecureRandom createSecure() {
if (currentMethod != null && getRandomMethod != null) {
// tests, so just use a seed from the non secure random
byte[] seed = new byte[16];
get().nextBytes(seed);
return new SecureRandom(seed);
} else {
return new SecureRandom();
}
}
@SuppressForbidden(reason = "ThreadLocalRandom is okay when not running tests")
private static Random getWithoutSeed() {
assert currentMethod == null && getRandomMethod == null : "running under tests but tried to create non-reproducible random";

View File

@ -391,6 +391,7 @@ public final class ClusterSettings extends AbstractScopedSettings {
BootstrapSettings.MEMORY_LOCK_SETTING,
BootstrapSettings.SYSTEM_CALL_FILTER_SETTING,
BootstrapSettings.CTRLHANDLER_SETTING,
KeyStoreWrapper.SEED_SETTING,
IndexingMemoryController.INDEX_BUFFER_SIZE_SETTING,
IndexingMemoryController.MIN_INDEX_BUFFER_SIZE_SETTING,
IndexingMemoryController.MAX_INDEX_BUFFER_SIZE_SETTING,

View File

@ -39,6 +39,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Enumeration;
@ -57,6 +58,8 @@ import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.bootstrap.BootstrapSettings;
import org.elasticsearch.common.Randomness;
/**
* A wrapper around a Java KeyStore which provides supplements the keystore with extra metadata.
@ -69,6 +72,12 @@ import org.apache.lucene.util.SetOnce;
*/
public class KeyStoreWrapper implements SecureSettings {
public static final Setting<SecureString> SEED_SETTING = SecureSetting.secureString("keystore.seed", null);
/** Characters that may be used in the bootstrap seed setting added to all keystores. */
private static final char[] SEED_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +
"~!@#$%^&*-_=+?").toCharArray();
/** An identifier for the type of data that may be stored in a keystore entry. */
private enum KeyType {
STRING,
@ -147,16 +156,29 @@ public class KeyStoreWrapper implements SecureSettings {
}
/** Constructs a new keystore with the given password. */
static KeyStoreWrapper create(char[] password) throws Exception {
public static KeyStoreWrapper create(char[] password) throws Exception {
KeyStoreWrapper wrapper = new KeyStoreWrapper(FORMAT_VERSION, password.length != 0, NEW_KEYSTORE_TYPE,
NEW_KEYSTORE_STRING_KEY_ALGO, NEW_KEYSTORE_FILE_KEY_ALGO, new HashMap<>(), null);
KeyStore keyStore = KeyStore.getInstance(NEW_KEYSTORE_TYPE);
keyStore.load(null, null);
wrapper.keystore.set(keyStore);
wrapper.keystorePassword.set(new KeyStore.PasswordProtection(password));
addBootstrapSeed(wrapper);
return wrapper;
}
/** Add the bootstrap seed setting, which may be used as a unique, secure, random value by the node */
private static void addBootstrapSeed(KeyStoreWrapper wrapper) throws GeneralSecurityException {
SecureRandom random = Randomness.createSecure();
int passwordLength = 20; // Generate 20 character passwords
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = SEED_CHARS[random.nextInt(SEED_CHARS.length)];
}
wrapper.setString(SEED_SETTING.getKey(), characters);
Arrays.fill(characters, (char)0);
}
/**
* Loads information about the Elasticsearch keystore from the provided config directory.
*
@ -253,7 +275,7 @@ public class KeyStoreWrapper implements SecureSettings {
}
/** Write the keystore to the given config directory. */
void save(Path configDir) throws Exception {
public void save(Path configDir) throws Exception {
char[] password = this.keystorePassword.get().getPassword();
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.bootstrap.BootstrapSettings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
@ -67,4 +68,9 @@ public class KeyStoreWrapperTests extends ESTestCase {
assertEquals(-1, stream.read()); // nothing left
}
}
public void testKeystoreSeed() throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.create(new char[0]);
assertTrue(keystore.getSettingNames().contains(KeyStoreWrapper.SEED_SETTING.getKey()));
}
}

View File

@ -50,18 +50,18 @@ public class ListKeyStoreCommandTests extends KeyStoreCommandTestCase {
public void testEmpty() throws Exception {
createKeystore("");
execute();
assertTrue(terminal.getOutput(), terminal.getOutput().isEmpty());
assertEquals("keystore.seed\n", terminal.getOutput());
}
public void testOne() throws Exception {
createKeystore("", "foo", "bar");
execute();
assertEquals("foo\n", terminal.getOutput());
assertEquals("foo\nkeystore.seed\n", terminal.getOutput());
}
public void testMultiple() throws Exception {
createKeystore("", "foo", "1", "baz", "2", "bar", "3");
execute();
assertEquals("bar\nbaz\nfoo\n", terminal.getOutput()); // sorted
assertEquals("bar\nbaz\nfoo\nkeystore.seed\n", terminal.getOutput()); // sorted
}
}

View File

@ -75,6 +75,6 @@ public class RemoveSettingKeyStoreCommandTests extends KeyStoreCommandTestCase {
assertFalse(settings.contains("foo"));
assertFalse(settings.contains("baz"));
assertTrue(settings.contains("bar"));
assertEquals(1, settings.size());
assertEquals(2, settings.size()); // account for keystore.seed too
}
}