Handle NotEntitledException in SSL file utils (#123491)

SSL file utils currently only handle security manager access control
exceptions around file read checks. This PR extends these to support
entitlement checks as well. 

There is no easy way to unit test this since we can't run unit tests
with entitlements enabled (for now). The PR includes a REST test
instead. 

Relates: https://github.com/elastic/elasticsearch/issues/121960
This commit is contained in:
Nikolaj Volgushev 2025-02-27 14:06:09 +01:00 committed by GitHub
parent c7e7dbe904
commit a77626368f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 109 additions and 1 deletions

View file

@ -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'

View file

@ -9,6 +9,7 @@
module org.elasticsearch.sslconfig {
requires org.elasticsearch.base;
requires org.elasticsearch.entitlement;
exports org.elasticsearch.common.ssl;
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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<Path> paths, NotEntitledException cause, Path basePath) {
return innerAccessControlFailure(fileType, paths, cause, basePath);
}
static SslConfigException accessControlFailure(String fileType, List<Path> paths, AccessControlException cause, Path basePath) {
return innerAccessControlFailure(fileType, paths, cause, basePath);
}
private static SslConfigException innerAccessControlFailure(String fileType, List<Path> 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";

View file

@ -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) {

View file

@ -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) {

View file

@ -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;

View file

@ -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);
}
});

View file

@ -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;
}
}