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

Introduce fips_mode setting and associated checks #32326

Merged
merged 3 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ private KeyStoreWrapper(int formatVersion, boolean hasPassword, byte[] dataBytes
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);
Expand Down Expand Up @@ -593,8 +600,10 @@ private void ensureOpen() {
@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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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"),
Expand Down Expand Up @@ -294,6 +296,9 @@ public Security(Settings settings, final Path configPath) {
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);
Expand Down Expand Up @@ -578,6 +583,7 @@ public static List<Setting<?>> getSettings(boolean transportClientMode, List<Sec
}

// The following just apply in node mode
settingsList.add(FIPS_MODE_ENABLED);

// IP Filter settings
IPFilter.addSettings(settingsList);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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);
}
}
}