diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index d6732f53ea3bf..1b18e2a4160a0 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -227,12 +227,14 @@ private static SecureSettings loadSecureSettings(Environment initialEnv) throws } 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); } diff --git a/core/src/main/java/org/elasticsearch/common/Randomness.java b/core/src/main/java/org/elasticsearch/common/Randomness.java index 05ebe1a7377f9..278cac58ed12c 100644 --- a/core/src/main/java/org/elasticsearch/common/Randomness.java +++ b/core/src/main/java/org/elasticsearch/common/Randomness.java @@ -23,6 +23,7 @@ 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 static Random get() { } } + /** + * 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"; diff --git a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 8a10f55ea478c..d8cb231523dca 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -391,6 +391,7 @@ public void apply(Settings value, Settings current, Settings previous) { 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, diff --git a/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java b/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java index 338987cc714e2..f36fb8ec9bd81 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java +++ b/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java @@ -39,6 +39,7 @@ 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.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 @@ */ public class KeyStoreWrapper implements SecureSettings { + public static final Setting 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 @@ static Path keystorePath(Path configDir) { } /** 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 void decrypt(char[] password) throws GeneralSecurityException, IOExceptio } /** 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); diff --git a/core/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java b/core/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java index 0b42eb59f827f..0c9cdad618a19 100644 --- a/core/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java +++ b/core/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java @@ -26,6 +26,7 @@ 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 void testFileSettingExhaustiveBytes() throws Exception { 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())); + } } diff --git a/core/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java b/core/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java index 0cfc7bd537d1c..406821dafdc34 100644 --- a/core/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java +++ b/core/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java @@ -50,18 +50,18 @@ public void testMissing() throws Exception { 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 } } diff --git a/core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java b/core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java index 22da97bc1a77f..3ad48127b4b8a 100644 --- a/core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java +++ b/core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java @@ -75,6 +75,6 @@ public void testMany() throws Exception { 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 } }