security: add setting that makes system key required
This commit adds a setting that makes the system key required. If this setting is set to true, a node will fail to startup when the system key does not exist. Closes elastic/elasticsearch#3957 Original commit: elastic/x-pack-elasticsearch@e6d3000974
This commit is contained in:
parent
1de85f4740
commit
714b891b03
|
@ -13,6 +13,7 @@ import javax.crypto.Mac;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -32,6 +33,7 @@ import org.elasticsearch.ElasticsearchException;
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.Nullable;
|
||||||
import org.elasticsearch.common.component.AbstractComponent;
|
import org.elasticsearch.common.component.AbstractComponent;
|
||||||
import org.elasticsearch.common.settings.Setting;
|
import org.elasticsearch.common.settings.Setting;
|
||||||
|
import org.elasticsearch.common.settings.Setting.Property;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.env.Environment;
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.xpack.XPackPlugin;
|
import org.elasticsearch.xpack.XPackPlugin;
|
||||||
|
@ -59,12 +61,14 @@ public class CryptoService extends AbstractComponent {
|
||||||
private static final Pattern SIG_PATTERN = Pattern.compile("^\\$\\$[0-9]+\\$\\$[^\\$]*\\$\\$.+");
|
private static final Pattern SIG_PATTERN = Pattern.compile("^\\$\\$[0-9]+\\$\\$[^\\$]*\\$\\$.+");
|
||||||
private static final byte[] HKDF_APP_INFO = "es-security-crypto-service".getBytes(StandardCharsets.UTF_8);
|
private static final byte[] HKDF_APP_INFO = "es-security-crypto-service".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
public static final Setting<String> ENCRYPTION_ALGO_SETTING =
|
private static final Setting<Boolean> SYSTEM_KEY_REQUIRED_SETTING =
|
||||||
new Setting<>(setting("encryption.algorithm"), s -> DEFAULT_ENCRYPTION_ALGORITHM, s -> s, Setting.Property.NodeScope);
|
Setting.boolSetting(setting("system_key.required"), false, Property.NodeScope);
|
||||||
public static final Setting<Integer> ENCRYPTION_KEY_LENGTH_SETTING =
|
private static final Setting<String> ENCRYPTION_ALGO_SETTING =
|
||||||
Setting.intSetting(setting("encryption_key.length"), DEFAULT_KEY_LENGTH, Setting.Property.NodeScope);
|
new Setting<>(setting("encryption.algorithm"), s -> DEFAULT_ENCRYPTION_ALGORITHM, s -> s, Property.NodeScope);
|
||||||
public static final Setting<String> ENCRYPTION_KEY_ALGO_SETTING =
|
private static final Setting<Integer> ENCRYPTION_KEY_LENGTH_SETTING =
|
||||||
new Setting<>(setting("encryption_key.algorithm"), DEFAULT_KEY_ALGORITH, s -> s, Setting.Property.NodeScope);
|
Setting.intSetting(setting("encryption_key.length"), DEFAULT_KEY_LENGTH, Property.NodeScope);
|
||||||
|
private static final Setting<String> ENCRYPTION_KEY_ALGO_SETTING =
|
||||||
|
new Setting<>(setting("encryption_key.algorithm"), DEFAULT_KEY_ALGORITH, s -> s, Property.NodeScope);
|
||||||
|
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
private final String encryptionAlgorithm;
|
private final String encryptionAlgorithm;
|
||||||
|
@ -93,7 +97,7 @@ public class CryptoService extends AbstractComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFile = resolveSystemKey(env);
|
keyFile = resolveSystemKey(env);
|
||||||
systemKey = readSystemKey(keyFile);
|
systemKey = readSystemKey(keyFile, SYSTEM_KEY_REQUIRED_SETTING.get(settings));
|
||||||
randomKey = generateSecretKey(RANDOM_KEY_SIZE);
|
randomKey = generateSecretKey(RANDOM_KEY_SIZE);
|
||||||
randomKeyBase64 = Base64.getUrlEncoder().encodeToString(randomKey.getEncoded());
|
randomKeyBase64 = Base64.getUrlEncoder().encodeToString(randomKey.getEncoded());
|
||||||
|
|
||||||
|
@ -139,16 +143,17 @@ public class CryptoService extends AbstractComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecretKey readSystemKey(Path file) {
|
private static SecretKey readSystemKey(Path file, boolean required) throws IOException {
|
||||||
if (!Files.exists(file)) {
|
if (Files.exists(file)) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
byte[] bytes = Files.readAllBytes(file);
|
byte[] bytes = Files.readAllBytes(file);
|
||||||
return new SecretKeySpec(bytes, KEY_ALGO);
|
return new SecretKeySpec(bytes, KEY_ALGO);
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ElasticsearchException("could not read secret key", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (required) {
|
||||||
|
throw new FileNotFoundException("[" + file + "] must be present with a valid key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -492,5 +497,6 @@ public class CryptoService extends AbstractComponent {
|
||||||
settings.add(ENCRYPTION_KEY_LENGTH_SETTING);
|
settings.add(ENCRYPTION_KEY_LENGTH_SETTING);
|
||||||
settings.add(ENCRYPTION_KEY_ALGO_SETTING);
|
settings.add(ENCRYPTION_KEY_ALGO_SETTING);
|
||||||
settings.add(ENCRYPTION_ALGO_SETTING);
|
settings.add(ENCRYPTION_ALGO_SETTING);
|
||||||
|
settings.add(SYSTEM_KEY_REQUIRED_SETTING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.crypto;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -17,6 +18,7 @@ import org.elasticsearch.test.ESTestCase;
|
||||||
import org.elasticsearch.xpack.XPackPlugin;
|
import org.elasticsearch.xpack.XPackPlugin;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
|
@ -37,6 +39,7 @@ public class CryptoServiceTests extends ESTestCase {
|
||||||
Files.write(keyFile, CryptoService.generateKey());
|
Files.write(keyFile, CryptoService.generateKey());
|
||||||
settings = Settings.builder()
|
settings = Settings.builder()
|
||||||
.put("resource.reload.interval.high", "2s")
|
.put("resource.reload.interval.high", "2s")
|
||||||
|
.put("xpack.security.system_key.required", randomBoolean())
|
||||||
.put("path.home", home)
|
.put("path.home", home)
|
||||||
.build();
|
.build();
|
||||||
env = new Environment(settings);
|
env = new Environment(settings);
|
||||||
|
@ -148,7 +151,7 @@ public class CryptoServiceTests extends ESTestCase {
|
||||||
public void testEncryptionAndDecryptionCharsWithoutKey() throws Exception {
|
public void testEncryptionAndDecryptionCharsWithoutKey() throws Exception {
|
||||||
Files.delete(keyFile);
|
Files.delete(keyFile);
|
||||||
CryptoService service = new CryptoService(Settings.EMPTY, env);
|
CryptoService service = new CryptoService(Settings.EMPTY, env);
|
||||||
assertThat(service.isEncryptionEnabled(), is(false));
|
assertThat(service.isEncryptionEnabled(), is(false));
|
||||||
final char[] chars = randomAsciiOfLengthBetween(0, 1000).toCharArray();
|
final char[] chars = randomAsciiOfLengthBetween(0, 1000).toCharArray();
|
||||||
final char[] encryptedChars = service.encrypt(chars);
|
final char[] encryptedChars = service.encrypt(chars);
|
||||||
final char[] decryptedChars = service.decrypt(encryptedChars);
|
final char[] decryptedChars = service.decrypt(encryptedChars);
|
||||||
|
@ -158,18 +161,18 @@ public class CryptoServiceTests extends ESTestCase {
|
||||||
|
|
||||||
public void testEncryptionEnabledWithKey() throws Exception {
|
public void testEncryptionEnabledWithKey() throws Exception {
|
||||||
CryptoService service = new CryptoService(settings, env);
|
CryptoService service = new CryptoService(settings, env);
|
||||||
assertThat(service.isEncryptionEnabled(), is(true));
|
assertThat(service.isEncryptionEnabled(), is(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEncryptionEnabledWithoutKey() throws Exception {
|
public void testEncryptionEnabledWithoutKey() throws Exception {
|
||||||
Files.delete(keyFile);
|
Files.delete(keyFile);
|
||||||
CryptoService service = new CryptoService(Settings.EMPTY, env);
|
CryptoService service = new CryptoService(Settings.EMPTY, env);
|
||||||
assertThat(service.isEncryptionEnabled(), is(false));
|
assertThat(service.isEncryptionEnabled(), is(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEncryptedChar() throws Exception {
|
public void testEncryptedChar() throws Exception {
|
||||||
CryptoService service = new CryptoService(settings, env);
|
CryptoService service = new CryptoService(settings, env);
|
||||||
assertThat(service.isEncryptionEnabled(), is(true));
|
assertThat(service.isEncryptionEnabled(), is(true));
|
||||||
|
|
||||||
assertThat(service.isEncrypted((char[]) null), is(false));
|
assertThat(service.isEncrypted((char[]) null), is(false));
|
||||||
assertThat(service.isEncrypted(new char[0]), is(false));
|
assertThat(service.isEncrypted(new char[0]), is(false));
|
||||||
|
@ -189,4 +192,20 @@ public class CryptoServiceTests extends ESTestCase {
|
||||||
assertThat(regenerated, equalTo(signingKey));
|
assertThat(regenerated, equalTo(signingKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testSystemKeyFileRequired() throws Exception {
|
||||||
|
Files.delete(keyFile);
|
||||||
|
Settings customSettings = Settings.builder().put(settings).put("xpack.security.system_key.required", true).build();
|
||||||
|
FileNotFoundException fnfe = expectThrows(FileNotFoundException.class, () -> new CryptoService(customSettings, env));
|
||||||
|
assertThat(fnfe.getMessage(), containsString("must be present with a valid key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEmptySystemKeyFile() throws Exception {
|
||||||
|
// delete and create empty file
|
||||||
|
Files.delete(keyFile);
|
||||||
|
Files.createFile(keyFile);
|
||||||
|
assertTrue(Files.exists(keyFile));
|
||||||
|
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> new CryptoService(settings, env));
|
||||||
|
assertThat(iae.getMessage(), containsString("Empty key"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue