Skip to content

Commit

Permalink
[Entitlements] Add set_https_connection_properties entitlement and …
Browse files Browse the repository at this point in the history
…checks (elastic#118577)
  • Loading branch information
ldematte authored Jan 2, 2025
1 parent 9862a43 commit 5df57fd
Show file tree
Hide file tree
Showing 21 changed files with 265 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
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 {

Expand All @@ -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);
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@
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;
Expand All @@ -49,16 +54,43 @@ static CheckAction deniedToPlugins(Runnable action) {
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(
entry("runtime_exit", deniedToPlugins(RestEntitlementsCheckAction::runtimeExit)),
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);
Expand Down Expand Up @@ -93,11 +125,17 @@ private static void processBuilder_startPipeline() {
}
}

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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];
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ALL-UNNAMED:
- create_class_loader
- set_https_connection_properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
org.elasticsearch.entitlement.qa.common:
- create_class_loader
- set_https_connection_properties
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public EntitlementsAllowedIT(@Name("pathPrefix") String pathPrefix, @Name("actio
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,7 +121,15 @@ private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, Strin
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
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.
Expand Down Expand Up @@ -78,4 +83,28 @@ public ElasticsearchEntitlementChecker(PolicyManager policyManager) {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ public void checkCreateClassLoader(Class<?> callerClass) {
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
*/
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,23 @@ public void testParseCreateClassloader() throws IOException {
)
);
}

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)))
)
)
);
}
}
2 changes: 2 additions & 0 deletions modules/apm/src/main/plugin-metadata/entitlement-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elastic.apm.agent:
- set_https_connection_properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALL-UNNAMED:
- set_https_connection_properties # required by google-http-client
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALL-UNNAMED:
- set_https_connection_properties # required by google-http-client
Loading

0 comments on commit 5df57fd

Please sign in to comment.