From be40a6982a9a90879fe198260df6a4cb3c0e8582 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 24 Jul 2018 22:05:00 +0300 Subject: [PATCH] Introduce fips_mode setting and associated checks (#32326) * Introduce fips_mode setting and associated checks Introduce xpack.security.fips_mode.enabled setting ( default false) When it is set to true, a number of Bootstrap checks are performed: - Check that Secure Settings are of the latest version (3) - Check that no JKS keystores are configured - Check that compliant algorithms ( PBKDF2 family ) are used for password hashing --- .../common/settings/KeyStoreWrapper.java | 13 ++- .../FIPS140JKSKeystoreBootstrapCheck.java | 54 ++++++++++ ...asswordHashingAlgorithmBootstrapCheck.java | 43 ++++++++ .../FIPS140SecureSettingsBootstrapCheck.java | 52 +++++++++ .../xpack/security/Security.java | 6 ++ ...FIPS140JKSKeystoreBootstrapCheckTests.java | 66 ++++++++++++ ...rdHashingAlgorithmBootstrapCheckTests.java | 34 ++++++ ...S140SecureSettingsBootstrapCheckTests.java | 102 ++++++++++++++++++ 8 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheck.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheckTests.java diff --git a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java index 3a8a06949b2..eee45743ee3 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java +++ b/server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java @@ -166,6 +166,13 @@ public class KeyStoreWrapper implements SecureSettings { this.dataBytes = dataBytes; } + /** + * Get the metadata format version for the keystore + **/ + public int getFormatVersion() { + return formatVersion; + } + /** Returns a path representing the ES keystore in the given config dir. */ public static Path keystorePath(Path configDir) { return configDir.resolve(KEYSTORE_FILENAME); @@ -593,8 +600,10 @@ public class KeyStoreWrapper implements SecureSettings { @Override public synchronized void close() { this.closed = true; - for (Entry entry : entries.get().values()) { - Arrays.fill(entry.bytes, (byte)0); + if (null != entries.get() && entries.get().isEmpty() == false) { + for (Entry entry : entries.get().values()) { + Arrays.fill(entry.bytes, (byte) 0); + } } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java new file mode 100644 index 00000000000..4a2c7b97195 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.Settings; + + +public class FIPS140JKSKeystoreBootstrapCheck implements BootstrapCheck { + + private final boolean fipsModeEnabled; + + FIPS140JKSKeystoreBootstrapCheck(Settings settings) { + this.fipsModeEnabled = Security.FIPS_MODE_ENABLED.get(settings); + } + + /** + * Test if the node fails the check. + * + * @param context the bootstrap context + * @return the result of the bootstrap check + */ + @Override + public BootstrapCheckResult check(BootstrapContext context) { + + if (fipsModeEnabled) { + final Settings settings = context.settings; + Settings keystoreTypeSettings = settings.filter(k -> k.endsWith("keystore.type")) + .filter(k -> settings.get(k).equalsIgnoreCase("jks")); + if (keystoreTypeSettings.isEmpty() == false) { + return BootstrapCheckResult.failure("JKS Keystores cannot be used in a FIPS 140 compliant JVM. Please " + + "revisit [" + keystoreTypeSettings.toDelimitedString(',') + "] settings"); + } + // Default Keystore type is JKS if not explicitly set + Settings keystorePathSettings = settings.filter(k -> k.endsWith("keystore.path")) + .filter(k -> settings.hasValue(k.replace(".path", ".type")) == false); + if (keystorePathSettings.isEmpty() == false) { + return BootstrapCheckResult.failure("JKS Keystores cannot be used in a FIPS 140 compliant JVM. Please " + + "revisit [" + keystorePathSettings.toDelimitedString(',') + "] settings"); + } + + } + return BootstrapCheckResult.success(); + } + + @Override + public boolean alwaysEnforce() { + return fipsModeEnabled; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java new file mode 100644 index 00000000000..7f6d799cf5a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.XPackSettings; + +import java.util.Locale; + +public class FIPS140PasswordHashingAlgorithmBootstrapCheck implements BootstrapCheck { + + private final boolean fipsModeEnabled; + + FIPS140PasswordHashingAlgorithmBootstrapCheck(Settings settings) { + this.fipsModeEnabled = Security.FIPS_MODE_ENABLED.get(settings); + } + + /** + * Test if the node fails the check. + * + * @param context the bootstrap context + * @return the result of the bootstrap check + */ + @Override + public BootstrapCheckResult check(BootstrapContext context) { + final String selectedAlgorithm = XPackSettings.PASSWORD_HASHING_ALGORITHM.get(context.settings); + if (selectedAlgorithm.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) { + return BootstrapCheckResult.failure("Only PBKDF2 is allowed for password hashing in a FIPS-140 JVM. Please set the " + + "appropriate value for [ " + XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey() + " ] setting."); + } + return BootstrapCheckResult.success(); + } + + @Override + public boolean alwaysEnforce() { + return fipsModeEnabled; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheck.java new file mode 100644 index 00000000000..c766dd0ffaa --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheck.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.elasticsearch.bootstrap.BootstrapCheck; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; + +import java.io.IOException; +import java.io.UncheckedIOException; + +public class FIPS140SecureSettingsBootstrapCheck implements BootstrapCheck { + + private final boolean fipsModeEnabled; + private final Environment environment; + + FIPS140SecureSettingsBootstrapCheck(Settings settings, Environment environment) { + this.fipsModeEnabled = Security.FIPS_MODE_ENABLED.get(settings); + this.environment = environment; + } + + /** + * Test if the node fails the check. + * + * @param context the bootstrap context + * @return the result of the bootstrap check + */ + @Override + public BootstrapCheckResult check(BootstrapContext context) { + if (fipsModeEnabled) { + try (KeyStoreWrapper secureSettings = KeyStoreWrapper.load(environment.configFile())) { + if (secureSettings != null && secureSettings.getFormatVersion() < 3) { + return BootstrapCheckResult.failure("Secure settings store is not of the latest version. Please use " + + "bin/elasticsearch-keystore create to generate a new secure settings store and migrate the secure settings there."); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return BootstrapCheckResult.success(); + } + + @Override + public boolean alwaysEnforce() { + return fipsModeEnabled; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 92784159700..b9cdd22ba47 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -256,6 +256,8 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw DiscoveryPlugin, MapperPlugin, ExtensiblePlugin { private static final Logger logger = Loggers.getLogger(Security.class); + static final Setting FIPS_MODE_ENABLED = + Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope); static final Setting> AUDIT_OUTPUTS_SETTING = Setting.listSetting(SecurityField.setting("audit.outputs"), @@ -305,6 +307,9 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw new PkiRealmBootstrapCheck(getSslService()), new TLSLicenseBootstrapCheck(), new PasswordHashingAlgorithmBootstrapCheck(), + new FIPS140SecureSettingsBootstrapCheck(settings, env), + new FIPS140JKSKeystoreBootstrapCheck(settings), + new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings), new KerberosRealmBootstrapCheck(env))); checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); this.bootstrapChecks = Collections.unmodifiableList(checks); @@ -592,6 +597,7 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw } // The following just apply in node mode + settingsList.add(FIPS_MODE_ENABLED); // IP Filter settings IPFilter.addSettings(settingsList); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java new file mode 100644 index 00000000000..1d4da71e11b --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +public class FIPS140JKSKeystoreBootstrapCheckTests extends ESTestCase { + + public void testNoKeystoreIsAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true"); + assertFalse(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testSSLKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.ssl.keystore.path", "/this/is/the/path") + .put("xpack.ssl.keystore.type", "JKS"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testSSLImplicitKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.ssl.keystore.path", "/this/is/the/path") + .put("xpack.ssl.keystore.type", "JKS"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testTransportSSLKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.security.transport.ssl.keystore.path", "/this/is/the/path") + .put("xpack.security.transport.ssl.keystore.type", "JKS"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testHttpSSLKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.security.http.ssl.keystore.path", "/this/is/the/path") + .put("xpack.security.http.ssl.keystore.type", "JKS"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testRealmKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.security.authc.realms.ldap.ssl.keystore.path", "/this/is/the/path") + .put("xpack.security.authc.realms.ldap.ssl.keystore.type", "JKS"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } + + public void testImplicitRealmKeystoreTypeIsNotAllowed() { + final Settings.Builder settings = Settings.builder() + .put("xpack.security.fips_mode.enabled", "true") + .put("xpack.security.authc.realms.ldap.ssl.keystore.path", "/this/is/the/path"); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java new file mode 100644 index 00000000000..310a6e241e0 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.XPackSettings; + +public class FIPS140PasswordHashingAlgorithmBootstrapCheckTests extends ESTestCase { + + public void testPBKDF2AlgorithmIsAllowed() { + Settings settings = Settings.builder().put("xpack.security.fips_mode.enabled", "true").build(); + + settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2_10000").build(); + assertFalse(new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)).isFailure()); + + settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2").build(); + assertFalse(new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)).isFailure()); + } + + public void testBCRYPTAlgorithmIsNotAllowed() { + Settings settings = Settings.builder().put("xpack.security.fips_mode.enabled", "true").build(); + assertTrue(new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)).isFailure()); + settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "BCRYPT").build(); + assertTrue(new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)).isFailure()); + + settings = Settings.builder().put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "BCRYPT11").build(); + assertTrue(new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)).isFailure()); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheckTests.java new file mode 100644 index 00000000000..fb9e7155242 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140SecureSettingsBootstrapCheckTests.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security; + +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.SimpleFSDirectory; +import org.elasticsearch.bootstrap.BootstrapContext; +import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.io.ByteArrayOutputStream; +import java.nio.file.Path; +import java.security.AccessControlException; +import java.security.KeyStore; +import java.util.Base64; + +public class FIPS140SecureSettingsBootstrapCheckTests extends ESTestCase { + + public void testLegacySecureSettingsIsNotAllowed() throws Exception { + assumeFalse("Can't run in a FIPS JVM, PBE is not available", inFipsJvm()); + final Settings.Builder builder = Settings.builder() + .put("path.home", createTempDir()) + .put("xpack.security.fips_mode.enabled", "true"); + Environment env = TestEnvironment.newEnvironment(builder.build()); + generateV2Keystore(env); + assertTrue(new FIPS140SecureSettingsBootstrapCheck(builder.build(), env).check(new BootstrapContext(builder.build(), + null)).isFailure()); + } + + public void testCorrectSecureSettingsVersionIsAllowed() throws Exception { + final Settings.Builder builder = Settings.builder() + .put("path.home", createTempDir()) + .put("xpack.security.fips_mode.enabled", "true"); + Environment env = TestEnvironment.newEnvironment(builder.build()); + final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); + try { + keyStoreWrapper.save(env.configFile(), "password".toCharArray()); + } catch (final AccessControlException e) { + if (e.getPermission() instanceof RuntimePermission && e.getPermission().getName().equals("accessUserInformation")) { + // this is expected:but we don't care in tests + } else { + throw e; + } + } + assertFalse(new FIPS140SecureSettingsBootstrapCheck(builder.build(), env).check(new BootstrapContext(builder.build(), + null)).isFailure()); + } + + private void generateV2Keystore(Environment env) throws Exception { + Path configDir = env.configFile(); + SimpleFSDirectory directory = new SimpleFSDirectory(configDir); + byte[] fileBytes = new byte[20]; + random().nextBytes(fileBytes); + try (IndexOutput output = directory.createOutput("elasticsearch.keystore", IOContext.DEFAULT)) { + + CodecUtil.writeHeader(output, "elasticsearch.keystore", 2); + output.writeByte((byte) 0); // hasPassword = false + output.writeString("PKCS12"); + output.writeString("PBE"); // string algo + output.writeString("PBE"); // file algo + + output.writeVInt(2); // num settings + output.writeString("string_setting"); + output.writeString("STRING"); + output.writeString("file_setting"); + output.writeString("FILE"); + + SecretKeyFactory secretFactory = SecretKeyFactory.getInstance("PBE"); + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(null, null); + SecretKey secretKey = secretFactory.generateSecret(new PBEKeySpec("stringSecretValue".toCharArray())); + KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(new char[0]); + keystore.setEntry("string_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter); + + byte[] base64Bytes = Base64.getEncoder().encode(fileBytes); + char[] chars = new char[base64Bytes.length]; + for (int i = 0; i < chars.length; ++i) { + chars[i] = (char) base64Bytes[i]; // PBE only stores the lower 8 bits, so this narrowing is ok + } + secretKey = secretFactory.generateSecret(new PBEKeySpec(chars)); + keystore.setEntry("file_setting", new KeyStore.SecretKeyEntry(secretKey), protectionParameter); + + ByteArrayOutputStream keystoreBytesStream = new ByteArrayOutputStream(); + keystore.store(keystoreBytesStream, new char[0]); + byte[] keystoreBytes = keystoreBytesStream.toByteArray(); + output.writeInt(keystoreBytes.length); + output.writeBytes(keystoreBytes, keystoreBytes.length); + CodecUtil.writeFooter(output); + } + } +}