Skip to content

Commit d2cc36c

Browse files
committed
Rework CA certificate support to allow rootless containers
This patch includes several improvements and simplifications in CA certificate handling: * Support for CA certificates in containers running as a non-root user * Support for CA certificates in containers running with read-only filesystem * Unification of Docker entrypoint scripts into one * Entrypoint script now exports CACERT environment variable to point to the used truststore file Docs updates at https://github.com/docker-library/official-images/ pending. Possibly fixes: #464
1 parent 2845b61 commit d2cc36c

File tree

11 files changed

+164
-143
lines changed

11 files changed

+164
-143
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
This certificate/key pair has been generated with `openssl req -nodes -new -x509 -days 358000 -subj "/DC=Temurin/CN=DockerBuilder" -keyout certs/server.key -out certs/server.crt` and is only used for testing
1+
This certificate/key pair has been generated with `openssl req -nodes -new -x509 -days 358000 -subj "/DC=Temurin/CN=DockerBuilder" -keyout certs/dockerbuilder.key -out certs/dockerbuilder.crt` and is only used for testing
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0101010001
1+
01010100010101010001
Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,38 @@
11
#!/bin/bash
22

3-
set -o pipefail
3+
set -o pipefail
44

55
testDir="$(readlink -f "$(dirname "$BASH_SOURCE")")"
66
runDir="$(dirname "$(readlink -f "$BASH_SOURCE")")"
77

8-
# Find Java major/minor/build/patch version
9-
#
10-
# https://stackoverflow.com/a/74459237/6460
11-
IFS='"' read -r _ java_version_string _ < <(docker run "$1" java -version 2>&1)
12-
IFS='._' read -r \
13-
java_version_major \
14-
java_version_minor \
15-
java_version_build \
16-
java_version_patch \
17-
<<<"$java_version_string"
18-
198
# CMD1 in each run is just a `date` to make sure nothing is broken with or without the entrypoint
209
CMD1=date
2110

22-
# CMD2 in each run is to check for the `dockerbuilder` certificate in the Java keystore
23-
if [ "$java_version_major" -lt 11 ]; then
24-
# We are working with JDK/JRE 8
25-
#
26-
# `keytool` from JDK/JRE 8 does not have the `-cacerts` option and also does not have standardized location for the
27-
# `cacerts` file between the JDK and JRE, so we'd want to check both possible locations.
28-
CACERTS=/opt/java/openjdk/lib/security/cacerts
29-
CACERTS2=/opt/java/openjdk/jre/lib/security/cacerts
30-
31-
CMD2=(sh -c "keytool -list -keystore $CACERTS -storepass changeit -alias dockerbuilder || keytool -list -keystore $CACERTS2 -storepass changeit -alias dockerbuilder")
32-
else
33-
CMD2=(keytool -list -cacerts -storepass changeit -alias dockerbuilder)
34-
fi
35-
36-
#
37-
# We need to use `docker run`, since `run-in-container.sh` overwrites the entrypoint
11+
# CMD2 in each run is to check for the `dockerbuilder` certificate in the Java keystore. Entrypoint export $CACERT to
12+
# point to the Java keystore.
13+
CMD2=(sh -c "keytool -list -keystore \$CACERT -storepass changeit -alias dockerbuilder")
14+
15+
# For a custom entrypoint test, we need to create a new image. This image will get cleaned up at the end of the script
16+
# by the `finish` trap function.
17+
TESTIMAGE=$1.test
18+
19+
function finish {
20+
docker rmi "$TESTIMAGE" >&/dev/null
21+
}
22+
trap finish EXIT HUP INT TERM
23+
24+
# But first, we need to create an image with an overridden entrypoint
25+
docker build -t "$1.test" "$runDir" -f - <<EOF >&/dev/null
26+
FROM $1
27+
COPY custom-entrypoint.sh /
28+
ENTRYPOINT ["/custom-entrypoint.sh"]
29+
EOF
30+
31+
# NB: In this script, we need to use `docker run` explicitely, since the normally used `run-in-container.sh` overwrites
32+
# the entrypoint.
33+
34+
#
35+
# PHASE 1: Root containers
3836
#
3937

