diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile index 5673801bd00..e671d39a4d8 100644 --- a/distribution/docker/src/docker/Dockerfile +++ b/distribution/docker/src/docker/Dockerfile @@ -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. diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index d3795fcdf5f..9934ba80b4a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -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 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 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 volumes = singletonMap(tempDir, Paths.get("/run/secrets")); diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 62efe895c5e..b11e8f46e4a 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -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 volumes, Map 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 volumes, + Map envVars, + Integer uid, + Integer gid + ) { + executeDockerRun(distribution, volumes, envVars, uid, gid); waitForElasticsearchToStart(); @@ -127,14 +147,20 @@ public class Docker { Map volumes, Map envVars ) { - executeDockerRun(distribution, volumes, envVars); + executeDockerRun(distribution, volumes, envVars, null, null); waitForElasticsearchToExit(); return getContainerLogs(); } - private static void executeDockerRun(Distribution distribution, Map volumes, Map envVars) { + private static void executeDockerRun( + Distribution distribution, + Map volumes, + Map envVars, + Integer uid, + Integer gid + ) { removeContainer(); final List 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 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 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); }