Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Aug 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions core/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/java/org/elasticsearch/common/Randomness.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -69,6 +72,12 @@
*/
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,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}