Skip to content

Commit

Permalink
Allow running the Docker image with a non-default group (#61194)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pugnascotia committed Aug 24, 2020
1 parent f061511 commit 0d8d0f4
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 8 deletions.
5 changes: 4 additions & 1 deletion distribution/docker/src/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

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;
Expand All @@ -54,6 +55,7 @@
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;
Expand Down Expand Up @@ -174,8 +176,11 @@ public void test070BindMountCustomPathConfAndJvmOptions() throws Exception {
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"));
Expand Down Expand Up @@ -222,6 +227,41 @@ public void test071BindMountCustomPathWithDifferentUID() throws Exception {
});
}

/**
* 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.
*/
Expand All @@ -242,6 +282,8 @@ public void test080ConfigurePasswordThroughEnvironmentVariableFile() throws Exce
// 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"));

Expand Down
81 changes: 75 additions & 6 deletions qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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;
Expand Down Expand Up @@ -106,7 +107,26 @@ public static Installation runContainer(Distribution distribution) {
* @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();

Expand All @@ -127,14 +147,20 @@ public static Shell.Result runContainerExpectingFailure(
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<>();
Expand All @@ -157,7 +183,32 @@ private static void executeDockerRun(Distribution distribution, Map<Path, Path>

// 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
Expand Down Expand Up @@ -363,12 +414,30 @@ public static void mkDirWithPrivilegeEscalation(Path localPath, int uid, int gid
*/
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 -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("rm -rf " + localPath.getFileName());
args.add("chown -R " + ownership + " " + localPath.getFileName());
final String command = String.join(" ", args);
executePrivilegeEscalatedShellCmd(command, localPath, containerPath);
}
Expand Down

0 comments on commit 0d8d0f4

Please sign in to comment.