Add exclusive access files for security module (#123676)

This commit fills out missing entitlements for the security module.
Specifically they are config files which require exclusive access.
This commit is contained in:
Ryan Ernst 2025-03-08 07:02:36 -08:00 committed by GitHub
parent f15cc9667b
commit 7e1195dc9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 115 additions and 90 deletions

View file

@ -23,8 +23,12 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import static java.util.Comparator.comparing;
@ -42,27 +46,47 @@ public final class FileAccessTree {
/**
* An intermediary structure to help globally validate exclusive paths, and then build exclusive paths for individual modules.
*/
record ExclusivePath(String componentName, String moduleName, String path) {
record ExclusivePath(String componentName, Set<String> moduleNames, String path) {
@Override
public String toString() {
return "[[" + componentName + "] [" + moduleName + "] [" + path + "]]";
return "[[" + componentName + "] " + moduleNames + " [" + path + "]]";
}
}
static List<ExclusivePath> buildExclusivePathList(List<ExclusiveFileEntitlement> exclusiveFileEntitlements, PathLookup pathLookup) {
List<ExclusivePath> exclusivePaths = new ArrayList<>();
Map<String, ExclusivePath> exclusivePaths = new LinkedHashMap<>();
for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) {
for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) {
if (fd.exclusive()) {
List<Path> paths = fd.resolvePaths(pathLookup).toList();
for (Path path : paths) {
exclusivePaths.add(new ExclusivePath(efe.componentName(), efe.moduleName(), normalizePath(path)));
String normalizedPath = normalizePath(path);
var exclusivePath = exclusivePaths.computeIfAbsent(
normalizedPath,
k -> new ExclusivePath(efe.componentName(), new HashSet<>(), normalizedPath)
);
if (exclusivePath.componentName().equals(efe.componentName()) == false) {
throw new IllegalArgumentException(
"Path ["
+ normalizedPath
+ "] is already exclusive to ["
+ exclusivePath.componentName()
+ "]"
+ exclusivePath.moduleNames
+ ", cannot add exclusive access for ["
+ efe.componentName()
+ "]["
+ efe.moduleName
+ "]"
);
}
exclusivePath.moduleNames.add(efe.moduleName());
}
}
}
}
return exclusivePaths.stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList();
return exclusivePaths.values().stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList();
}
static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
@ -97,7 +121,7 @@ public final class FileAccessTree {
) {
List<String> updatedExclusivePaths = new ArrayList<>();
for (ExclusivePath exclusivePath : exclusivePaths) {
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleName().equals(moduleName) == false) {
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) {
updatedExclusivePaths.add(exclusivePath.path());
}
}

View file

@ -164,7 +164,8 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
Stream<String> result = pathLookup.settingResolver()
.apply(setting)
.filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false);
.filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false)
.distinct();
return result.map(Path::of);
}