4038
# Test run 1: No added certificates and environment variable is not set. We expect CMD1 to succeed and CMD2 to fail.
@@ -63,24 +61,47 @@ echo -n $?
6361
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$1" "${CMD2[@]}" >&/dev/null
6462
echo -n $?
6563

66-
TESTIMAGE=$1.test
64+
# Test run 5: Certificates are mounted and the environment variable is set, but the entrypoint is overridden. We expect
65+
# CMD1 to succeed and CMD2 to fail.
66+
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" $CMD1 >&/dev/null
67+
echo -n $?
68+
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" "${CMD2[@]}" >&/dev/null
69+
echo -n $?
6770

68-
function finish {
69-
docker rmi "$TESTIMAGE" >&/dev/null
70-
}
71-
trap finish EXIT HUP INT TERM
71+
#
72+
# PHASE 2: Non-root containers
73+
#
74+
75+
# Test run 1: No added certificates and environment variable is not set. We expect CMD1 to succeed and CMD2 to fail.
76+
docker run --read-only --user 1000:1000 --rm "$1" $CMD1 >&/dev/null
77+
echo -n $?
78+
docker run --read-only --user 1000:1000 --rm "$1" "${CMD2[@]}" >&/dev/null
79+
echo -n $?
80+
81+
# Test run 2: No added certificates, but the environment variable is set. Since there are no certificates, we still
82+
# expect CMD1 to succeed and CMD2 to fail.
83+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 "$1" $CMD1 >&/dev/null
84+
echo -n $?
85+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 "$1" "${CMD2[@]}" >&/dev/null
86+
echo -n $?
87+
88+
# Test run 3: Certificates are mounted, but the environment variable is not set, i.e. certificate importing should not
89+
# be activated. We expect CMD1 to succeed and CMD2 to fail.
90+
docker run --read-only --user 1000:1000 --rm --volume=$testDir/certs:/certificates "$1" $CMD1 >&/dev/null
91+
echo -n $?
92+
docker run --read-only --user 1000:1000 --rm --volume=$testDir/certs:/certificates "$1" "${CMD2[@]}" >&/dev/null
93+
echo -n $?
94+
95+
# Test run 4: Certificates are mounted and the environment variable is set. We expect both CMD1 and CMD2 to succeed.
96+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$1" $CMD1 >&/dev/null
97+
echo -n $?
98+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$1" "${CMD2[@]}" >&/dev/null
99+
echo -n $?
72100

73101
# Test run 5: Certificates are mounted and the environment variable is set, but the entrypoint is overridden. We expect
74102
# CMD1 to succeed and CMD2 to fail.
75103
#
76-
# But first, we need to create an image with an overridden entrypoint
77-
docker build -t "$1.test" "$runDir" -f - <<EOF >&/dev/null
78-
FROM $1
79-
COPY custom-entrypoint.sh /
80-
ENTRYPOINT ["/custom-entrypoint.sh"]
81-
EOF
82-
83-
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" $CMD1 >&/dev/null
104+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" $CMD1 >&/dev/null
84105
echo -n $?
85-
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" "${CMD2[@]}" >&/dev/null
106+
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" "${CMD2[@]}" >&/dev/null
86107
echo -n $?

