diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/EvilSecurityTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/EvilSecurityTests.java deleted file mode 100644 index bc8308f48e52d..0000000000000 --- a/qa/evil-tests/src/test/java/org/elasticsearch/bootstrap/EvilSecurityTests.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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.bootstrap; - -import org.apache.lucene.util.Constants; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.PathUtils; -import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.env.Environment; -import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.test.ESTestCase; - -import java.io.FilePermission; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.PermissionCollection; -import java.security.Permissions; -import java.util.Set; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasToString; - -@SuppressForbidden(reason = "modifies system properties and attempts to create symbolic links intentionally") -public class EvilSecurityTests extends ESTestCase { - - /** test generated permissions */ - public void testGeneratedPermissions() throws Exception { - Path path = createTempDir(); - // make a fake ES home and ensure we only grant permissions to that. - Path esHome = path.resolve("esHome"); - Settings.Builder settingsBuilder = Settings.builder(); - settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString()); - Settings settings = settingsBuilder.build(); - - Path fakeTmpDir = createTempDir(); - String realTmpDir = System.getProperty("java.io.tmpdir"); - Permissions permissions; - try { - System.setProperty("java.io.tmpdir", fakeTmpDir.toString()); - Environment environment = TestEnvironment.newEnvironment(settings); - permissions = Security.createPermissions(environment, null); - } finally { - System.setProperty("java.io.tmpdir", realTmpDir); - } - - // the fake es home - assertNoPermissions(esHome, permissions); - // its parent - assertNoPermissions(esHome.getParent(), permissions); - // some other sibling - assertNoPermissions(esHome.getParent().resolve("other"), permissions); - // double check we overwrote java.io.tmpdir correctly for the test - assertNoPermissions(PathUtils.get(realTmpDir), permissions); - } - - /** test generated permissions for all configured paths */ - @SuppressForbidden(reason = "to create FilePermission object") - public void testEnvironmentPaths() throws Exception { - Path path = createTempDir(); - // make a fake ES home and ensure we only grant permissions to that. - Path esHome = path.resolve("esHome"); - - Settings.Builder settingsBuilder = Settings.builder(); - settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.resolve("home").toString()); - settingsBuilder.putList( - Environment.PATH_DATA_SETTING.getKey(), - esHome.resolve("data1").toString(), - esHome.resolve("data2").toString() - ); - settingsBuilder.put(Environment.PATH_SHARED_DATA_SETTING.getKey(), esHome.resolve("custom").toString()); - settingsBuilder.put(Environment.PATH_LOGS_SETTING.getKey(), esHome.resolve("logs").toString()); - Settings settings = settingsBuilder.build(); - - Path fakeTmpDir = createTempDir(); - String realTmpDir = System.getProperty("java.io.tmpdir"); - Permissions permissions; - Environment environment; - try { - System.setProperty("java.io.tmpdir", fakeTmpDir.toString()); - environment = new Environment(settings, esHome.resolve("conf")); - permissions = Security.createPermissions(environment, null); - } finally { - System.setProperty("java.io.tmpdir", realTmpDir); - } - - // the fake es home - assertNoPermissions(esHome, permissions); - // its parent - assertNoPermissions(esHome.getParent(), permissions); - // some other sibling - assertNoPermissions(esHome.getParent().resolve("other"), permissions); - // double check we overwrote java.io.tmpdir correctly for the test - assertNoPermissions(PathUtils.get(realTmpDir), permissions); - - // check that all directories got permissions: - - // bin file: ro - assertExactPermissions(new FilePermission(environment.binDir().toString(), "read,readlink"), permissions); - // lib file: ro - assertExactPermissions(new FilePermission(environment.libDir().toString(), "read,readlink"), permissions); - // modules file: ro - assertExactPermissions(new FilePermission(environment.modulesDir().toString(), "read,readlink"), permissions); - // config file: ro - assertExactPermissions(new FilePermission(environment.configDir().toString(), "read,readlink"), permissions); - // plugins: ro - assertExactPermissions(new FilePermission(environment.pluginsDir().toString(), "read,readlink"), permissions); - - // data paths: r/w - for (Path dataPath : environment.dataDirs()) { - assertExactPermissions(new FilePermission(dataPath.toString(), "read,readlink,write,delete"), permissions); - } - assertExactPermissions(new FilePermission(environment.sharedDataDir().toString(), "read,readlink,write,delete"), permissions); - // logs: r/w - assertExactPermissions(new FilePermission(environment.logsDir().toString(), "read,readlink,write,delete"), permissions); - // temp dir: r/w - assertExactPermissions(new FilePermission(fakeTmpDir.toString(), "read,readlink,write,delete"), permissions); - } - - public void testDuplicateDataPaths() throws IOException { - assumeFalse("https://github.com/elastic/elasticsearch/issues/44558", Constants.WINDOWS); - final Path path = createTempDir(); - final Path home = path.resolve("home"); - final Path data = path.resolve("data"); - final Path duplicate; - if (randomBoolean()) { - duplicate = data; - } else { - duplicate = createTempDir().toAbsolutePath().resolve("link"); - Files.createSymbolicLink(duplicate, data); - } - - final Settings settings = Settings.builder() - .put(Environment.PATH_HOME_SETTING.getKey(), home.toString()) - .putList(Environment.PATH_DATA_SETTING.getKey(), data.toString(), duplicate.toString()) - .build(); - - final Environment environment = TestEnvironment.newEnvironment(settings); - final IllegalStateException e = expectThrows(IllegalStateException.class, () -> Security.createPermissions(environment, null)); - assertThat(e, hasToString(containsString("path [" + duplicate.toRealPath() + "] is duplicated by [" + duplicate + "]"))); - } - - public void testEnsureSymlink() throws IOException { - Path p = createTempDir(); - - Path exists = p.resolve("exists"); - Files.createDirectory(exists); - - // symlink - Path linkExists = p.resolve("linkExists"); - try { - Files.createSymbolicLink(linkExists, exists); - } catch (UnsupportedOperationException | IOException e) { - assumeNoException("test requires filesystem that supports symbolic links", e); - } catch (SecurityException e) { - assumeNoException("test cannot create symbolic links with security manager enabled", e); - } - Security.ensureDirectoryExists(linkExists); - Files.createTempFile(linkExists, null, null); - } - - public void testEnsureBrokenSymlink() throws IOException { - Path p = createTempDir(); - - // broken symlink - Path brokenLink = p.resolve("brokenLink"); - try { - Files.createSymbolicLink(brokenLink, p.resolve("nonexistent")); - } catch (UnsupportedOperationException | IOException e) { - assumeNoException("test requires filesystem that supports symbolic links", e); - } catch (SecurityException e) { - assumeNoException("test cannot create symbolic links with security manager enabled", e); - } - try { - Security.ensureDirectoryExists(brokenLink); - fail("didn't get expected exception"); - } catch (IOException expected) {} - } - - /** When a configured dir is a symlink, test that permissions work on link target */ - public void testSymlinkPermissions() throws IOException { - // see https://github.com/elastic/elasticsearch/issues/12170 - assumeFalse("windows does not automatically grant permission to the target of symlinks", Constants.WINDOWS); - Path dir = createTempDir(); - - Path target = dir.resolve("target"); - Files.createDirectory(target); - - // symlink - Path link = dir.resolve("link"); - try { - Files.createSymbolicLink(link, target); - } catch (UnsupportedOperationException | IOException e) { - assumeNoException("test requires filesystem that supports symbolic links", e); - } catch (SecurityException e) { - assumeNoException("test cannot create symbolic links with security manager enabled", e); - } - Permissions permissions = new Permissions(); - FilePermissionUtils.addDirectoryPath(permissions, "testing", link, "read", false); - assertExactPermissions(new FilePermission(link.toString(), "read"), permissions); - assertExactPermissions(new FilePermission(link.resolve("foo").toString(), "read"), permissions); - assertExactPermissions(new FilePermission(target.toString(), "read"), permissions); - assertExactPermissions(new FilePermission(target.resolve("foo").toString(), "read"), permissions); - } - - /** - * checks exact file permissions, meaning those and only those for that path. - */ - @SuppressForbidden(reason = "to create FilePermission object") - static void assertExactPermissions(FilePermission expected, PermissionCollection actual) { - String target = expected.getName(); // see javadocs - Set permissionSet = asSet(expected.getActions().split(",")); - boolean read = permissionSet.remove("read"); - boolean readlink = permissionSet.remove("readlink"); - boolean write = permissionSet.remove("write"); - boolean delete = permissionSet.remove("delete"); - boolean execute = permissionSet.remove("execute"); - assertTrue("unrecognized permission: " + permissionSet, permissionSet.isEmpty()); - assertEquals(read, actual.implies(new FilePermission(target, "read"))); - assertEquals(readlink, actual.implies(new FilePermission(target, "readlink"))); - assertEquals(write, actual.implies(new FilePermission(target, "write"))); - assertEquals(delete, actual.implies(new FilePermission(target, "delete"))); - assertEquals(execute, actual.implies(new FilePermission(target, "execute"))); - } - - /** - * checks that this path has no permissions - */ - @SuppressForbidden(reason = "to create FilePermission object") - static void assertNoPermissions(Path path, PermissionCollection actual) { - String target = path.toString(); - assertFalse(actual.implies(new FilePermission(target, "read"))); - assertFalse(actual.implies(new FilePermission(target, "readlink"))); - assertFalse(actual.implies(new FilePermission(target, "write"))); - assertFalse(actual.implies(new FilePermission(target, "delete"))); - assertFalse(actual.implies(new FilePermission(target, "execute"))); - } -} diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index 8ad85d0168349..c0df102bfbb16 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -41,7 +41,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.jdk.JarHell; -import org.elasticsearch.jdk.RuntimeVersionFeature; import org.elasticsearch.monitor.jvm.HotThreads; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.monitor.os.OsProbe; @@ -62,7 +61,6 @@ import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; -import java.security.Permission; import java.security.Security; import java.util.ArrayList; import java.util.HashMap; @@ -77,7 +75,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.elasticsearch.bootstrap.BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING; import static org.elasticsearch.nativeaccess.WindowsFunctions.ConsoleCtrlHandler.CTRL_CLOSE_EVENT; /** @@ -133,20 +130,6 @@ private static Bootstrap initPhase1() { final boolean useEntitlements = true; try { initSecurityProperties(); - - /* - * We want the JVM to think there is a security manager installed so that if internal policy decisions that would be based on - * the presence of a security manager or lack thereof act as if there is a security manager present (e.g., DNS cache policy). - * This forces such policies to take effect immediately. - */ - if (useEntitlements == false && RuntimeVersionFeature.isSecurityManagerAvailable()) { - org.elasticsearch.bootstrap.Security.setSecurityManager(new SecurityManager() { - @Override - public void checkPermission(Permission perm) { - // grant all permissions so that we can later set the security manager to the one that we want - } - }); - } LogConfigurator.registerErrorListener(); BootstrapInfo.init(); @@ -242,61 +225,47 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { final PluginsLoader pluginsLoader; - if (bootstrap.useEntitlements()) { - LogManager.getLogger(Elasticsearch.class).info("Bootstrapping Entitlements"); - - var pluginData = Stream.concat( - modulesBundles.stream() - .map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), - pluginsBundles.stream() - .map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) - ).toList(); - - var pluginPolicyPatches = collectPluginPolicyPatches(modulesBundles, pluginsBundles, logger); - var pluginPolicies = PolicyUtils.createPluginPolicies(pluginData, pluginPolicyPatches, Build.current().version()); - var serverPolicyPatch = PolicyUtils.parseEncodedPolicyIfExists( - System.getProperty(SERVER_POLICY_PATCH_NAME), - Build.current().version(), - false, - "server", - PolicyManager.SERVER_LAYER_MODULES.stream().map(Module::getName).collect(Collectors.toUnmodifiableSet()) - ); + LogManager.getLogger(Elasticsearch.class).info("Bootstrapping Entitlements"); + + var pluginData = Stream.concat( + modulesBundles.stream() + .map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), + pluginsBundles.stream().map(bundle -> new PolicyUtils.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) + ).toList(); + + var pluginPolicyPatches = collectPluginPolicyPatches(modulesBundles, pluginsBundles, logger); + var pluginPolicies = PolicyUtils.createPluginPolicies(pluginData, pluginPolicyPatches, Build.current().version()); + var serverPolicyPatch = PolicyUtils.parseEncodedPolicyIfExists( + System.getProperty(SERVER_POLICY_PATCH_NAME), + Build.current().version(), + false, + "server", + PolicyManager.SERVER_LAYER_MODULES.stream().map(Module::getName).collect(Collectors.toUnmodifiableSet()) + ); - pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies)); - - var scopeResolver = ScopeResolver.create(pluginsLoader.pluginLayers(), APM_AGENT_PACKAGE_NAME); - Map sourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream()) - .collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), PluginBundle::getDir)); - EntitlementBootstrap.bootstrap( - serverPolicyPatch, - pluginPolicies, - scopeResolver::resolveClassToScope, - nodeEnv.settings()::getValues, - nodeEnv.dataDirs(), - nodeEnv.repoDirs(), - nodeEnv.configDir(), - nodeEnv.libDir(), - nodeEnv.modulesDir(), - nodeEnv.pluginsDir(), - sourcePaths, - nodeEnv.logsDir(), - nodeEnv.tmpDir(), - args.pidFile(), - Set.of(EntitlementSelfTester.class) - ); - EntitlementSelfTester.entitlementSelfTest(); - } else { - assert RuntimeVersionFeature.isSecurityManagerAvailable(); - // no need to explicitly enable native access for legacy code - pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, Map.of()); - // install SM after natives, shutdown hooks, etc. - LogManager.getLogger(Elasticsearch.class).info("Bootstrapping java SecurityManager"); - org.elasticsearch.bootstrap.Security.configure( - nodeEnv, - SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(args.nodeSettings()), - args.pidFile() - ); - } + pluginsLoader = PluginsLoader.createPluginsLoader(modulesBundles, pluginsBundles, findPluginsWithNativeAccess(pluginPolicies)); + + var scopeResolver = ScopeResolver.create(pluginsLoader.pluginLayers(), APM_AGENT_PACKAGE_NAME); + Map sourcePaths = Stream.concat(modulesBundles.stream(), pluginsBundles.stream()) + .collect(Collectors.toUnmodifiableMap(bundle -> bundle.pluginDescriptor().getName(), PluginBundle::getDir)); + EntitlementBootstrap.bootstrap( + serverPolicyPatch, + pluginPolicies, + scopeResolver::resolveClassToScope, + nodeEnv.settings()::getValues, + nodeEnv.dataDirs(), + nodeEnv.repoDirs(), + nodeEnv.configDir(), + nodeEnv.libDir(), + nodeEnv.modulesDir(), + nodeEnv.pluginsDir(), + sourcePaths, + nodeEnv.logsDir(), + nodeEnv.tmpDir(), + args.pidFile(), + Set.of(EntitlementSelfTester.class) + ); + EntitlementSelfTester.entitlementSelfTest(); bootstrap.setPluginsLoader(pluginsLoader); } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/FilePermissionUtils.java b/server/src/main/java/org/elasticsearch/bootstrap/FilePermissionUtils.java deleted file mode 100644 index 396318a2b2cf7..0000000000000 --- a/server/src/main/java/org/elasticsearch/bootstrap/FilePermissionUtils.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.bootstrap; - -import org.elasticsearch.core.SuppressForbidden; - -import java.io.FilePermission; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.Permissions; - -public class FilePermissionUtils { - - /** no instantiation */ - private FilePermissionUtils() {} - - /** - * Add access to single file path - * @param policy current policy to add permissions to - * @param path the path itself - * @param permissions set of file permissions to grant to the path - */ - @SuppressForbidden(reason = "only place where creating Java-9 compatible FilePermission objects is possible") - public static void addSingleFilePath(Permissions policy, Path path, String permissions) throws IOException { - policy.add(new FilePermission(path.toString(), permissions)); - if (Files.exists(path)) { - /* - * The file permission model since JDK 9 requires this due to the removal of pathname canonicalization. See also - * https://github.com/elastic/elasticsearch/issues/21534. - */ - final Path realPath = path.toRealPath(); - if (path.toString().equals(realPath.toString()) == false) { - policy.add(new FilePermission(realPath.toString(), permissions)); - } - } - } - - /** - * Add access to path with direct and/or recursive access. This also creates the directory if it does not exist. - * - * @param policy current policy to add permissions to - * @param configurationName the configuration name associated with the path (for error messages only) - * @param path the path itself - * @param permissions set of file permissions to grant to the path - * @param recursiveAccessOnly indicates if the permission should provide recursive access to files underneath - */ - @SuppressForbidden(reason = "only place where creating Java-9 compatible FilePermission objects is possible") - public static void addDirectoryPath( - Permissions policy, - String configurationName, - Path path, - String permissions, - boolean recursiveAccessOnly - ) throws IOException { - // paths may not exist yet, this also checks accessibility - try { - Security.ensureDirectoryExists(path); - } catch (IOException e) { - throw new IllegalStateException("Unable to access '" + configurationName + "' (" + path + ")", e); - } - - // For some file permissions (data.path) we create a Permissions object that only checks the concrete - // path. Adding the directory would only create more overhead for this fast path. - if (recursiveAccessOnly == false) { - // add access for path itself - policy.add(new FilePermission(path.toString(), permissions)); - } - policy.add(new FilePermission(path.toString() + path.getFileSystem().getSeparator() + "-", permissions)); - /* - * The file permission model since JDK 9 requires this due to the removal of pathname canonicalization. See also - * https://github.com/elastic/elasticsearch/issues/21534. - */ - final Path realPath = path.toRealPath(); - if (path.toString().equals(realPath.toString()) == false) { - if (recursiveAccessOnly == false) { - // add access for path itself - policy.add(new FilePermission(realPath.toString(), permissions)); - } - // add access for files underneath - policy.add(new FilePermission(realPath.toString() + realPath.getFileSystem().getSeparator() + "-", permissions)); - } - } -} diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Security.java b/server/src/main/java/org/elasticsearch/bootstrap/Security.java deleted file mode 100644 index a352112b67afb..0000000000000 --- a/server/src/main/java/org/elasticsearch/bootstrap/Security.java +++ /dev/null @@ -1,538 +0,0 @@ -/* - * 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.bootstrap; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.SecuredConfigFileAccessPermission; -import org.elasticsearch.SecuredConfigFileSettingAccessPermission; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.PathUtils; -import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.env.Environment; -import org.elasticsearch.http.HttpTransportSettings; -import org.elasticsearch.jdk.JarHell; -import org.elasticsearch.logging.LogManager; -import org.elasticsearch.logging.Logger; -import org.elasticsearch.plugins.PluginsUtils; -import org.elasticsearch.secure_sm.SecureSM; -import org.elasticsearch.transport.TcpTransport; - -import java.io.FilePermission; -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.lang.reflect.Field; -import java.net.SocketPermission; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.AccessMode; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.NotDirectoryException; -import java.nio.file.Path; -import java.security.Permission; -import java.security.Permissions; -import java.security.Policy; -import java.security.UnresolvedPermission; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.regex.Pattern; - -import static java.lang.invoke.MethodType.methodType; -import static org.elasticsearch.bootstrap.ESPolicy.POLICY_RESOURCE; -import static org.elasticsearch.bootstrap.FilePermissionUtils.addDirectoryPath; -import static org.elasticsearch.bootstrap.FilePermissionUtils.addSingleFilePath; -import static org.elasticsearch.reservedstate.service.FileSettingsService.OPERATOR_DIRECTORY; -import static org.elasticsearch.reservedstate.service.FileSettingsService.SETTINGS_FILE_NAME; - -/** - * Initializes SecurityManager with necessary permissions. - *
- *

Initialization

- * The JVM is not initially started with security manager enabled, - * instead we turn it on early in the startup process. This is a tradeoff - * between security and ease of use: - *
    - *
  • Assigns file permissions to user-configurable paths that can - * be specified from the command-line or {@code elasticsearch.yml}.
  • - *
  • Allows for some contained usage of native code that would not - * otherwise be permitted.
  • - *
- *
- *

Permissions

- * Permissions use a policy file packaged as a resource, this file is - * also used in tests. File permissions are generated dynamically and - * combined with this policy file. - *

- * For each configured path, we ensure it exists and is accessible before - * granting permissions, otherwise directory creation would require - * permissions to parent directories. - *

- * In some exceptional cases, permissions are assigned to specific jars only, - * when they are so dangerous that general code should not be granted the - * permission, but there are extenuating circumstances. - *

- * Scripts (groovy) are assigned minimal permissions. This does not provide adequate - * sandboxing, as these scripts still have access to ES classes, and could - * modify members, etc that would cause bad things to happen later on their - * behalf (no package protections are yet in place, this would need some - * cleanups to the scripting apis). But still it can provide some defense for users - * that enable dynamic scripting without being fully aware of the consequences. - *
- *

Debugging Security

- * A good place to start when there is a problem is to turn on security debugging: - *
- * ES_JAVA_OPTS="-Djava.security.debug=access,failure" bin/elasticsearch
- * 
- *

- * When running tests you have to pass it to the test runner like this: - *

- * gradle test -Dtests.jvm.argline="-Djava.security.debug=access,failure" ...
- * 
- * See - * Troubleshooting Security for information. - */ -final class Security { - - private static Logger logger; // not init'd until configure call below - - static { - prepopulateSecurityCaller(); - } - - /** no instantiation */ - private Security() {} - - static void setSecurityManager(@SuppressWarnings("removal") SecurityManager sm) { - System.setSecurityManager(sm); - } - - /** - * Initializes SecurityManager for the environment - * Can only happen once! - * @param environment configuration for generating dynamic permissions - * @param filterBadDefaults true if we should filter out bad java defaults in the system policy. - */ - static void configure(Environment environment, boolean filterBadDefaults, Path pidFile) throws IOException { - logger = LogManager.getLogger(Security.class); - - // enable security policy: union of template and environment-based paths, and possibly plugin permissions - Map codebases = PolicyUtil.getCodebaseJarMap(JarHell.parseModulesAndClassPath()); - Policy mainPolicy = PolicyUtil.readPolicy(ESPolicy.class.getResource(POLICY_RESOURCE), codebases); - Map pluginPolicies = getPluginAndModulePermissions(environment); - Policy.setPolicy( - new ESPolicy( - mainPolicy, - createPermissions(environment, pidFile), - pluginPolicies, - filterBadDefaults, - createRecursiveDataPathPermission(environment), - readSecuredConfigFiles(environment, mainPolicy, codebases.values(), pluginPolicies) - ) - ); - - // enable security manager - final String[] classesThatCanExit = new String[] { - // SecureSM matches class names as regular expressions so we escape the $ that arises from the nested class name - ElasticsearchUncaughtExceptionHandler.PrivilegedHaltAction.class.getName().replace("$", "\\$"), - Bootstrap.class.getName() }; - setSecurityManager(new SecureSM(classesThatCanExit)); - - // do some basic tests - selfTest(); - } - - /** - * Sets properties (codebase URLs) for policy files. - * we look for matching plugins and set URLs to fit - */ - @SuppressForbidden(reason = "proper use of URL") - static Map getPluginAndModulePermissions(Environment environment) throws IOException { - Map map = new HashMap<>(); - Consumer addPolicy = pluginPolicy -> { - if (pluginPolicy == null) { - return; - } - - // consult this policy for each of the plugin's jars: - for (URL jar : pluginPolicy.jars()) { - if (map.put(jar, pluginPolicy.policy()) != null) { - // just be paranoid ok? - throw new IllegalStateException("per-plugin permissions already granted for jar file: " + jar); - } - } - }; - - for (Path plugin : PluginsUtils.findPluginDirs(environment.pluginsDir())) { - addPolicy.accept(PolicyUtil.getPluginPolicyInfo(plugin, environment.tmpDir())); - } - for (Path plugin : PluginsUtils.findPluginDirs(environment.modulesDir())) { - addPolicy.accept(PolicyUtil.getModulePolicyInfo(plugin, environment.tmpDir())); - } - - return Collections.unmodifiableMap(map); - } - - /** returns dynamic Permissions to configured paths and bind ports */ - static Permissions createPermissions(Environment environment, Path pidFile) throws IOException { - Permissions policy = new Permissions(); - addClasspathPermissions(policy); - addFilePermissions(policy, environment, pidFile); - addBindPermissions(policy, environment.settings()); - return policy; - } - - private static List createRecursiveDataPathPermission(Environment environment) throws IOException { - Permissions policy = new Permissions(); - for (Path path : environment.dataDirs()) { - addDirectoryPath(policy, Environment.PATH_DATA_SETTING.getKey(), path, "read,readlink,write,delete", true); - } - return toFilePermissions(policy); - } - - private static Map> readSecuredConfigFiles( - Environment environment, - Policy template, - Collection mainCodebases, - Map pluginPolicies - ) throws IOException { - Map> securedConfigFiles = new HashMap<>(); - Map> securedSettingKeys = new HashMap<>(); - - for (URL url : mainCodebases) { - for (Permission p : PolicyUtil.getPolicyPermissions(url, template, environment.tmpDir())) { - readSecuredConfigFilePermissions(environment, url, p, securedConfigFiles, securedSettingKeys); - } - } - - for (var pp : pluginPolicies.entrySet()) { - for (Permission p : PolicyUtil.getPolicyPermissions(pp.getKey(), pp.getValue(), environment.tmpDir())) { - readSecuredConfigFilePermissions(environment, pp.getKey(), p, securedConfigFiles, securedSettingKeys); - } - } - - // compile a Pattern for each setting key we'll be looking for - // the key could include a * wildcard - List>> settingPatterns = securedSettingKeys.entrySet() - .stream() - .map(e -> Map.entry(Pattern.compile(e.getKey()), e.getValue())) - .toList(); - - for (String setting : environment.settings().keySet()) { - for (Map.Entry> ps : settingPatterns) { - if (ps.getKey().matcher(setting).matches()) { - // add the setting value to the secured files for these codebase URLs - String settingValue = environment.settings().get(setting); - // Some settings can also be an HTTPS URL in addition to a file path; if that's the case just skip this one. - // If the setting shouldn't be an HTTPS URL, that'll be caught by that setting's validation later in the process. - // HTTP (no S) URLs are not supported. - if (settingValue.toLowerCase(Locale.ROOT).startsWith("https://") == false) { - Path file = environment.configDir().resolve(settingValue); - if (file.startsWith(environment.configDir()) == false) { - throw new IllegalStateException( - ps.getValue() + " tried to grant access to file outside config directory " + file - ); - } - if (logger.isDebugEnabled()) { - ps.getValue() - .forEach( - url -> logger.debug("Jar {} securing access to config file {} through setting {}", url, file, setting) - ); - } - securedConfigFiles.computeIfAbsent(file.toString(), k -> new HashSet<>()).addAll(ps.getValue()); - } - } - } - } - - // always add some config files as exclusive files that no one can access - // there's no reason for anyone to read these once the security manager is initialized - // so if something has tried to grant itself access, crash out with an error - addSpeciallySecuredConfigFile(securedConfigFiles, environment.configDir().resolve("elasticsearch.yml").toString()); - addSpeciallySecuredConfigFile(securedConfigFiles, environment.configDir().resolve("jvm.options").toString()); - addSpeciallySecuredConfigFile(securedConfigFiles, environment.configDir().resolve("jvm.options.d/-").toString()); - - return Collections.unmodifiableMap(securedConfigFiles); - } - - private static void readSecuredConfigFilePermissions( - Environment environment, - URL url, - Permission p, - Map> securedFiles, - Map> securedSettingKeys - ) { - String securedFileName = extractSecuredName(p, SecuredConfigFileAccessPermission.class); - if (securedFileName != null) { - Path securedFile = environment.configDir().resolve(securedFileName); - if (securedFile.startsWith(environment.configDir()) == false) { - throw new IllegalStateException("[" + url + "] tried to grant access to file outside config directory " + securedFile); - } - logger.debug("Jar {} securing access to config file {}", url, securedFile); - securedFiles.computeIfAbsent(securedFile.toString(), k -> new HashSet<>()).add(url); - } - - String securedKey = extractSecuredName(p, SecuredConfigFileSettingAccessPermission.class); - if (securedKey != null) { - securedSettingKeys.computeIfAbsent(securedKey, k -> new HashSet<>()).add(url); - } - } - - private static String extractSecuredName(Permission p, Class permissionType) { - if (permissionType.isInstance(p)) { - return p.getName(); - } else if (p instanceof UnresolvedPermission up && up.getUnresolvedType().equals(permissionType.getCanonicalName())) { - return up.getUnresolvedName(); - } else { - return null; - } - } - - private static void addSpeciallySecuredConfigFile(Map> securedFiles, String path) { - Set attemptedToGrant = securedFiles.put(path, Set.of()); - if (attemptedToGrant != null) { - throw new IllegalStateException(attemptedToGrant + " tried to grant access to special config file " + path); - } - } - - /** Adds access to classpath jars/classes for jar hell scan, etc */ - @SuppressForbidden(reason = "accesses fully qualified URLs to configure security") - static void addClasspathPermissions(Permissions policy) throws IOException { - // add permissions to everything in classpath - // really it should be covered by lib/, but there could be e.g. agents or similar configured) - for (URL url : JarHell.parseClassPath()) { - Path path; - try { - path = PathUtils.get(url.toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - // resource itself - if (Files.isDirectory(path)) { - addDirectoryPath(policy, "class.path", path, "read,readlink", false); - } else { - addSingleFilePath(policy, path, "read,readlink"); - } - } - } - - /** - * Adds access to all configurable paths. - */ - static void addFilePermissions(Permissions policy, Environment environment, Path pidFile) throws IOException { - // read-only dirs - addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.binDir(), "read,readlink", false); - addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.libDir(), "read,readlink", false); - addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.modulesDir(), "read,readlink", false); - addDirectoryPath(policy, Environment.PATH_HOME_SETTING.getKey(), environment.pluginsDir(), "read,readlink", false); - addDirectoryPath(policy, "path.conf", environment.configDir(), "read,readlink", false); - - // read-write dirs - addDirectoryPath(policy, "java.io.tmpdir", environment.tmpDir(), "read,readlink,write,delete", false); - addDirectoryPath(policy, Environment.PATH_LOGS_SETTING.getKey(), environment.logsDir(), "read,readlink,write,delete", false); - if (environment.sharedDataDir() != null) { - addDirectoryPath( - policy, - Environment.PATH_SHARED_DATA_SETTING.getKey(), - environment.sharedDataDir(), - "read,readlink,write,delete", - false - ); - } - final Set dataFilesPaths = new HashSet<>(); - for (Path path : environment.dataDirs()) { - addDirectoryPath(policy, Environment.PATH_DATA_SETTING.getKey(), path, "read,readlink,write,delete", false); - /* - * We have to do this after adding the path because a side effect of that is that the directory is created; the Path#toRealPath - * invocation will fail if the directory does not already exist. We use Path#toRealPath to follow symlinks and handle issues - * like unicode normalization or case-insensitivity on some filesystems (e.g., the case-insensitive variant of HFS+ on macOS). - */ - try { - final Path realPath = path.toRealPath(); - if (dataFilesPaths.add(realPath) == false) { - throw new IllegalStateException("path [" + realPath + "] is duplicated by [" + path + "]"); - } - } catch (final IOException e) { - throw new IllegalStateException("unable to access [" + path + "]", e); - } - } - for (Path path : environment.repoDirs()) { - addDirectoryPath(policy, Environment.PATH_REPO_SETTING.getKey(), path, "read,readlink,write,delete", false); - } - - if (pidFile != null) { - // we just need permission to remove the file if its elsewhere. - addSingleFilePath(policy, pidFile, "delete"); - } - // we need to touch the operator/settings.json file when restoring from snapshots, on some OSs it needs file write permission - addSingleFilePath(policy, environment.configDir().resolve(OPERATOR_DIRECTORY).resolve(SETTINGS_FILE_NAME), "read,readlink,write"); - } - - /** - * Add dynamic {@link SocketPermission}s based on HTTP and transport settings. - * - * @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to. - * @param settings the {@link Settings} instance to read the HTTP and transport settings from - */ - private static void addBindPermissions(Permissions policy, Settings settings) { - addSocketPermissionForHttp(policy, settings); - addSocketPermissionForTransportProfiles(policy, settings); - } - - /** - * Add dynamic {@link SocketPermission} based on HTTP settings. - * - * @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to. - * @param settings the {@link Settings} instance to read the HTTP settings from - */ - private static void addSocketPermissionForHttp(final Permissions policy, final Settings settings) { - // http is simple - final String httpRange = HttpTransportSettings.SETTING_HTTP_PORT.get(settings).getPortRangeString(); - addSocketPermissionForPortRange(policy, httpRange); - } - - /** - * Add dynamic {@link SocketPermission} based on transport settings. This method will first check if there is a port range specified in - * the transport profile specified by {@code profileSettings} and will fall back to {@code settings}. - * - * @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission}s to - * @param settings the {@link Settings} instance to read the transport settings from - */ - private static void addSocketPermissionForTransportProfiles(final Permissions policy, final Settings settings) { - // transport is way over-engineered - Set profiles = TcpTransport.getProfileSettings(settings); - Set uniquePortRanges = new HashSet<>(); - // loop through all profiles and add permissions for each one - for (final TcpTransport.ProfileSettings profile : profiles) { - if (uniquePortRanges.add(profile.portOrRange)) { - // profiles fall back to the transport.port if it's not explicit but we want to only add one permission per range - addSocketPermissionForPortRange(policy, profile.portOrRange); - } - } - } - - /** - * Add dynamic {@link SocketPermission} for the specified port range. - * - * @param policy the {@link Permissions} instance to apply the dynamic {@link SocketPermission} to. - * @param portRange the port range - */ - private static void addSocketPermissionForPortRange(final Permissions policy, final String portRange) { - // listen is always called with 'localhost' but use wildcard to be sure, no name service is consulted. - // see SocketPermission implies() code - policy.add(new SocketPermission("*:" + portRange, "listen,resolve")); - } - - /** - * Ensures configured directory {@code path} exists. - * @throws IOException if {@code path} exists, but is not a directory, not accessible, or broken symbolic link. - */ - static void ensureDirectoryExists(Path path) throws IOException { - // this isn't atomic, but neither is createDirectories. - if (Files.isDirectory(path)) { - // verify access, following links (throws exception if something is wrong) - // we only check READ as a sanity test - path.getFileSystem().provider().checkAccess(path.toRealPath(), AccessMode.READ); - } else { - // doesn't exist, or not a directory - try { - Files.createDirectories(path); - } catch (FileAlreadyExistsException e) { - // convert optional specific exception so the context is clear - IOException e2 = new NotDirectoryException(path.toString()); - e2.addSuppressed(e); - throw e2; - } - } - } - - /** Simple checks that everything is ok */ - @SuppressForbidden(reason = "accesses jvm default tempdir as a self-test") - static void selfTest() throws IOException { - // check we can manipulate temporary files - try { - Path p = Files.createTempFile(null, null); - try { - Files.delete(p); - } catch (IOException ignored) { - // potentially virus scanner - } - } catch (SecurityException problem) { - throw new SecurityException("Security misconfiguration: cannot access java.io.tmpdir", problem); - } - } - - /** - * Prepopulates the system's security manager callers map with this class as a caller. - * This is loathsome, but avoids the annoying warning message at run time. - * Returns true if the callers map has been populated. - */ - static boolean prepopulateSecurityCaller() { - Field f; - try { - f = getDeclaredField(Class.forName("java.lang.System$CallersHolder", true, null), "callers"); - } catch (NoSuchFieldException | ClassNotFoundException ignore) { - return false; - } - try { - Class c = Class.forName("sun.misc.Unsafe"); - MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(c, MethodHandles.lookup()); - VarHandle handle = lookup.findStaticVarHandle(c, "theUnsafe", c); - Object theUnsafe = handle.get(); - MethodHandle mh = lookup.findVirtual(c, "staticFieldBase", methodType(Object.class, Field.class)); - mh = mh.asType(mh.type().changeParameterType(0, Object.class)); - Object base = mh.invokeExact(theUnsafe, f); - mh = lookup.findVirtual(c, "staticFieldOffset", methodType(long.class, Field.class)); - mh = mh.asType(mh.type().changeParameterType(0, Object.class)); - long offset = (long) mh.invokeExact(theUnsafe, f); - mh = lookup.findVirtual(c, "getObject", methodType(Object.class, Object.class, long.class)); - mh = mh.asType(mh.type().changeParameterType(0, Object.class)); - Object callers = (Object) mh.invokeExact(theUnsafe, base, offset); - if (Map.class.isAssignableFrom(callers.getClass())) { - @SuppressWarnings("unchecked") - Map, Boolean> map = Map.class.cast(callers); - map.put(org.elasticsearch.bootstrap.Security.class, true); - return true; - } - } catch (Throwable t) { - throw new ElasticsearchException(t); - } - return false; - } - - @SuppressForbidden(reason = "access violation required") - private static Field getDeclaredField(Class c, String name) throws NoSuchFieldException { - return c.getDeclaredField(name); - } - - /** - * Assumes the given {@link Permissions} only contains {@link FilePermission} elements and returns them as - * a list. - * - * @param permissions permissions to unwrap - * @return list of file permissions found - */ - static List toFilePermissions(Permissions permissions) { - return permissions.elementsAsStream().map(p -> { - if (p instanceof FilePermission == false) { - throw new AssertionError("[" + p + "] was not a file permission"); - } - return (FilePermission) p; - }).toList(); - } -} diff --git a/server/src/test/java/org/elasticsearch/bootstrap/NoSecurityManagerTests.java b/server/src/test/java/org/elasticsearch/bootstrap/NoSecurityManagerTests.java deleted file mode 100644 index d7628be0d7f00..0000000000000 --- a/server/src/test/java/org/elasticsearch/bootstrap/NoSecurityManagerTests.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.bootstrap; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; - -import org.apache.lucene.tests.util.LuceneTestCase; -import org.elasticsearch.jdk.RuntimeVersionFeature; -import org.elasticsearch.test.GraalVMThreadsFilter; - -import static org.hamcrest.Matchers.is; - -@ThreadLeakFilters(filters = { GraalVMThreadsFilter.class }) -public class NoSecurityManagerTests extends LuceneTestCase { - - public void testPrepopulateSecurityCaller() { - assumeTrue("security manager must be available", RuntimeVersionFeature.isSecurityManagerAvailable()); - assumeTrue("Unexpected security manager:" + System.getSecurityManager(), System.getSecurityManager() == null); - boolean isAtLeastJava17 = Runtime.version().feature() >= 17; - boolean isPrepopulated = Security.prepopulateSecurityCaller(); - if (isAtLeastJava17) { - assertThat(isPrepopulated, is(true)); - } else { - assertThat(isPrepopulated, is(false)); - } - } -} diff --git a/server/src/test/java/org/elasticsearch/bootstrap/SecurityTests.java b/server/src/test/java/org/elasticsearch/bootstrap/SecurityTests.java deleted file mode 100644 index 98a1f577cfa3a..0000000000000 --- a/server/src/test/java/org/elasticsearch/bootstrap/SecurityTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.bootstrap; - -import org.elasticsearch.jdk.RuntimeVersionFeature; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public class SecurityTests extends ESTestCase { - - public void testEnsureExists() throws IOException { - Path p = createTempDir(); - - // directory exists - Path exists = p.resolve("exists"); - Files.createDirectory(exists); - Security.ensureDirectoryExists(exists); - Files.createTempFile(exists, null, null); - } - - public void testEnsureNotExists() throws IOException { - Path p = createTempDir(); - - // directory does not exist: create it - Path notExists = p.resolve("notexists"); - Security.ensureDirectoryExists(notExists); - Files.createTempFile(notExists, null, null); - } - - public void testEnsureRegularFile() throws IOException { - Path p = createTempDir(); - - // regular file - Path regularFile = p.resolve("regular"); - Files.createFile(regularFile); - try { - Security.ensureDirectoryExists(regularFile); - fail("didn't get expected exception"); - } catch (IOException expected) {} - } - - /** can't execute processes */ - public void testProcessExecution() throws Exception { - assumeTrue( - "test requires security manager", - RuntimeVersionFeature.isSecurityManagerAvailable() && System.getSecurityManager() != null - ); - try { - Runtime.getRuntime().exec("ls"); - fail("didn't get expected exception"); - } catch (SecurityException expected) {} - } -}