Browse Source

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.
Ryan Ernst 8 years ago
parent
commit
b2d6ff9116

+ 6 - 4
core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

@@ -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);
         }

+ 17 - 0
core/src/main/java/org/elasticsearch/common/Randomness.java

@@ -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";

+ 1 - 0
core/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java

@@ -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,

+ 24 - 2
core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

@@ -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);

+ 6 - 0
core/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java

@@ -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()));
+    }
 }

+ 3 - 3
core/src/test/java/org/elasticsearch/common/settings/ListKeyStoreCommandTests.java

@@ -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
     }
 }

+ 1 - 1
core/src/test/java/org/elasticsearch/common/settings/RemoveSettingKeyStoreCommandTests.java

@@ -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
     }
 }