Add bootstrap check that enforces TLS if a production license is in the local clusterstate (elastic/x-pack-elasticsearch#2499)

This change will enforce transport SSL to be enforced if security is enabled and the
license in the clusterstate is a production license. The cluster state is loaded from
local storage such that we don't need to join a cluster to make these checks. Yet, the cluster
might have already got a different license if the node got disconnected while the license got
downgraded and then TLS got disabled. This corner case requires manual intervention which
we consider ok given the simplicity of this change.

Relates to elastic/x-pack-elasticsearch#2463

Original commit: elastic/x-pack-elasticsearch@5765b7cd21
This commit is contained in:
Simon Willnauer 2017-09-14 13:52:53 +02:00 committed by GitHub
parent 3b00251a96
commit 91b57ee63f
8 changed files with 134 additions and 49 deletions

View File

@ -782,4 +782,23 @@ public class License implements ToXContentObject {
}
}
}
/**
* Returns <code>true</code> iff the license is a production licnese
*/
public boolean isProductionLicense() {
switch (operationMode()) {
case MISSING:
case TRIAL:
case BASIC:
return false;
case STANDARD:
case GOLD:
case PLATINUM:
return true;
default:
throw new AssertionError("unknown operation mode: " + operationMode());
}
}
}

View File

@ -271,7 +271,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
}
public License getLicense() {
final License license = getLicense(clusterService.state());
final License license = getLicense(clusterService.state().metaData());
return license == LicensesMetaData.LICENSE_TOMBSTONE ? null : license;
}
@ -469,8 +469,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
};
}
public static License getLicense(final ClusterState state) {
final LicensesMetaData licensesMetaData = state.metaData().custom(LicensesMetaData.TYPE);
public static License getLicense(final MetaData metaData) {
final LicensesMetaData licensesMetaData = metaData.custom(LicensesMetaData.TYPE);
return getLicense(licensesMetaData);
}

View File

@ -5,7 +5,6 @@
*/
package org.elasticsearch.xpack;
import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.network.NetworkModule;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
@ -56,20 +55,9 @@ public class XPackSettings {
public static final Setting<Boolean> LOGSTASH_ENABLED = Setting.boolSetting("xpack.logstash.enabled", true,
Setting.Property.NodeScope);
/**
* Legacy setting for enabling or disabling transport ssl. Defaults to true. This is just here to make upgrading easier since the
* user needs to set this setting in 5.x to upgrade
*/
public static final Setting<Boolean> TRANSPORT_SSL_ENABLED =
new Setting<>("xpack.security.transport.ssl.enabled", (s) -> Boolean.toString(true),
(s) -> {
final boolean parsed = Booleans.parseBoolean(s);
if (parsed == false) {
throw new IllegalArgumentException("transport ssl cannot be disabled. Remove setting [" +
XPackPlugin.featureSettingPrefix(XPackPlugin.SECURITY) + ".transport.ssl.enabled]");
}
return true;
}, Property.NodeScope, Property.Deprecated);
/** Setting for enabling or disabling TLS. Defaults to false. */
public static final Setting<Boolean> TRANSPORT_SSL_ENABLED = Setting.boolSetting("xpack.security.transport.ssl.enabled", false,
Property.NodeScope);
/** Setting for enabling or disabling http ssl. Defaults to false. */
public static final Setting<Boolean> HTTP_SSL_ENABLED = Setting.boolSetting("xpack.security.http.ssl.enabled", false,

View File

@ -168,6 +168,7 @@ import org.elasticsearch.xpack.security.user.AnonymousUser;
import org.elasticsearch.xpack.ssl.SSLBootstrapCheck;
import org.elasticsearch.xpack.ssl.SSLConfigurationSettings;
import org.elasticsearch.xpack.ssl.SSLService;
import org.elasticsearch.xpack.ssl.TLSLicenseBootstrapCheck;
import org.elasticsearch.xpack.template.TemplateUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -247,7 +248,8 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin, Clus
checks.addAll(Arrays.asList(
new SSLBootstrapCheck(sslService, env),
new TokenSSLBootstrapCheck(),
new PkiRealmBootstrapCheck(sslService)));
new PkiRealmBootstrapCheck(sslService),
new TLSLicenseBootstrapCheck()));
checks.addAll(InternalRealms.getBootstrapChecks(settings));
this.bootstrapChecks = Collections.unmodifiableList(checks);
} else {
@ -918,32 +920,10 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin, Clus
@Override
public void accept(DiscoveryNode node, ClusterState state) {
License license = LicenseService.getLicense(state);
validateLicense(license);
}
void validateLicense(License license) {
if (license != null) {
switch (license.operationMode()) {
case MISSING:
case TRIAL:
case BASIC:
break;
case STANDARD:
case GOLD:
case PLATINUM:
if (isTLSEnabled == false) {
throw new IllegalStateException("TLS setup is required for license type [" + license.operationMode().name()
+ "]");
}
break;
default:
throw new AssertionError("unknown operation mode: " + license.operationMode());
}
License license = LicenseService.getLicense(state.metaData());
if (license != null && license.isProductionLicense() && isTLSEnabled == false) {
throw new IllegalStateException("TLS setup is required for license type [" + license.operationMode().name() + "]");
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.ssl;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.bootstrap.BootstrapCheck;
import org.elasticsearch.bootstrap.BootstrapContext;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.License;
import org.elasticsearch.license.LicenseService;
import org.elasticsearch.xpack.XPackSettings;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Bootstrap check to ensure that if we are starting up with a production license in the local clusterstate TLS is enabled
*/
public final class TLSLicenseBootstrapCheck implements BootstrapCheck {
@Override
public BootstrapCheckResult check(BootstrapContext context) {
if (XPackSettings.TRANSPORT_SSL_ENABLED.get(context.settings) == false) {
License license = LicenseService.getLicense(context.metaData);
if (license != null && license.isProductionLicense()) {
return BootstrapCheckResult.failure("Transport SSL must be enabled for setups with production licenses. Please set " +
"[xpack.security.transport.ssl.enabled] or disables security via [xpack.security.enabled]");
}
}
return BootstrapCheckResult.success();
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.license;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.joda.DateMathParser;
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
@ -345,4 +346,8 @@ public class TestUtils {
super.update(mode, active);
}
}
public static void putLicense(MetaData.Builder builder, License license) {
builder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license));
}
}

View File

@ -213,23 +213,26 @@ public class SecurityTests extends ESTestCase {
createComponents(Settings.EMPTY);
BiConsumer<DiscoveryNode, ClusterState> joinValidator = security.getJoinValidator();
assertNotNull(joinValidator);
ClusterState state = ClusterState.builder(ClusterName.DEFAULT).build();
joinValidator.accept(new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT), state);
DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT);
joinValidator.accept(node, ClusterState.builder(ClusterName.DEFAULT).build());
assertTrue(joinValidator instanceof Security.ValidateTLSOnJoin);
int numIters = randomIntBetween(1,10);
for (int i = 0; i < numIters; i++) {
boolean tlsOn = randomBoolean();
Security.ValidateTLSOnJoin validator = new Security.ValidateTLSOnJoin(tlsOn);
MetaData.Builder builder = MetaData.builder();
License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24));
TestUtils.putLicense(builder, license);
ClusterState state = ClusterState.builder(ClusterName.DEFAULT).metaData(builder.build()).build();
EnumSet<License.OperationMode> productionModes = EnumSet.of(License.OperationMode.GOLD, License.OperationMode.PLATINUM,
License.OperationMode.STANDARD);
if (productionModes.contains(license.operationMode()) && tlsOn == false) {
IllegalStateException ise = expectThrows(IllegalStateException.class, () -> validator.validateLicense(license));
IllegalStateException ise = expectThrows(IllegalStateException.class, () -> validator.accept(node, state));
assertEquals("TLS setup is required for license type [" + license.operationMode().name() + "]", ise.getMessage());
} else {
validator.validateLicense(license);
validator.accept(node, state);
}
validator.validateLicense(null);
validator.accept(node, ClusterState.builder(ClusterName.DEFAULT).metaData(MetaData.builder().build()).build());
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.ssl;
import org.elasticsearch.bootstrap.BootstrapContext;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.License;
import org.elasticsearch.license.TestUtils;
import org.elasticsearch.test.ESTestCase;
import java.util.EnumSet;
public class TLSLicenseBootstrapCheckTests extends ESTestCase {
public void testBootstrapCheck() throws Exception {
assertTrue(new TLSLicenseBootstrapCheck().check(new BootstrapContext(Settings.EMPTY, MetaData.EMPTY_META_DATA)).isSuccess());
assertTrue(new TLSLicenseBootstrapCheck().check(new BootstrapContext(Settings.builder().put("xpack.security.transport.ssl.enabled"
, randomBoolean()).build(), MetaData.EMPTY_META_DATA)).isSuccess());
int numIters = randomIntBetween(1,10);
for (int i = 0; i < numIters; i++) {
License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24));
EnumSet<License.OperationMode> productionModes = EnumSet.of(License.OperationMode.GOLD, License.OperationMode.PLATINUM,
License.OperationMode.STANDARD);
MetaData.Builder builder = MetaData.builder();
TestUtils.putLicense(builder, license);
MetaData build = builder.build();
if (productionModes.contains(license.operationMode()) == false) {
assertTrue(new TLSLicenseBootstrapCheck().check(new BootstrapContext(
Settings.builder().put("xpack.security.transport.ssl.enabled", true).build(), build)).isSuccess());
} else {
assertTrue(new TLSLicenseBootstrapCheck().check(new BootstrapContext(
Settings.builder().put("xpack.security.transport.ssl.enabled", false).build(), build)).isFailure());
assertEquals("Transport SSL must be enabled for setups with production licenses." +
" Please set [xpack.security.transport.ssl.enabled] or disables security via [xpack.security.enabled]",
new TLSLicenseBootstrapCheck().check(new BootstrapContext(
Settings.builder().put("xpack.security.transport.ssl.enabled", false).build(), build)).getMessage());
}
}
}
}