diff --git a/libs/ssl-config/build.gradle b/libs/ssl-config/build.gradle index d63df95003ab..3ee86e206b58 100644 --- a/libs/ssl-config/build.gradle +++ b/libs/ssl-config/build.gradle @@ -10,6 +10,7 @@ apply plugin: "elasticsearch.publish" dependencies { api project(':libs:core') + api project(':libs:entitlement') testImplementation(project(":test:framework")) { exclude group: 'org.elasticsearch', module: 'ssl-config' diff --git a/libs/ssl-config/src/main/java/module-info.java b/libs/ssl-config/src/main/java/module-info.java index 172ab6de193b..16d86bbc6ced 100644 --- a/libs/ssl-config/src/main/java/module-info.java +++ b/libs/ssl-config/src/main/java/module-info.java @@ -9,6 +9,7 @@ module org.elasticsearch.sslconfig { requires org.elasticsearch.base; + requires org.elasticsearch.entitlement; exports org.elasticsearch.common.ssl; } diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java index 6b708fa086cd..b1ddb89cdee7 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemKeyConfig.java @@ -10,6 +10,7 @@ package org.elasticsearch.common.ssl; import org.elasticsearch.core.Tuple; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import java.io.IOException; import java.nio.file.Path; @@ -127,6 +128,8 @@ public final class PemKeyConfig implements SslKeyConfig { return privateKey; } catch (AccessControlException e) { throw SslFileUtil.accessControlFailure(KEY_FILE_TYPE, List.of(path), e, configBasePath); + } catch (NotEntitledException e) { + throw SslFileUtil.notEntitledFailure(KEY_FILE_TYPE, List.of(path), e, configBasePath); } catch (IOException e) { throw SslFileUtil.ioException(KEY_FILE_TYPE, List.of(path), e); } catch (GeneralSecurityException e) { diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java index 1c7295d6acc8..04ea83ce6fa1 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemTrustConfig.java @@ -9,6 +9,8 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; @@ -99,6 +101,8 @@ public final class PemTrustConfig implements SslTrustConfig { return PemUtils.readCertificates(paths); } catch (AccessControlException e) { throw SslFileUtil.accessControlFailure(CA_FILE_TYPE, paths, e, basePath); + } catch (NotEntitledException e) { + throw SslFileUtil.notEntitledFailure(CA_FILE_TYPE, paths, e, basePath); } catch (IOException e) { throw SslFileUtil.ioException(CA_FILE_TYPE, paths, e); } catch (GeneralSecurityException e) { diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java index b7418a96f180..bb9c8d69513b 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/PemUtils.java @@ -10,6 +10,7 @@ package org.elasticsearch.common.ssl; import org.elasticsearch.core.CharArrays; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import java.io.BufferedReader; import java.io.IOException; @@ -112,6 +113,8 @@ public final class PemUtils { return privateKey; } catch (AccessControlException e) { throw SslFileUtil.accessControlFailure("PEM private key", List.of(path), e, null); + } catch (NotEntitledException e) { + throw SslFileUtil.notEntitledFailure("PEM private key", List.of(path), e, null); } catch (IOException e) { throw SslFileUtil.ioException("PEM private key", List.of(path), e); } catch (GeneralSecurityException e) { diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java index b5eee1355049..e94ef627ec36 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslFileUtil.java @@ -9,6 +9,8 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; + import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.AccessDeniedException; @@ -78,7 +80,15 @@ final class SslFileUtil { return new SslConfigException(message, cause); } + static SslConfigException notEntitledFailure(String fileType, List paths, NotEntitledException cause, Path basePath) { + return innerAccessControlFailure(fileType, paths, cause, basePath); + } + static SslConfigException accessControlFailure(String fileType, List paths, AccessControlException cause, Path basePath) { + return innerAccessControlFailure(fileType, paths, cause, basePath); + } + + private static SslConfigException innerAccessControlFailure(String fileType, List paths, Exception cause, Path basePath) { String message = "cannot read configured " + fileType + " [" + pathsToString(paths) + "] because "; if (paths.size() == 1) { message += "access to read the file is blocked"; diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java index 7952b7a89ed9..af400d5dce6f 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreKeyConfig.java @@ -11,6 +11,7 @@ package org.elasticsearch.common.ssl; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import java.io.IOException; import java.nio.file.Path; @@ -168,6 +169,8 @@ public class StoreKeyConfig implements SslKeyConfig { return KeyStoreUtil.readKeyStore(path, type, storePassword); } catch (AccessControlException e) { throw SslFileUtil.accessControlFailure("[" + type + "] keystore", List.of(path), e, configBasePath); + } catch (NotEntitledException e) { + throw SslFileUtil.notEntitledFailure("[" + type + "] keystore", List.of(path), e, configBasePath); } catch (IOException e) { throw SslFileUtil.ioException("[" + type + "] keystore", List.of(path), e); } catch (GeneralSecurityException e) { diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java index 0d5c28e652f3..16c57f7dfc82 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/StoreTrustConfig.java @@ -9,6 +9,8 @@ package org.elasticsearch.common.ssl; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; + import java.io.IOException; import java.nio.file.Path; import java.security.AccessControlException; @@ -95,6 +97,8 @@ public final class StoreTrustConfig implements SslTrustConfig { return KeyStoreUtil.readKeyStore(path, type, password); } catch (AccessControlException e) { throw SslFileUtil.accessControlFailure(fileTypeForException(), List.of(path), e, configBasePath); + } catch (NotEntitledException e) { + throw SslFileUtil.notEntitledFailure(fileTypeForException(), List.of(path), e, configBasePath); } catch (IOException e) { throw SslFileUtil.ioException(fileTypeForException(), List.of(path), e, getAdditionalErrorDetails()); } catch (GeneralSecurityException e) { diff --git a/x-pack/plugin/core/src/main/java/module-info.java b/x-pack/plugin/core/src/main/java/module-info.java index 00fd77a118c2..c6f8376d63fa 100644 --- a/x-pack/plugin/core/src/main/java/module-info.java +++ b/x-pack/plugin/core/src/main/java/module-info.java @@ -7,6 +7,7 @@ module org.elasticsearch.xcore { requires org.elasticsearch.cli; + requires org.elasticsearch.entitlement; requires org.elasticsearch.base; requires org.elasticsearch.grok; requires org.elasticsearch.server; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloader.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloader.java index cb32ef2d8b18..8d77852c88ac 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloader.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloader.java @@ -12,6 +12,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.ssl.SslConfiguration; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.entitlement.runtime.api.NotEntitledException; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; @@ -109,7 +110,7 @@ public final class SSLConfigurationReloader { fileWatcher.addListener(changeListener); try { resourceWatcherService.add(fileWatcher, Frequency.HIGH); - } catch (IOException | AccessControlException e) { + } catch (IOException | AccessControlException | NotEntitledException e) { logger.error("failed to start watching directory [{}] for ssl configurations [{}] - {}", path, configurations, e); } }); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/ssl/SslEntitlementRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/ssl/SslEntitlementRestIT.java new file mode 100644 index 000000000000..f661bb04dc3d --- /dev/null +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/ssl/SslEntitlementRestIT.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.ssl; + +import org.elasticsearch.common.io.Streams; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.LogType; +import org.elasticsearch.test.cluster.MutableSettingsProvider; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.junit.ClassRule; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.is; + +public class SslEntitlementRestIT extends ESRestTestCase { + + private static final MutableSettingsProvider settingsProvider = new MutableSettingsProvider(); + + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .apply(SecurityOnTrialLicenseRestTestCase.commonTrialSecurityClusterConfig) + .settings(settingsProvider) + .systemProperty("es.entitlements.enabled", "true") + .build(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + public void testSslEntitlementInaccessiblePath() throws IOException { + settingsProvider.put("xpack.security.transport.ssl.key", "/bad/path/transport.key"); + settingsProvider.put("xpack.security.transport.ssl.certificate", "/bad/path/transport.crt"); + expectThrows(Exception.class, () -> cluster.restart(false)); + AtomicBoolean found = new AtomicBoolean(false); + for (int i = 0; i < cluster.getNumNodes(); i++) { + try (InputStream log = cluster.getNodeLog(i, LogType.SERVER)) { + Streams.readAllLines(log, line -> { + if (line.contains("failed to load SSL configuration") && line.contains("because access to read the file is blocked")) { + found.set(true); + } + }); + } + } + assertThat("cluster logs did not include events of blocked file access", found.get(), is(true)); + } + + @Override + protected Settings restAdminSettings() { + String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray())); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray())); + return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); + } + + @Override + protected boolean preserveClusterUponCompletion() { + // as the cluster is dead its state can not be wiped successfully so we have to bypass wiping the cluster + return true; + } +}