Skip to content

Commit 8a6d68b

Browse files
authored
Make the Docker build more re-usable in Cloud (elastic#50277)
Closes elastic#49926 and elastic#46166. Rework the Docker image so that it comes with a tiny init system, to ensure ML processes are correctly cleaned up, and to run ES as a regular user instead of root. Also: * Ensure no files in the image have the setuid/setgid flag * Also improve dependency tracking in the build * Remove TAKE_FILE_OWNERSHIP option and its documentation
1 parent 6d4de1f commit 8a6d68b

File tree

8 files changed

+114
-68
lines changed

8 files changed

+114
-68
lines changed

distribution/docker/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ subprojects { Project subProject ->
208208
def tarFile = "${parent.projectDir}/build/elasticsearch${oss ? '-oss' : ''}_test.${VersionProperties.elasticsearch}.docker.tar"
209209

210210
final Task exportDockerImageTask = task(exportTaskName, type: LoggedExec) {
211+
inputs.file("${parent.projectDir}/build/markers/${buildTaskName}.marker")
211212
executable 'docker'
212213
outputs.file(tarFile)
213214
args "save",

distribution/docker/src/docker/Dockerfile

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ RUN chmod 0775 config data logs
3535
COPY config/elasticsearch.yml config/log4j2.properties config/
3636
RUN chmod 0660 config/elasticsearch.yml config/log4j2.properties
3737

38+
# `tini` is a tiny but valid init for containers. This is used to cleanly
39+
# control how ES and any child processes are shut down.
40+
#
41+
# The tini GitHub page gives instructions for verifying the binary using
42+
# gpg, but the keyservers are slow to return the key and this can fail the
43+
# build. Instead, we check the binary against a checksum that we have
44+
# computed.
45+
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini /tini
46+
COPY config/tini.sha512 /tini.sha512
47+
RUN sha512sum -c /tini.sha512 && chmod +x /tini
48+
3849
################################################################################
3950
# Build stage 1 (the actual elasticsearch image):
4051
# Copy elasticsearch from stage 0
@@ -45,6 +56,8 @@ FROM centos:7
4556

4657
ENV ELASTIC_CONTAINER true
4758

59+
COPY --from=builder /tini /tini
60+
4861
RUN for iter in {1..10}; do yum update --setopt=tsflags=nodocs -y && \
4962
yum install --setopt=tsflags=nodocs -y nc shadow-utils zip unzip && \
5063
yum clean all && exit_code=0 && break || exit_code=\$? && echo "yum error: retry \$iter in 10s" && sleep 10; done; \
@@ -65,14 +78,14 @@ RUN ln -sf /etc/pki/ca-trust/extracted/java/cacerts /usr/share/elasticsearch/jdk
6578

6679
ENV PATH /usr/share/elasticsearch/bin:\$PATH
6780

68-
COPY --chown=1000:0 bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
81+
COPY bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
6982

70-
# Openshift overrides USER and uses ones with randomly uid>1024 and gid=0
71-
# Allow ENTRYPOINT (and ES) to run even with a different user
72-
RUN chgrp 0 /usr/local/bin/docker-entrypoint.sh && \
73-
chmod g=u /etc/passwd && \
83+
RUN chmod g=u /etc/passwd && \
7484
chmod 0775 /usr/local/bin/docker-entrypoint.sh
7585

86+
# Ensure that there are no files with setuid or setgid, in order to mitigate "stackclash" attacks.
87+
RUN find / -xdev -perm -4000 -exec chmod ug-s {} +
88+
7689
EXPOSE 9200 9300
7790

7891
LABEL org.label-schema.build-date="${build_date}" \
@@ -95,7 +108,9 @@ LABEL org.label-schema.build-date="${build_date}" \
95108
org.opencontainers.image.vendor="Elastic" \
96109
org.opencontainers.image.version="${version}"
97110

98-
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
111+
USER elasticsearch:root
112+
113+
ENTRYPOINT ["/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
99114
# Dummy overridable parameter parsed by entrypoint
100115
CMD ["eswrapper"]
101116

distribution/docker/src/docker/bin/docker-entrypoint.sh

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,22 @@ set -e
44
# Files created by Elasticsearch should always be group writable too
55
umask 0002
66

7-
run_as_other_user_if_needed() {
8-
if [[ "$(id -u)" == "0" ]]; then
9-
# If running as root, drop to specified UID and run command
10-
exec chroot --userspec=1000 / "${@}"
11-
else
12-
# Either we are running in Openshift with random uid and are a member of the root group
13-
# or with a custom --user
14-
exec "${@}"
15-
fi
16-
}
17-
187
# Allow user specify custom CMD, maybe bin/elasticsearch itself
198
# for example to directly specify `-E` style parameters for elasticsearch on k8s
209
# or simply to run /bin/bash to check the image
21-
if [[ "$1" != "eswrapper" ]]; then
22-
if [[ "$(id -u)" == "0" && $(basename "$1") == "elasticsearch" ]]; then
23-
# centos:7 chroot doesn't have the `--skip-chdir` option and
24-
# changes our CWD.
25-
# Rewrite CMD args to replace $1 with `elasticsearch` explicitly,
26-
# so that we are backwards compatible with the docs
27-
# from the previous Elasticsearch versions<6
28-
# and configuration option D:
29-
# https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docker.html#_d_override_the_image_8217_s_default_ulink_url_https_docs_docker_com_engine_reference_run_cmd_default_command_or_options_cmd_ulink
30-
# Without this, user could specify `elasticsearch -E x.y=z` but
31-
# `bin/elasticsearch -E x.y=z` would not work.
32-
set -- "elasticsearch" "${@:2}"
33-
# Use chroot to switch to UID 1000
34-
exec chroot --userspec=1000 / "$@"
35-
else
36-
# User probably wants to run something else, like /bin/bash, with another uid forced (Openshift?)
37-
exec "$@"
38-
fi
10+
if [[ "$1" == "eswrapper" || $(basename "$1") == "elasticsearch" ]]; then
11+
# Rewrite CMD args to remove the explicit command,
12+
# so that we are backwards compatible with the docs
13+
# from the previous Elasticsearch versions < 6
14+
# and configuration option:
15+
# https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docker.html#_d_override_the_image_8217_s_default_ulink_url_https_docs_docker_com_engine_reference_run_cmd_default_command_or_options_cmd_ulink
16+
# Without this, user could specify `elasticsearch -E x.y=z` but
17+
# `bin/elasticsearch -E x.y=z` would not work. In any case,
18+
# we want to continue through this script, and not exec early.
19+
set -- "${@:2}"
20+
else
21+
# Run whatever command the user wanted
22+
exec "$@"
3923
fi
4024

4125
# Allow environment variables to be set by creating a file with the
@@ -56,18 +40,13 @@ if [[ -f bin/elasticsearch-users ]]; then
5640
# enabled, but we have no way of knowing which node we are yet. We'll just
5741
# honor the variable if it's present.
5842
if [[ -n "$ELASTIC_PASSWORD" ]]; then
59-
[[ -f /usr/share/elasticsearch/config/elasticsearch.keystore ]] || (run_as_other_user_if_needed elasticsearch-keystore create)
60-
if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then
61-
(run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password')
43+
[[ -f /usr/share/elasticsearch/config/elasticsearch.keystore ]] || (elasticsearch-keystore create)
44+
if ! (elasticsearch-keystore list | grep -q '^bootstrap.password$'); then
45+
(echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password')
6246
fi
6347
fi
6448
fi
6549

66-
if [[ "$(id -u)" == "0" ]]; then
67-
# If requested and running as root, mutate the ownership of bind-mounts
68-
if [[ -n "$TAKE_FILE_OWNERSHIP" ]]; then
69-
chown -R 1000:0 /usr/share/elasticsearch/{data,logs}
70-
fi
71-
fi
72-
73-
run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch
50+
# Signal forwarding and child reaping is handled by `tini`, which is the
51+
# actual entrypoint of the container
52+
exec /usr/share/elasticsearch/bin/elasticsearch
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ffdb31563e34bca91a094f962544b9d31f5d138432f2d639a0856ff605b3a69f47e48191da42d6956ab62a1b24eafca1a95b299901257832225d26770354ce5e /tini

distribution/src/bin/elasticsearch-env-from-file

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,44 @@ for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do
2020
fi
2121

2222
if [[ ! -e "${!VAR_NAME_FILE}" ]]; then
23-
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE does not exist" >&2
23+
# Maybe the file doesn't exist, maybe we just can't read it due to file permissions.
24+
# Check permissions on each part of the path
25+
path=''
26+
if ! echo "${!VAR_NAME_FILE}" | grep -q '^/'; then
27+
path='.'
28+
fi
29+
30+
dirname "${!VAR_NAME_FILE}" | tr '/' '\n' | while read part; do
31+
if [[ "$path" == "/" ]]; then
32+
path="${path}${part}"
33+
else
34+
path="$path/$part"
35+
fi
36+
37+
if ! [[ -x "$path" ]]; then
38+
echo "ERROR: Cannot read ${!VAR_NAME_FILE} from $VAR_NAME_FILE, due to lack of permissions on '$path'" 2>&1
39+
exit 1
40+
fi
41+
done
42+
43+
if ! [[ -r "${!VAR_NAME_FILE}" ]]; then
44+
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE is not readable." 2>&1
45+
else
46+
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE does not exist" >&2
47+
fi
48+
2449
exit 1
2550
fi
2651

2752
FILE_PERMS="$(stat -L -c '%a' ${!VAR_NAME_FILE})"
2853

2954
if [[ "$FILE_PERMS" != "400" && "$FILE_PERMS" != "600" ]]; then
30-
if [[ -h "${!VAR_NAME_FILE}" ]]; then
31-
echo "ERROR: File $(readlink "${!VAR_NAME_FILE}") (target of symlink ${!VAR_NAME_FILE} from $VAR_NAME_FILE) must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
32-
else
33-
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
34-
fi
35-
exit 1
55+
if [[ -L "${!VAR_NAME_FILE}" ]]; then
56+
echo "ERROR: File $(readlink "${!VAR_NAME_FILE}") (target of symlink ${!VAR_NAME_FILE} from $VAR_NAME_FILE) must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
57+
else
58+
echo "ERROR: File ${!VAR_NAME_FILE} from $VAR_NAME_FILE must have file permissions 400 or 600, but actually has: $FILE_PERMS" >&2
59+
fi
60+
exit 1
3661
fi
3762

3863
echo "Setting $VAR_NAME from $VAR_NAME_FILE at ${!VAR_NAME_FILE}" >&2
@@ -43,4 +68,3 @@ for VAR_NAME_FILE in $(env | cut -f1 -d= | grep '_FILE$'); do
4368
unset "$VAR_NAME_FILE"
4469
fi
4570
done
46-

docs/reference/setup/install/docker.asciidoc

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ endif::[]
8787
This sample Docker Compose file brings up a three-node {es} cluster.
8888
Node `es01` listens on `localhost:9200` and `es02` and `es03` talk to `es01` over a Docker network.
8989

90-
Please note that this configuration exposes port 9200 on all network interfaces, and given how
91-
Docker manipulates `iptables` on Linux, this means that your {es} cluster is publically accessible,
92-
potentially ignoring any firewall settings. If you don't want to expose port 9200 and instead use
90+
Please note that this configuration exposes port 9200 on all network interfaces, and given how
91+
Docker manipulates `iptables` on Linux, this means that your {es} cluster is publicly accessible,
92+
potentially ignoring any firewall settings. If you don't want to expose port 9200 and instead use
9393
a reverse proxy, replace `9200:9200` with `127.0.0.1:9200:9200` in the docker-compose.yml file.
9494
{es} will then only be accessible from the host machine itself.
9595

@@ -221,12 +221,6 @@ chmod g+rwx esdatadir
221221
chgrp 0 esdatadir
222222
--------------------------------------------
223223

224-
As a last resort, you can force the container to mutate the ownership of
225-
any bind-mounts used for the <<path-settings,data and log dirs>> through the
226-
environment variable `TAKE_FILE_OWNERSHIP`. When you do this, they will be owned by
227-
uid:gid `1000:0`, which provides the required read/write access to the {es} process.
228-
229-
230224
===== Increase ulimits for nofile and nproc
231225

232226
Increased ulimits for <<setting-system-settings,nofile>> and <<max-number-threads-check,nproc>>

qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -651,21 +651,42 @@ public void test120DockerLogsIncludeElasticsearchLogs() throws Exception {
651651
}
652652

653653
/**
654-
* Check that the Java process running inside the container has the expect PID, UID and username.
654+
* Check that the Java process running inside the container has the expected UID, GID and username.
655655
*/
656-
public void test130JavaHasCorrectPidAndOwnership() {
657-
final List<String> processes = sh.run("ps -o pid,uid,user -C java").stdout.lines().skip(1).collect(Collectors.toList());
656+
public void test130JavaHasCorrectOwnership() {
657+
final List<String> processes = sh.run("ps -o uid,gid,user -C java").stdout.lines().skip(1).collect(Collectors.toList());
658658

659659
assertThat("Expected a single java process", processes, hasSize(1));
660660

661661
final String[] fields = processes.get(0).trim().split("\\s+");
662662

663663
assertThat(fields, arrayWithSize(3));
664+
assertThat("Incorrect UID", fields[0], equalTo("1000"));
665+
assertThat("Incorrect GID", fields[1], equalTo("0"));
666+
assertThat("Incorrect username", fields[2], equalTo("elasticsearch"));
667+
}
668+
669+
/**
670+
* Check that the init process running inside the container has the expected PID, UID, GID and user.
671+
* The PID is particularly important because PID 1 handles signal forwarding and child reaping.
672+
*/
673+
public void test131InitProcessHasCorrectPID() {
674+
final List<String> processes = sh.run("ps -o pid,uid,gid,user -p 1").stdout.lines().skip(1).collect(Collectors.toList());
675+
676+
assertThat("Expected a single process", processes, hasSize(1));
677+
678+
final String[] fields = processes.get(0).trim().split("\\s+");
679+
680+
assertThat(fields, arrayWithSize(4));
664681
assertThat("Incorrect PID", fields[0], equalTo("1"));
665682
assertThat("Incorrect UID", fields[1], equalTo("1000"));
666-
assertThat("Incorrect username", fields[2], equalTo("elasticsearch"));
683+
assertThat("Incorrect GID", fields[2], equalTo("0"));
684+
assertThat("Incorrect username", fields[3], equalTo("elasticsearch"));
667685
}
668686

687+
/**
688+
* Check that Elasticsearch reports per-node cgroup information.
689+
*/
669690
public void test140CgroupOsStatsAreAvailable() throws Exception {
670691
waitForElasticsearch(installation);
671692

qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.http.client.fluent.Request;
2727
import org.elasticsearch.common.CheckedRunnable;
2828

29+
import java.nio.file.Files;
2930
import java.nio.file.Path;
3031
import java.nio.file.Paths;
3132
import java.nio.file.attribute.PosixFileAttributes;
@@ -152,9 +153,19 @@ private static void executeDockerRun(Distribution distribution, Map<Path, Path>
152153

153154
// Bind-mount any volumes
154155
if (volumes != null) {
155-
volumes.forEach((localPath, containerPath) -> args.add("--volume \"" + localPath + ":" + containerPath + "\""));
156+
volumes.forEach((localPath, containerPath) -> {
157+
assertTrue(localPath + " doesn't exist", Files.exists(localPath));
158+
159+
if (Platforms.WINDOWS == false && System.getProperty("user.name").equals("root")) {
160+
// The tests are running as root, but the process in the Docker container runs as `elasticsearch` (UID 1000),
161+
// so we need to ensure that the container process is able to read the bind-mounted files.
162+
sh.run("chown -R 1000:0 " + localPath);
163+
}
164+
args.add("--volume \"" + localPath + ":" + containerPath + "\"");
165+
});
156166
}
157167

168+
// Image name
158169
args.add(distribution.flavor.name + ":test");
159170

160171
final String command = String.join(" ", args);

0 commit comments

Comments
 (0)