View file

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList;
@ -386,7 +387,7 @@ public class FileAccessTreeTests extends ESTestCase {
original.moduleName(),
new FilesEntitlement(List.of(originalFileData.withPlatform(WINDOWS)))
);
var originalExclusivePath = new ExclusivePath("component1", "module1", normalizePath(path("/a/b")));
var originalExclusivePath = new ExclusivePath("component1", Set.of("module1"), normalizePath(path("/a/b")));
// Some basic tests
@ -406,27 +407,14 @@ public class FileAccessTreeTests extends ESTestCase {
var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath);
var distinctPaths = List.of(
originalExclusivePath,
new ExclusivePath("component2", original.moduleName(), originalExclusivePath.path()),
new ExclusivePath(original.componentName(), "module2", originalExclusivePath.path()),
new ExclusivePath(original.componentName(), original.moduleName(), normalizePath(path("/c/d")))
new ExclusivePath("component2", Set.of(original.moduleName()), originalExclusivePath.path()),
new ExclusivePath(original.componentName(), Set.of("module2"), originalExclusivePath.path()),
new ExclusivePath(original.componentName(), Set.of(original.moduleName()), normalizePath(path("/c/d")))
);
assertEquals(
"Distinct elements should not be combined",
distinctPaths,
buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP)
);
// Do merge things we should
List<ExclusiveFileEntitlement> interleavedEntitlements = new ArrayList<>();
distinctEntitlements.forEach(e -> {
interleavedEntitlements.add(e);
interleavedEntitlements.add(original);
});
assertEquals(
"Identical elements should be combined wherever they are in the list",
distinctPaths,
buildExclusivePathList(interleavedEntitlements, TEST_PATH_LOOKUP)
var iae = expectThrows(IllegalArgumentException.class, () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP));
assertThat(
iae.getMessage(),
equalTo("Path [/a/b] is already exclusive to [component1][module1], cannot add exclusive access for [component2][module1]")
);
var equivalentEntitlements = List.of(original, differentMode, differentPlatform);
@ -486,7 +474,7 @@ public class FileAccessTreeTests extends ESTestCase {
static List<ExclusivePath> exclusivePaths(String componentName, String moduleName, String... paths) {
List<ExclusivePath> exclusivePaths = new ArrayList<>();
for (String path : paths) {
exclusivePaths.add(new ExclusivePath(componentName, moduleName, normalizePath(path(path))));
exclusivePaths.add(new ExclusivePath(componentName, Set.of(moduleName), normalizePath(path(path))));
}
return exclusivePaths;
}

View file

@ -38,6 +38,7 @@ import static java.util.Map.entry;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.SERVER_COMPONENT_NAME;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
@ -444,9 +445,9 @@ public class PolicyManagerTests extends ESTestCase {
}
public void testFilesEntitlementsWithExclusive() {
var baseTestPath = Path.of("/tmp").toAbsolutePath();
var testPath1 = Path.of("/tmp/test").toAbsolutePath();
var testPath2 = Path.of("/tmp/test/foo").toAbsolutePath();
var baseTestPath = Path.of("/base").toAbsolutePath();
var testPath1 = Path.of("/base/test").toAbsolutePath();
var testPath2 = Path.of("/base/test/foo").toAbsolutePath();
var iae = expectThrows(
IllegalArgumentException.class,
() -> new PolicyManager(
@ -458,7 +459,7 @@ public class PolicyManagerTests extends ESTestCase {
"test",
List.of(
new Scope(
"test",
"test.module1",
List.of(
new FilesEntitlement(
List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true))
@ -472,7 +473,7 @@ public class PolicyManagerTests extends ESTestCase {
"test",
List.of(
new Scope(
"test",
"test.module2",
List.of(
new FilesEntitlement(
List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true))
@ -490,8 +491,13 @@ public class PolicyManagerTests extends ESTestCase {
Set.of()
)
);
assertTrue(iae.getMessage().contains("duplicate/overlapping exclusive paths found in files entitlements:"));
assertTrue(iae.getMessage().contains(Strings.format("[test] [%s]]", testPath1.toString())));
assertThat(
iae.getMessage(),
equalTo(
"Path [/base/test] is already exclusive to [plugin1][test.module1],"
+ " cannot add exclusive access for [plugin2][test.module2]"
)
);
iae = expectThrows(
IllegalArgumentException.class,

View file

@ -10,11 +10,9 @@
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;
import java.security.AccessControlException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
@ -126,10 +124,8 @@ public final class PemKeyConfig implements SslKeyConfig {
throw new SslConfigException("could not load ssl private key file [" + path + "]");
}
return privateKey;
} catch (AccessControlException e) {
} catch (SecurityException 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) {
@ -140,7 +136,7 @@ public final class PemKeyConfig implements SslKeyConfig {
private List<Certificate> getCertificates(Path path) {
try {
return PemUtils.readCertificates(Collections.singleton(path));
} catch (AccessControlException e) {
} catch (SecurityException e) {
throw SslFileUtil.accessControlFailure(CERT_FILE_TYPE, List.of(path), e, configBasePath);
} catch (IOException e) {
throw SslFileUtil.ioException(CERT_FILE_TYPE, List.of(path), e);

View file

@ -9,12 +9,9 @@
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;
import java.security.AccessControlException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.Certificate;
@ -99,10 +96,8 @@ public final class PemTrustConfig implements SslTrustConfig {
private List<Certificate> readCertificates(List<Path> paths) {
try {
return PemUtils.readCertificates(paths);
} catch (AccessControlException e) {
} catch (SecurityException 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,7 +10,6 @@
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;
@ -19,7 +18,6 @@ import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessControlException;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
@ -111,10 +109,8 @@ public final class PemUtils {
throw new SslConfigException("could not load ssl private key file [" + path + "]");
}
return privateKey;
} catch (AccessControlException e) {
} catch (SecurityException 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

@ -16,7 +16,6 @@ import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.AccessControlException;
import java.security.GeneralSecurityException;
import java.security.UnrecoverableKeyException;
import java.util.List;
@ -84,7 +83,7 @@ final class SslFileUtil {
return innerAccessControlFailure(fileType, paths, cause, basePath);
}
static SslConfigException accessControlFailure(String fileType, List<Path> paths, AccessControlException cause, Path basePath) {
static SslConfigException accessControlFailure(String fileType, List<Path> paths, SecurityException cause, Path basePath) {
return innerAccessControlFailure(fileType, paths, cause, basePath);
}

View file

@ -11,11 +11,9 @@ 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;
import java.security.AccessControlException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@ -167,10 +165,8 @@ public class StoreKeyConfig implements SslKeyConfig {
private KeyStore readKeyStore(Path path) {
try {
return KeyStoreUtil.readKeyStore(path, type, storePassword);
} catch (AccessControlException e) {
} catch (SecurityException 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,11 +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;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
@ -95,10 +92,8 @@ public final class StoreTrustConfig implements SslTrustConfig {
private KeyStore readKeyStore(Path path) {
try {
return KeyStoreUtil.readKeyStore(path, type, password);
} catch (AccessControlException e) {
} catch (SecurityException 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

@ -64,7 +64,6 @@ import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -247,7 +246,7 @@ public class Analysis {
} catch (IOException ioe) {
String message = Strings.format("IOException while reading %s: %s", settingPath, path);
throw new IllegalArgumentException(message, ioe);
} catch (AccessControlException ace) {
} catch (SecurityException ace) {
throw new IllegalArgumentException(Strings.format("Access denied trying to read file %s: %s", settingPath, path), ace);
}
}

View file

@ -20,7 +20,6 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.AccessControlException;
import java.util.Arrays;
import java.util.stream.StreamSupport;
@ -256,7 +255,7 @@ public class FileWatcher extends AbstractResourceWatcher<FileChangesListener> {
FileObserver child = new FileObserver(file);
child.init(initial);
return child;
} catch (AccessControlException e) {
} catch (SecurityException e) {
// don't have permissions, use a placeholder
logger.debug(() -> Strings.format("Don't have permissions to watch path [%s]", file), e);
return new DeniedObserver(file);

View file

@ -45,7 +45,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.AccessControlException;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Predicate;
@ -259,7 +258,7 @@ public class StoreRecoveryTests extends ESTestCase {
BasicFileAttributes sourceAttr = Files.readAttributes(path.resolve("foo.bar"), BasicFileAttributes.class);
// we won't get here - no permission ;)
return destAttr.fileKey() != null && destAttr.fileKey().equals(sourceAttr.fileKey());
} catch (AccessControlException ex) {
} catch (SecurityException ex) {
return true; // if we run into that situation we know it's supported.
} catch (UnsupportedOperationException ex) {
return false;

View file

@ -10,6 +10,7 @@ package org.elasticsearch.xpack.security;
import org.elasticsearch.watcher.FileWatcher;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
@ -34,6 +35,15 @@ public class PrivilegedFileWatcher extends FileWatcher {
super(path);
}
public PrivilegedFileWatcher(Path path, boolean checkFileContents) {
super(path, checkFileContents);
}
@Override
protected InputStream newInputStream(Path path) throws IOException {
return Files.newInputStream(path);
}
@Override
protected boolean fileExists(Path path) {
return doPrivileged((PrivilegedAction<Boolean>) () -> Files.exists(path));

View file

@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
import org.elasticsearch.xpack.core.security.support.Validation;
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
import org.elasticsearch.xpack.security.support.SecurityFiles;
import java.io.IOException;
@ -57,7 +58,7 @@ public class FileUserRolesStore {
file = resolveFile(config.env());
userRoles = parseFileLenient(file, logger);
listeners = new CopyOnWriteArrayList<>(Collections.singletonList(listener));
FileWatcher watcher = new FileWatcher(file.getParent());
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent());
watcher.addListener(new FileListener());
try {
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);

View file

@ -24,6 +24,7 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
import org.elasticsearch.xpack.security.support.FileLineParser;
@ -59,7 +60,7 @@ public class FileServiceAccountTokenStore extends CachingServiceAccountTokenStor
super(env.settings(), threadPool);
this.clusterService = clusterService;
file = resolveFile(env);
FileWatcher watcher = new FileWatcher(file.getParent());
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent());
watcher.addListener(new FileReloadListener(file, this::tryReload));
try {
resourceWatcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);

View file

@ -37,6 +37,7 @@ import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator;
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
import org.elasticsearch.xpack.core.security.support.Validation;
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
import org.elasticsearch.xpack.security.authz.FileRoleValidator;
import java.io.IOException;
@ -110,7 +111,7 @@ public class FileRolesStore implements BiConsumer<Set<String>, ActionListener<Ro
}
this.licenseState = licenseState;
this.xContentRegistry = xContentRegistry;
FileWatcher watcher = new FileWatcher(file.getParent());
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent());
watcher.addListener(new FileListener());
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry, roleValidator);

View file

@ -31,6 +31,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings;
import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings;
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import java.io.IOException;
@ -59,7 +60,7 @@ public class FileOperatorUsersStore {
public FileOperatorUsersStore(Environment env, ResourceWatcherService watcherService) {
this.file = XPackPlugin.resolveConfigFile(env, "operator_users.yml");
this.operatorUsersDescriptor = parseFile(this.file, logger);
FileWatcher watcher = new FileWatcher(file.getParent(), true);
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent(), true);
watcher.addListener(new FileOperatorUsersStore.FileListener());
try {
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);

View file

@ -4,6 +4,35 @@ org.elasticsearch.security:
- relative_path: ""
relative_to: config
mode: read
- relative_path: users
relative_to: config
mode: read
exclusive: true
- relative_path: x-pack/users
relative_to: config
mode: read
exclusive: true
- path_setting: xpack.security.authc.realms.ldap.*.files.role_mapping
basedir_if_relative: config
mode: read
exclusive: true
- path_setting: xpack.security.authc.realms.pki.*.files.role_mapping
basedir_if_relative: config
mode: read
exclusive: true
- path_setting: xpack.security.authc.realms.kerberos.*.keytab.path
basedir_if_relative: config
mode: read
exclusive: true
- path_setting: xpack.security.authc.realms.jwt.*.pkc_jwkset_path
basedir_if_relative: config
mode: read
exclusive: true
- path_setting: xpack.security.authc.realms.saml.*.idp.metadata.path
basedir_if_relative: config
mode: read
exclusive: true
io.netty.transport:
- manage_threads
- inbound_network
@ -25,18 +54,7 @@ org.opensaml.xmlsec.impl:
- org.apache.xml.security.ignoreLineBreaks
org.opensaml.saml.impl:
- files:
- relative_path: idp-docs-metadata.xml
relative_to: config
mode: read
- relative_path: idp-metadata.xml
relative_to: config
mode: read
- relative_path: saml-metadata.xml
relative_to: config
mode: read
- relative_path: metadata.xml
relative_to: config
mode: read
- relative_path: "saml/"
relative_to: config
- path_setting: xpack.security.authc.realms.saml.*.idp.metadata.path
basedir_if_relative: config
mode: read
exclusive: true