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
This commit is contained in:
parent
e0b7e4b1c5
commit
be40a6982a
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Boolean> FIPS_MODE_ENABLED =
|
||||
Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope);
|
||||
|
||||
static final Setting<List<String>> 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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue