Allow running the Docker image with a non-default group (#61194)
Closes #60864. Tweak the JDK directories' permissions in the ES Docker image so that ES can run under a different user and group. These changes assume that the image is being run with bind-mounted config, data and logs directories, and reads and writes to these locations will still fail when both the UID and GID are not the default. Everything should be OK when running with the default GID of zero, however.
This commit is contained in:
parent
f0615113b6
commit
0d8d0f423c
|
@ -94,7 +94,10 @@ ENV PATH /usr/share/elasticsearch/bin:\$PATH
|
|||
|
||||
COPY bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
RUN chmod g=u /etc/passwd && \\
|
||||
# The JDK's directories' permissions don't allow `java` to be executed under a different
|
||||
# group to the default. Fix this.
|
||||
RUN find /usr/share/elasticsearch/jdk -type d -exec chmod 0755 '{}' \\; && \\
|
||||
chmod g=u /etc/passwd && \\
|
||||
chmod 0775 /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Ensure that there are no files with setuid or setgid, in order to mitigate "stackclash" attacks.
|
||||
|
|
|
@ -41,6 +41,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import static java.nio.file.attribute.PosixFilePermissions.fromString;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.elasticsearch.packaging.util.Docker.chownWithPrivilegeEscalation;
|
||||
import static org.elasticsearch.packaging.util.Docker.copyFromContainer;
|
||||
import static org.elasticsearch.packaging.util.Docker.existsInContainer;
|
||||
import static org.elasticsearch.packaging.util.Docker.getContainerLogs;
|
||||
|
@ -54,6 +55,7 @@ import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailu
|
|||
import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation;
|
||||
import static org.elasticsearch.packaging.util.Docker.waitForElasticsearch;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p600;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p644;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p660;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p775;
|
||||
import static org.elasticsearch.packaging.util.FileUtils.append;
|
||||
|
@ -174,8 +176,11 @@ public class DockerTests extends PackagingTestCase {
|
|||
final String jvmOptions = "-Xms512m\n-Xmx512m\n-Dlog4j2.disable.jmx=true\n";
|
||||
append(tempDir.resolve("jvm.options"), jvmOptions);
|
||||
|
||||
// Make the temp directory and contents accessible when bind-mounted
|
||||
// Make the temp directory and contents accessible when bind-mounted.
|
||||
Files.setPosixFilePermissions(tempDir, fromString("rwxrwxrwx"));
|
||||
// These permissions are necessary to run the tests under Vagrant
|
||||
Files.setPosixFilePermissions(tempDir.resolve("elasticsearch.yml"), p644);
|
||||
Files.setPosixFilePermissions(tempDir.resolve("log4j2.properties"), p644);
|
||||
|
||||
// Restart the container
|
||||
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/usr/share/elasticsearch/config"));
|
||||
|
@ -222,6 +227,41 @@ public class DockerTests extends PackagingTestCase {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that it is possible to run Elasticsearch under a different user and group to the default.
|
||||
*/
|
||||
public void test072RunEsAsDifferentUserAndGroup() throws Exception {
|
||||
assumeFalse(Platforms.WINDOWS);
|
||||
|
||||
final Path tempEsDataDir = tempDir.resolve("esDataDir");
|
||||
final Path tempEsConfigDir = tempDir.resolve("esConfDir");
|
||||
final Path tempEsLogsDir = tempDir.resolve("esLogsDir");
|
||||
|
||||
Files.createDirectory(tempEsConfigDir);
|
||||
Files.createDirectory(tempEsConfigDir.resolve("jvm.options.d"));
|
||||
Files.createDirectory(tempEsDataDir);
|
||||
Files.createDirectory(tempEsLogsDir);
|
||||
|
||||
copyFromContainer(installation.config("elasticsearch.yml"), tempEsConfigDir);
|
||||
copyFromContainer(installation.config("jvm.options"), tempEsConfigDir);
|
||||
copyFromContainer(installation.config("log4j2.properties"), tempEsConfigDir);
|
||||
|
||||
chownWithPrivilegeEscalation(tempEsConfigDir, "501:501");
|
||||
chownWithPrivilegeEscalation(tempEsDataDir, "501:501");
|
||||
chownWithPrivilegeEscalation(tempEsLogsDir, "501:501");
|
||||
|
||||
// Define the bind mounts
|
||||
final Map<Path, Path> volumes = new HashMap<>();
|
||||
volumes.put(tempEsDataDir.toAbsolutePath(), installation.data);
|
||||
volumes.put(tempEsConfigDir.toAbsolutePath(), installation.config);
|
||||
volumes.put(tempEsLogsDir.toAbsolutePath(), installation.logs);
|
||||
|
||||
// Restart the container
|
||||
runContainer(distribution(), volumes, null, 501, 501);
|
||||
|
||||
waitForElasticsearch(installation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the elastic user's password can be configured via a file and the ELASTIC_PASSWORD_FILE environment variable.
|
||||
*/
|
||||
|
@ -242,6 +282,8 @@ public class DockerTests extends PackagingTestCase {
|
|||
// File permissions need to be secured in order for the ES wrapper to accept
|
||||
// them for populating env var values
|
||||
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p600);
|
||||
// But when running in Vagrant, also ensure ES can actually access the file
|
||||
chownWithPrivilegeEscalation(tempDir.resolve(passwordFilename), "1000:0");
|
||||
|
||||
final Map<Path, Path> volumes = singletonMap(tempDir, Paths.get("/run/secrets"));
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import java.util.Set;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import static java.nio.file.attribute.PosixFilePermissions.fromString;
|
||||
import static org.elasticsearch.packaging.util.FileExistenceMatchers.fileExists;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p644;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p660;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p755;
|
||||
|
@ -106,7 +107,26 @@ public class Docker {
|
|||
* @param envVars environment variables to set when running the container, or null
|
||||
*/
|
||||
public static Installation runContainer(Distribution distribution, Map<Path, Path> volumes, Map<String, String> envVars) {
|
||||
executeDockerRun(distribution, volumes, envVars);
|
||||
return runContainer(distribution, volumes, envVars, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an Elasticsearch Docker container, with options for overriding the config directory
|
||||
* through a bind mount, and passing additional environment variables.
|
||||
* @param distribution details about the docker image being tested.
|
||||
* @param volumes a map that declares any volume mappings to apply, or null
|
||||
* @param envVars environment variables to set when running the container, or null
|
||||
* @param uid optional UID to run the container under
|
||||
* @param gid optional GID to run the container under
|
||||
*/
|
||||
public static Installation runContainer(
|
||||
Distribution distribution,
|
||||
Map<Path, Path> volumes,
|
||||
Map<String, String> envVars,
|
||||
Integer uid,
|
||||
Integer gid
|
||||
) {
|
||||
executeDockerRun(distribution, volumes, envVars, uid, gid);
|
||||
|
||||
waitForElasticsearchToStart();
|
||||
|
||||
|
@ -127,14 +147,20 @@ public class Docker {
|
|||
Map<Path, Path> volumes,
|
||||
Map<String, String> envVars
|
||||
) {
|
||||
executeDockerRun(distribution, volumes, envVars);
|
||||
executeDockerRun(distribution, volumes, envVars, null, null);
|
||||
|
||||
waitForElasticsearchToExit();
|
||||
|
||||
return getContainerLogs();
|
||||
}
|
||||
|
||||
private static void executeDockerRun(Distribution distribution, Map<Path, Path> volumes, Map<String, String> envVars) {
|
||||
private static void executeDockerRun(
|
||||
Distribution distribution,
|
||||
Map<Path, Path> volumes,
|
||||
Map<String, String> envVars,
|
||||
Integer uid,
|
||||
Integer gid
|
||||
) {
|
||||
removeContainer();
|
||||
|
||||
final List<String> args = new ArrayList<>();
|
||||
|
@ -157,7 +183,32 @@ public class Docker {
|
|||
|
||||
// Bind-mount any volumes
|
||||
if (volumes != null) {
|
||||
volumes.forEach((localPath, containerPath) -> args.add("--volume \"" + localPath + ":" + containerPath + "\""));
|
||||
volumes.forEach((localPath, containerPath) -> {
|
||||
assertThat(localPath, fileExists());
|
||||
|
||||
if (Platforms.WINDOWS == false && System.getProperty("user.name").equals("root") && uid == null) {
|
||||
// The tests are running as root, but the process in the Docker container runs as `elasticsearch` (UID 1000),
|
||||
// so we need to ensure that the container process is able to read the bind-mounted files.
|
||||
//
|
||||
// NOTE that we don't do this if a UID is specified - in that case, we assume that the caller knows
|
||||
// what they're doing!
|
||||
sh.run("chown -R 1000:0 " + localPath);
|
||||
}
|
||||
args.add("--volume \"" + localPath + ":" + containerPath + "\"");
|
||||
});
|
||||
}
|
||||
|
||||
if (uid == null) {
|
||||
if (gid != null) {
|
||||
throw new IllegalArgumentException("Cannot override GID without also overriding UID");
|
||||
}
|
||||
} else {
|
||||
args.add("--user");
|
||||
if (gid != null) {
|
||||
args.add(uid + ":" + gid);
|
||||
} else {
|
||||
args.add(uid.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Image name
|
||||
|
@ -363,12 +414,30 @@ public class Docker {
|
|||
*/
|
||||
public static void rmDirWithPrivilegeEscalation(Path localPath) {
|
||||
final Path containerBasePath = Paths.get("/mount");
|
||||
final Path containerPath = containerBasePath.resolve(Paths.get("/").relativize(localPath));
|
||||
final Path containerPath = containerBasePath.resolve(localPath.getParent().getFileName());
|
||||
final List<String> args = new ArrayList<>();
|
||||
|
||||
args.add("cd " + containerBasePath.toAbsolutePath());
|
||||
args.add("&&");
|
||||
args.add("rm -rf " + localPath.getFileName());
|
||||
args.add("rm -r " + localPath.getFileName());
|
||||
final String command = String.join(" ", args);
|
||||
executePrivilegeEscalatedShellCmd(command, localPath, containerPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the ownership of a path using Docker backed privilege escalation.
|
||||
* @param localPath The path to the file or directory to change.
|
||||
* @param ownership the ownership to apply. Can either be just the user, or the user and group, separated by a colon (":"),
|
||||
* or just the group if prefixed with a colon.
|
||||
*/
|
||||
public static void chownWithPrivilegeEscalation(Path localPath, String ownership) {
|
||||
final Path containerBasePath = Paths.get("/mount");
|
||||
final Path containerPath = containerBasePath.resolve(localPath.getParent().getFileName());
|
||||
final List<String> args = new ArrayList<>();
|
||||
|
||||
args.add("cd " + containerBasePath.toAbsolutePath());
|
||||
args.add("&&");
|
||||
args.add("chown -R " + ownership + " " + localPath.getFileName());
|
||||
final String command = String.join(" ", args);
|
||||
executePrivilegeEscalatedShellCmd(command, localPath, containerPath);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue