Clarify gid used by docker image process and bind-mount method (#49632)

Fix reference about the uid:gid that Elasticsearch runs as inside
the Docker container and add a packaging test to ensure that bind
mounting a data dir with a random uid and gid:0 works as
expected.

Backport of #49529
Closes #47929
This commit is contained in:
Dimitrios Liappis 2019-11-27 13:42:54 +02:00 committed by GitHub
parent 502873b144
commit 4b6915ea41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 45 deletions

View File

@ -196,7 +196,7 @@ sudo sysctl -w vm.max_map_count=262144
===== Configuration files must be readable by the `elasticsearch` user
By default, {es} runs inside the container as user `elasticsearch` using
uid:gid `1000:1000`.
uid:gid `1000:0`.
IMPORTANT: One exception is https://docs.openshift.com/container-platform/3.6/creating_images/guidelines.html#openshift-specific-guidelines[Openshift],
which runs containers using an arbitrarily assigned user ID.
@ -204,7 +204,7 @@ Openshift presents persistent volumes with the gid set to `0`, which works witho
If you are bind-mounting a local directory or file, it must be readable by the `elasticsearch` user.
In addition, this user must have write access to the <<path-settings,data and log dirs>>.
A good strategy is to grant group access to gid `1000` or `0` for the local directory.
A good strategy is to grant group access to gid `0` for the local directory.
For example, to prepare a local directory for storing data through a bind-mount:
@ -212,7 +212,7 @@ For example, to prepare a local directory for storing data through a bind-mount:
--------------------------------------------
mkdir esdatadir
chmod g+rwx esdatadir
chgrp 1000 esdatadir
chgrp 0 esdatadir
--------------------------------------------
As a last resort, you can force the container to mutate the ownership of
@ -351,7 +351,7 @@ For example, to bind-mount `custom_elasticsearch.yml` with `docker run`, specify
--------------------------------------------
IMPORTANT: The container **runs {es} as user `elasticsearch` using
**uid:gid `1000:1000`**. Bind mounted host directories and files must be accessible by this user,
uid:gid `1000:0`**. Bind mounted host directories and files must be accessible by this user,
and the data and log directories must be writable by this user.
[[_c_customized_image]]

View File

@ -43,7 +43,9 @@ import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwners
import static org.elasticsearch.packaging.util.Docker.copyFromContainer;
import static org.elasticsearch.packaging.util.Docker.ensureImageIsLoaded;
import static org.elasticsearch.packaging.util.Docker.existsInContainer;
import static org.elasticsearch.packaging.util.Docker.mkDirWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.removeContainer;
import static org.elasticsearch.packaging.util.Docker.rmDirWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.runContainer;
import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailure;
import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation;
@ -181,6 +183,27 @@ public class DockerTests extends PackagingTestCase {
assertThat(nodesResponse, containsString("\"using_compressed_ordinary_object_pointers\":\"false\""));
}
/**
* Check that the default config can be overridden using a bind mount, and that env vars are respected
*/
public void test71BindMountCustomPathWithDifferentUID() throws Exception {
final Path tempEsDataDir = tempDir.resolve("esDataDir");
// Make the local directory and contents accessible when bind-mounted
mkDirWithPrivilegeEscalation(tempEsDataDir, 1500, 0);
// Restart the container
final Map<Path, Path> volumes = singletonMap(tempEsDataDir.toAbsolutePath(), Paths.get("/usr/share/elasticsearch/data"));
runContainer(distribution(), volumes, null);
waitForElasticsearch(installation);
final String nodesResponse = makeRequest(Request.Get("http://localhost:9200/_nodes"));
assertThat(nodesResponse, containsString("\"_nodes\":{\"total\":1,\"successful\":1,\"failed\":0}"));
rmDirWithPrivilegeEscalation(tempEsDataDir);
}
/**
* Check that environment variables can be populated by setting variables with the suffix "_FILE",
* which point to files that hold the required values.

View File

@ -24,6 +24,8 @@ import org.apache.commons.logging.LogFactory;
import org.elasticsearch.common.CheckedRunnable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.List;
@ -35,6 +37,7 @@ import static java.nio.file.attribute.PosixFilePermissions.fromString;
import static org.elasticsearch.packaging.util.FileMatcher.p644;
import static org.elasticsearch.packaging.util.FileMatcher.p660;
import static org.elasticsearch.packaging.util.FileMatcher.p755;
import static org.elasticsearch.packaging.util.FileMatcher.p770;
import static org.elasticsearch.packaging.util.FileMatcher.p775;
import static org.elasticsearch.packaging.util.FileUtils.getCurrentVersion;
import static org.hamcrest.CoreMatchers.containsString;
@ -281,6 +284,76 @@ public class Docker {
return result.isSuccess();
}
/**
* Run privilege escalated shell command on the local file system via a bind mount inside a Docker container.
* @param shellCmd The shell command to execute on the localPath e.g. `mkdir /containerPath/dir`.
* @param localPath The local path where shellCmd will be executed on (inside a container).
* @param containerPath The path to mount localPath inside the container.
*/
private static void executePrivilegeEscalatedShellCmd(String shellCmd, Path localPath, Path containerPath) {
final List<String> args = new ArrayList<>();
args.add("docker run");
// Don't leave orphaned containers
args.add("--rm");
// Mount localPath to a known location inside the container, so that we can execute shell commands on it later
args.add("--volume \"" + localPath.getParent() + ":" + containerPath.getParent() + "\"");
// Use a lightweight musl libc based small image
args.add("alpine");
// And run inline commands via the POSIX shell
args.add("/bin/sh -c \"" + shellCmd + "\"");
final String command = String.join(" ", args);
logger.info("Running command: " + command);
sh.run(command);
}
/**
* Create a directory with specified uid/gid using Docker backed privilege escalation.
* @param localPath The path to the directory to create.
* @param uid The numeric id for localPath
* @param gid The numeric id for localPath
*/
public static void mkDirWithPrivilegeEscalation(Path localPath, int uid, int gid) {
final Path containerBasePath = Paths.get("/mount");
final Path containerPath = containerBasePath.resolve(Paths.get("/").relativize(localPath));
final List<String> args = new ArrayList<>();
args.add("mkdir " + containerPath.toAbsolutePath());
args.add("&&");
args.add("chown " + uid + ":" + gid + " " + containerPath.toAbsolutePath());
args.add("&&");
args.add("chmod 0770 " + containerPath.toAbsolutePath());
final String command = String.join(" ", args);
executePrivilegeEscalatedShellCmd(command, localPath, containerPath);
final PosixFileAttributes dirAttributes = FileUtils.getPosixFileAttributes(localPath);
final Map<String, Integer> numericPathOwnership = FileUtils.getNumericUnixPathOwnership(localPath);
assertEquals(localPath + " has wrong uid", numericPathOwnership.get("uid").intValue(), uid);
assertEquals(localPath + " has wrong gid", numericPathOwnership.get("gid").intValue(), gid);
assertEquals(localPath + " has wrong permissions", dirAttributes.permissions(), p770);
}
/**
* Delete a directory using Docker backed privilege escalation.
* @param localPath The path to the directory to delete.
*/
public static void rmDirWithPrivilegeEscalation(Path localPath) {
final Path containerBasePath = Paths.get("/mount");
final Path containerPath = containerBasePath.resolve(Paths.get("/").relativize(localPath));
final List<String> args = new ArrayList<>();
args.add("cd " + containerBasePath.toAbsolutePath());
args.add("&&");
args.add("rm -rf " + localPath.getFileName());
final String command = String.join(" ", args);
executePrivilegeEscalatedShellCmd(command, localPath, containerPath);
}
/**
* Checks that the specified path's permissions and ownership match those specified.
*/

View File

@ -46,6 +46,7 @@ public class FileMatcher extends TypeSafeMatcher<Path> {
public enum Fileness { File, Directory }
public static final Set<PosixFilePermission> p775 = fromString("rwxrwxr-x");
public static final Set<PosixFilePermission> p770 = fromString("rwxrwx---");
public static final Set<PosixFilePermission> p755 = fromString("rwxr-xr-x");
public static final Set<PosixFilePermission> p750 = fromString("rwxr-x---");
public static final Set<PosixFilePermission> p660 = fromString("rw-rw----");

View File

@ -33,6 +33,7 @@ import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
@ -42,7 +43,9 @@ import java.nio.file.attribute.PosixFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@ -240,6 +243,23 @@ public class FileUtils {
}
}
/**
* Gets numeric ownership attributes that are supported by Unix filesystems
* @return a Map of the uid/gid integer values
*/
public static Map<String, Integer> getNumericUnixPathOwnership(Path path) {
Map<String, Integer> numericPathOwnership = new HashMap<>();
try {
numericPathOwnership.put("uid", (int) Files.getAttribute(path, "unix:uid", LinkOption.NOFOLLOW_LINKS));
numericPathOwnership.put("gid", (int) Files.getAttribute(path, "unix:gid", LinkOption.NOFOLLOW_LINKS));
} catch (IOException e) {
throw new RuntimeException(e);
}
return numericPathOwnership;
}
// vagrant creates /tmp for us in windows so we use that to avoid long paths
public static Path getTempDir() {
return Paths.get("/tmp");