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:
Rory Hunter 2020-08-24 11:04:38 +01:00 committed by Rory Hunter
parent f0615113b6
commit 0d8d0f423c
3 changed files with 122 additions and 8 deletions

View File

@ -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.

View File

@ -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"));

View File

@ -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);
}