docker_templates/entrypoint.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env sh
2+
# Converted to POSIX shell to avoid the need for bash in the image
3+
4+
set -e
5+
6+
# JDK truststore location
7+
CACERT=$JAVA_HOME/lib/security/cacerts
8+
9+
# JDK8 puts its JRE in a subdirectory
10+
if [ -f "$JAVA_HOME/jre/lib/security/cacerts" ]; then
11+
CACERT=$JAVA_HOME/jre/lib/security/cacerts
12+
fi
13+
14+
# Opt-in is only activated if the environment variable is set
15+
if [ -n "$USE_SYSTEM_CA_CERTS" ]; then
16+
17+
if [ ! -w /tmp ]; then
18+
echo "Using additional CA certificates requires write permissions to /tmp. Cannot create truststore."
19+
exit 1
20+
fi
21+
22+
# Figure out whether we can write to the JVM truststore. If we can, we'll add the certificates there. If not,
23+
# we'll use a temporary truststore.
24+
if [ ! -w "$CACERT" ]; then
25+
# We cannot write to the JVM truststore, so we create a temporary one
26+
CACERT_NEW=$(mktemp)
27+
echo "Using a temporary truststore at $CACERT_NEW"
28+
cp $CACERT $CACERT_NEW
29+
CACERT=$CACERT_NEW
30+
# If we use a custom truststore, we need to make sure that the JVM uses it
31+
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS} -Djavax.net.ssl.trustStore=${CACERT} -Djavax.net.ssl.trustStorePassword=changeit"
32+
fi
33+
34+
tmp_store=$(mktemp)
35+
36+
# Copy full system CA store to a temporary location
37+
trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$tmp_store"
38+
39+
# Add the system CA certificates to the JVM truststore.
40+
keytool -importkeystore -destkeystore "$CACERT" -srckeystore "$tmp_store" -srcstorepass changeit -deststorepass changeit -noprompt # >/dev/null
41+
42+
# Import the additional certificate into JVM truststore
43+
for i in /certificates/*crt; do
44+
if [ ! -f "$i" ]; then
45+
continue
46+
fi
47+
keytool -import -noprompt -alias "$(basename "$i" .crt)" -file "$i" -keystore "$CACERT" -storepass changeit # >/dev/null
48+
done
49+
50+
# Add additionation certificates to the system CA store. This requires write permissions to several system
51+
# locations, which is not possible in a container with read-only filesystem and/or non-root container.
52+
if [ "$(id -u)" -eq 0 ]; then
53+
54+
# Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty.
55+
# The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the
56+
# system location, for whatever reason.
57+
if [ -d /certificates ] && [ "$(ls -A /certificates 2>/dev/null)" ]; then
58+
59+
# UBI/CentOS
60+
if [ -d /usr/share/pki/ca-trust-source/anchors/ ]; then
61+
cp -a /certificates/* /usr/share/pki/ca-trust-source/anchors/
62+
fi
63+
64+
# Ubuntu/Alpine
65+
if [ -d /usr/local/share/ca-certificates/ ]; then
66+
cp -a /certificates/* /usr/local/share/ca-certificates/
67+
fi
68+
fi
69+
70+
# UBI/CentOS
71+
if which update-ca-trust >/dev/null; then
72+
update-ca-trust
73+
fi
74+
75+
# Ubuntu/Alpine
76+
if which update-ca-certificates >/dev/null; then
77+
update-ca-certificates
78+
fi
79+
else
80+
# If we are not root, we cannot update the system truststore. That's bad news for tools like `curl` and `wget`,
81+
# but since the JVM is the primary focus here, we can live with that.
82+
true
83+
fi
84+
fi
85+
86+
# Let's provide a variable with the correct path for tools that want or need to use it
87+
export CACERT
88+
89+
exec "$@"

docker_templates/scripts/entrypoint.alpine-linux.sh

Lines changed: 0 additions & 30 deletions
This file was deleted.

docker_templates/scripts/entrypoint.centos.sh

Lines changed: 0 additions & 1 deletion
This file was deleted.

docker_templates/scripts/entrypoint.ubi9-minimal.sh

Lines changed: 0 additions & 30 deletions
This file was deleted.

docker_templates/scripts/entrypoint.ubuntu.sh

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)