[Entitlements] Add set_https_connection_properties entitlement and checks (#118577)

This commit is contained in:
Lorenzo Dematté 2025-01-02 16:06:37 +01:00 committed by GitHub
parent 9862a43cb6
commit 5df57fda72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 265 additions and 8 deletions

View file

@ -13,6 +13,11 @@ import java.net.URL;
import java.net.URLStreamHandlerFactory;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
@SuppressWarnings("unused") // Called from instrumentation code inserted by the Entitlements agent
public interface EntitlementChecker {
@ -21,7 +26,7 @@ public interface EntitlementChecker {
void check$java_lang_Runtime$halt(Class<?> callerClass, Runtime runtime, int status);
// URLClassLoader ctor
// URLClassLoader constructors
void check$java_net_URLClassLoader$(Class<?> callerClass, URL[] urls);
void check$java_net_URLClassLoader$(Class<?> callerClass, URL[] urls, ClassLoader parent);
@ -32,6 +37,15 @@ public interface EntitlementChecker {
void check$java_net_URLClassLoader$(Class<?> callerClass, String name, URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory);
// "setFactory" methods
void check$javax_net_ssl_HttpsURLConnection$setSSLSocketFactory(Class<?> callerClass, HttpsURLConnection conn, SSLSocketFactory sf);
void check$javax_net_ssl_HttpsURLConnection$$setDefaultSSLSocketFactory(Class<?> callerClass, SSLSocketFactory sf);
void check$javax_net_ssl_HttpsURLConnection$$setDefaultHostnameVerifier(Class<?> callerClass, HostnameVerifier hv);
void check$javax_net_ssl_SSLContext$$setDefault(Class<?> callerClass, SSLContext context);
// Process creation
void check$java_lang_ProcessBuilder$start(Class<?> callerClass, ProcessBuilder that);

View file

@ -23,12 +23,17 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import static java.util.Map.entry;
import static org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction.CheckAction.alwaysDenied;
import static org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction.CheckAction.deniedToPlugins;
import static org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction.CheckAction.forPlugins;
import static org.elasticsearch.rest.RestRequest.Method.GET;
@ -49,6 +54,10 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
static CheckAction forPlugins(Runnable action) {
return new CheckAction(action, false);
}
static CheckAction alwaysDenied(Runnable action) {
return new CheckAction(action, true);
}
}
private static final Map<String, CheckAction> checkActions = Map.ofEntries(
@ -56,9 +65,32 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
entry("runtime_halt", deniedToPlugins(RestEntitlementsCheckAction::runtimeHalt)),
entry("create_classloader", forPlugins(RestEntitlementsCheckAction::createClassLoader)),
entry("processBuilder_start", deniedToPlugins(RestEntitlementsCheckAction::processBuilder_start)),
entry("processBuilder_startPipeline", deniedToPlugins(RestEntitlementsCheckAction::processBuilder_startPipeline))
entry("processBuilder_startPipeline", deniedToPlugins(RestEntitlementsCheckAction::processBuilder_startPipeline)),
entry("set_https_connection_properties", forPlugins(RestEntitlementsCheckAction::setHttpsConnectionProperties)),
entry("set_default_ssl_socket_factory", alwaysDenied(RestEntitlementsCheckAction::setDefaultSSLSocketFactory)),
entry("set_default_hostname_verifier", alwaysDenied(RestEntitlementsCheckAction::setDefaultHostnameVerifier)),
entry("set_default_ssl_context", alwaysDenied(RestEntitlementsCheckAction::setDefaultSSLContext))
);
private static void setDefaultSSLContext() {
logger.info("Calling SSLContext.setDefault");
try {
SSLContext.setDefault(SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static void setDefaultHostnameVerifier() {
logger.info("Calling HttpsURLConnection.setDefaultHostnameVerifier");
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> false);
}
private static void setDefaultSSLSocketFactory() {
logger.info("Calling HttpsURLConnection.setDefaultSSLSocketFactory");
HttpsURLConnection.setDefaultSSLSocketFactory(new TestSSLSocketFactory());
}
@SuppressForbidden(reason = "Specifically testing Runtime.exit")
private static void runtimeExit() {
Runtime.getRuntime().exit(123);
@ -93,11 +125,17 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
}
}
private static void setHttpsConnectionProperties() {
logger.info("Calling setSSLSocketFactory");
var connection = new TestHttpsURLConnection();
connection.setSSLSocketFactory(new TestSSLSocketFactory());
}
public RestEntitlementsCheckAction(String prefix) {
this.prefix = prefix;
}
public static Set<String> getServerAndPluginsCheckActions() {
public static Set<String> getCheckActionsAllowedInPlugins() {
return checkActions.entrySet()
.stream()
.filter(kv -> kv.getValue().isAlwaysDeniedToPlugins() == false)

View file

@ -0,0 +1,48 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.qa.common;
import java.io.IOException;
import java.security.cert.Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
class TestHttpsURLConnection extends HttpsURLConnection {
TestHttpsURLConnection() {
super(null);
}
@Override
public void connect() throws IOException {}
@Override
public void disconnect() {}
@Override
public boolean usingProxy() {
return false;
}
@Override
public String getCipherSuite() {
return "";
}
@Override
public Certificate[] getLocalCertificates() {
return new Certificate[0];
}
@Override
public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
return new Certificate[0];
}
}

View file

@ -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
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.qa.common;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLSocketFactory;
class TestSSLSocketFactory extends SSLSocketFactory {
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) {
return null;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return null;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return null;
}
@Override
public String[] getDefaultCipherSuites() {
return new String[0];
}
@Override
public String[] getSupportedCipherSuites() {
return new String[0];
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return null;
}
}

View file

@ -1,2 +1,3 @@
ALL-UNNAMED:
- create_class_loader
- set_https_connection_properties

View file

@ -1,2 +1,3 @@
org.elasticsearch.entitlement.qa.common:
- create_class_loader
- set_https_connection_properties

View file

@ -46,7 +46,7 @@ public class EntitlementsAllowedIT extends ESRestTestCase {
public static Iterable<Object[]> data() {
return Stream.of("allowed", "allowed_nonmodular")
.flatMap(
path -> RestEntitlementsCheckAction.getServerAndPluginsCheckActions().stream().map(action -> new Object[] { path, action })
path -> RestEntitlementsCheckAction.getCheckActionsAllowedInPlugins().stream().map(action -> new Object[] { path, action })
)
.toList();
}

View file

@ -9,6 +9,7 @@
package org.elasticsearch.entitlement.initialization;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.internal.provider.ProviderLocator;
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
@ -120,7 +121,15 @@ public class EntitlementInitialization {
// TODO: should this check actually be part of the parser?
for (Scope scope : policy.scopes) {
if (moduleNames.contains(scope.name) == false) {
throw new IllegalStateException("policy [" + policyFile + "] contains invalid module [" + scope.name + "]");
throw new IllegalStateException(
Strings.format(
"Invalid module name in policy: plugin [%s] does not have module [%s]; available modules [%s]; policy file [%s]",
pluginName,
scope.name,
String.join(", ", moduleNames),
policyFile
)
);
}
}
return policy;

View file

@ -16,6 +16,11 @@ import java.net.URL;
import java.net.URLStreamHandlerFactory;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
/**
* Implementation of the {@link EntitlementChecker} interface, providing additional
* API methods for managing the checks.
@ -78,4 +83,28 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker {
public void check$java_lang_ProcessBuilder$$startPipeline(Class<?> callerClass, List<ProcessBuilder> builders) {
policyManager.checkStartProcess(callerClass);
}
@Override
public void check$javax_net_ssl_HttpsURLConnection$setSSLSocketFactory(
Class<?> callerClass,
HttpsURLConnection connection,
SSLSocketFactory sf
) {
policyManager.checkSetHttpsConnectionProperties(callerClass);
}
@Override
public void check$javax_net_ssl_HttpsURLConnection$$setDefaultSSLSocketFactory(Class<?> callerClass, SSLSocketFactory sf) {
policyManager.checkSetGlobalHttpsConnectionProperties(callerClass);
}
@Override
public void check$javax_net_ssl_HttpsURLConnection$$setDefaultHostnameVerifier(Class<?> callerClass, HostnameVerifier hv) {
policyManager.checkSetGlobalHttpsConnectionProperties(callerClass);
}
@Override
public void check$javax_net_ssl_SSLContext$$setDefault(Class<?> callerClass, SSLContext context) {
policyManager.checkSetGlobalHttpsConnectionProperties(callerClass);
}
}

View file

@ -130,6 +130,14 @@ public class PolicyManager {
checkEntitlementPresent(callerClass, CreateClassLoaderEntitlement.class);
}
public void checkSetHttpsConnectionProperties(Class<?> callerClass) {
checkEntitlementPresent(callerClass, SetHttpsConnectionPropertiesEntitlement.class);
}
public void checkSetGlobalHttpsConnectionProperties(Class<?> callerClass) {
neverEntitled(callerClass, "set global https connection properties");
}
private void checkEntitlementPresent(Class<?> callerClass, Class<? extends Entitlement> entitlementClass) {
var requestingModule = requestingModule(callerClass);
if (isTriviallyAllowed(requestingModule)) {

View file

@ -34,8 +34,11 @@ import java.util.stream.Stream;
*/
public class PolicyParser {
private static final Map<String, Class<?>> EXTERNAL_ENTITLEMENTS = Stream.of(FileEntitlement.class, CreateClassLoaderEntitlement.class)
.collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity()));
private static final Map<String, Class<?>> EXTERNAL_ENTITLEMENTS = Stream.of(
FileEntitlement.class,
CreateClassLoaderEntitlement.class,
SetHttpsConnectionPropertiesEntitlement.class
).collect(Collectors.toUnmodifiableMap(PolicyParser::getEntitlementTypeName, Function.identity()));
protected final XContentParser policyParser;
protected final String policyName;

View file

@ -0,0 +1,18 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.entitlement.runtime.policy;
/**
* An Entitlement to allow setting properties to a single Https connection after this has been created
*/
public class SetHttpsConnectionPropertiesEntitlement implements Entitlement {
@ExternalEntitlement(esModulesOnly = false)
public SetHttpsConnectionPropertiesEntitlement() {}
}

View file

@ -74,4 +74,23 @@ public class PolicyParserTests extends ESTestCase {
)
);
}
public void testParseSetHttpsConnectionProperties() throws IOException {
Policy parsedPolicy = new PolicyParser(new ByteArrayInputStream("""
entitlement-module-name:
- set_https_connection_properties
""".getBytes(StandardCharsets.UTF_8)), "test-policy.yaml", true).parsePolicy();
Policy builtPolicy = new Policy(
"test-policy.yaml",
List.of(new Scope("entitlement-module-name", List.of(new CreateClassLoaderEntitlement())))
);
assertThat(
parsedPolicy.scopes,
contains(
both(transformedMatch((Scope scope) -> scope.name, equalTo("entitlement-module-name"))).and(
transformedMatch(scope -> scope.entitlements, contains(instanceOf(SetHttpsConnectionPropertiesEntitlement.class)))
)
)
);
}
}