Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework CA certificate support to allow rootless containers #538

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .test/tests/java-ca-certificates-update/certs/README.md
Original file line number Diff line number Diff line change
@@ -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
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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0101010001
01010100010101010001
105 changes: 63 additions & 42 deletions .test/tests/java-ca-certificates-update/run.sh
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
#!/bin/bash

set -o pipefail
set -o pipefail

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

# Find Java major/minor/build/patch version
#
# https://stackoverflow.com/a/74459237/6460
IFS='"' read -r _ java_version_string _ < <(docker run "$1" java -version 2>&1)
IFS='._' read -r \
java_version_major \
java_version_minor \
java_version_build \
java_version_patch \
<<<"$java_version_string"

# CMD1 in each run is just a `date` to make sure nothing is broken with or without the entrypoint
CMD1=date

# CMD2 in each run is to check for the `dockerbuilder` certificate in the Java keystore
if [ "$java_version_major" -lt 11 ]; then
# We are working with JDK/JRE 8
#
# `keytool` from JDK/JRE 8 does not have the `-cacerts` option and also does not have standardized location for the
# `cacerts` file between the JDK and JRE, so we'd want to check both possible locations.
CACERTS=/opt/java/openjdk/lib/security/cacerts
CACERTS2=/opt/java/openjdk/jre/lib/security/cacerts

CMD2=(sh -c "keytool -list -keystore $CACERTS -storepass changeit -alias dockerbuilder || keytool -list -keystore $CACERTS2 -storepass changeit -alias dockerbuilder")
else
CMD2=(keytool -list -cacerts -storepass changeit -alias dockerbuilder)
fi

#
# We need to use `docker run`, since `run-in-container.sh` overwrites the entrypoint
# CMD2 in each run is to check for the `dockerbuilder` certificate in the Java keystore. Entrypoint export $CACERT to
# point to the Java keystore.
CMD2=(sh -c "keytool -list -keystore \$CACERT -storepass changeit -alias dockerbuilder")

# For a custom entrypoint test, we need to create a new image. This image will get cleaned up at the end of the script
# by the `finish` trap function.
TESTIMAGE=$1.test

function finish {
docker rmi "$TESTIMAGE" >&/dev/null
}
trap finish EXIT HUP INT TERM

# But first, we need to create an image with an overridden entrypoint
docker build -t "$1.test" "$runDir" -f - <<EOF >&/dev/null
FROM $1
COPY custom-entrypoint.sh /
ENTRYPOINT ["/custom-entrypoint.sh"]
EOF

# NB: In this script, we need to use `docker run` explicitly, since the normally used `run-in-container.sh` overwrites
# the entrypoint.

#
# PHASE 1: Root containers
#

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

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

function finish {
docker rmi "$TESTIMAGE" >&/dev/null
}
trap finish EXIT HUP INT TERM
#
# PHASE 2: Non-root containers
#

# Test run 1: No added certificates and environment variable is not set. We expect CMD1 to succeed and CMD2 to fail.
docker run --read-only --user 1000:1000 --rm "$1" $CMD1 >&/dev/null
echo -n $?
docker run --read-only --user 1000:1000 --rm "$1" "${CMD2[@]}" >&/dev/null
echo -n $?

# Test run 2: No added certificates, but the environment variable is set. Since there are no certificates, we still
# expect CMD1 to succeed and CMD2 to fail.
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 "$1" $CMD1 >&/dev/null
echo -n $?
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 "$1" "${CMD2[@]}" >&/dev/null
echo -n $?

# Test run 3: Certificates are mounted, but the environment variable is not set, i.e. certificate importing should not
# be activated. We expect CMD1 to succeed and CMD2 to fail.
docker run --read-only --user 1000:1000 --rm --volume=$testDir/certs:/certificates "$1" $CMD1 >&/dev/null
echo -n $?
docker run --read-only --user 1000:1000 --rm --volume=$testDir/certs:/certificates "$1" "${CMD2[@]}" >&/dev/null
echo -n $?

# Test run 4: Certificates are mounted and the environment variable is set. We expect both CMD1 and CMD2 to succeed.
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$1" $CMD1 >&/dev/null
echo -n $?
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$1" "${CMD2[@]}" >&/dev/null
echo -n $?

# Test run 5: Certificates are mounted and the environment variable is set, but the entrypoint is overridden. We expect
# CMD1 to succeed and CMD2 to fail.
#
# But first, we need to create an image with an overridden entrypoint
docker build -t "$1.test" "$runDir" -f - <<EOF >&/dev/null
FROM $1
COPY custom-entrypoint.sh /
ENTRYPOINT ["/custom-entrypoint.sh"]
EOF

docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" $CMD1 >&/dev/null
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" $CMD1 >&/dev/null
echo -n $?
docker run --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" "${CMD2[@]}" >&/dev/null
docker run --read-only --user 1000:1000 -v /tmp --rm -e USE_SYSTEM_CA_CERTS=1 --volume=$testDir/certs:/certificates "$TESTIMAGE" "${CMD2[@]}" >&/dev/null
echo -n $?
87 changes: 73 additions & 14 deletions 11/jdk/alpine/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,87 @@

set -e

# JDK truststore location
CACERT=$JAVA_HOME/lib/security/cacerts

# JDK8 puts its JRE in a subdirectory
if [ -f "$JAVA_HOME/jre/lib/security/cacerts" ]; then
CACERT=$JAVA_HOME/jre/lib/security/cacerts
fi

# Opt-in is only activated if the environment variable is set
if [ -n "$USE_SYSTEM_CA_CERTS" ]; then

# Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty.
# The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the
# system location, for whatever reason.
if [ -d /certificates ] && [ -n "$(ls -A /certificates 2>/dev/null)" ]; then
cp -a /certificates/* /usr/local/share/ca-certificates/
if [ ! -w /tmp ]; then
echo "Using additional CA certificates requires write permissions to /tmp. Cannot create truststore."
exit 1
fi

CACERT="$JAVA_HOME/lib/security/cacerts"

# JDK8 puts its JRE in a subdirectory
if [ -f "$JAVA_HOME/jre/lib/security/cacerts" ]; then
CACERT="$JAVA_HOME/jre/lib/security/cacerts"
# Figure out whether we can write to the JVM truststore. If we can, we'll add the certificates there. If not,
# we'll use a temporary truststore.
if [ ! -w "$CACERT" ]; then
# We cannot write to the JVM truststore, so we create a temporary one
CACERT_NEW=$(mktemp)
echo "Using a temporary truststore at $CACERT_NEW"
cp $CACERT $CACERT_NEW
CACERT=$CACERT_NEW
# If we use a custom truststore, we need to make sure that the JVM uses it
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS} -Djavax.net.ssl.trustStore=${CACERT} -Djavax.net.ssl.trustStorePassword=changeit"
fi

# OpenJDK images used to create a hook for `update-ca-certificates`. Since we are using an entrypoint anyway, we
# might as well just generate the truststore and skip the hooks.
update-ca-certificates
tmp_store=$(mktemp)

# Copy full system CA store to a temporary location
trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$tmp_store"

# Add the system CA certificates to the JVM truststore.
keytool -importkeystore -destkeystore "$CACERT" -srckeystore "$tmp_store" -srcstorepass changeit -deststorepass changeit -noprompt # >/dev/null

# Import the additional certificate into JVM truststore
for i in /certificates/*crt; do
if [ ! -f "$i" ]; then
continue
fi
keytool -import -noprompt -alias "$(basename "$i" .crt)" -file "$i" -keystore "$CACERT" -storepass changeit # >/dev/null
done

trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$CACERT"
# Add additional certificates to the system CA store. This requires write permissions to several system
# locations, which is not possible in a container with read-only filesystem and/or non-root container.
if [ "$(id -u)" -eq 0 ]; then

# Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty.
# The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the
# system location, for whatever reason.
if [ -d /certificates ] && [ "$(ls -A /certificates 2>/dev/null)" ]; then

# UBI/CentOS
if [ -d /usr/share/pki/ca-trust-source/anchors/ ]; then
cp -a /certificates/* /usr/share/pki/ca-trust-source/anchors/
fi

# Ubuntu/Alpine
if [ -d /usr/local/share/ca-certificates/ ]; then
cp -a /certificates/* /usr/local/share/ca-certificates/
fi
fi

# UBI/CentOS
if which update-ca-trust >/dev/null; then
update-ca-trust
fi

# Ubuntu/Alpine
if which update-ca-certificates >/dev/null; then
update-ca-certificates
fi
else
# If we are not root, we cannot update the system truststore. That's bad news for tools like `curl` and `wget`,
# but since the JVM is the primary focus here, we can live with that.
true
fi
fi

# Let's provide a variable with the correct path for tools that want or need to use it
export CACERT

exec "$@"
91 changes: 75 additions & 16 deletions 11/jdk/centos/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,89 @@
#!/usr/bin/env bash
# Shebang needs to be `bash`, see https://github.com/adoptium/containers/issues/415 for details
#!/usr/bin/env sh
# Converted to POSIX shell to avoid the need for bash in the image

set -e

# JDK truststore location
CACERT=$JAVA_HOME/lib/security/cacerts

# JDK8 puts its JRE in a subdirectory
if [ -f "$JAVA_HOME/jre/lib/security/cacerts" ]; then
CACERT=$JAVA_HOME/jre/lib/security/cacerts
fi

# Opt-in is only activated if the environment variable is set
if [ -n "$USE_SYSTEM_CA_CERTS" ]; then

# Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty.
# The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the
# system location, for whatever reason.
if [ -d /certificates ] && [ "$(ls -A /certificates)" ]; then
cp -a /certificates/* /usr/share/pki/ca-trust-source/anchors/
if [ ! -w /tmp ]; then
echo "Using additional CA certificates requires write permissions to /tmp. Cannot create truststore."
exit 1
fi

CACERT=$JAVA_HOME/lib/security/cacerts

# JDK8 puts its JRE in a subdirectory
if [ -f "$JAVA_HOME/jre/lib/security/cacerts" ]; then
CACERT=$JAVA_HOME/jre/lib/security/cacerts
# Figure out whether we can write to the JVM truststore. If we can, we'll add the certificates there. If not,
# we'll use a temporary truststore.
if [ ! -w "$CACERT" ]; then
# We cannot write to the JVM truststore, so we create a temporary one
CACERT_NEW=$(mktemp)
echo "Using a temporary truststore at $CACERT_NEW"
cp $CACERT $CACERT_NEW
CACERT=$CACERT_NEW
# If we use a custom truststore, we need to make sure that the JVM uses it
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS} -Djavax.net.ssl.trustStore=${CACERT} -Djavax.net.ssl.trustStorePassword=changeit"
fi

# RHEL-based images already include a routine to update a java truststore from the system CA bundle within
# `update-ca-trust`. All we need to do is to link the system CA bundle to the java truststore.
update-ca-trust
tmp_store=$(mktemp)

# Copy full system CA store to a temporary location
trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$tmp_store"

# Add the system CA certificates to the JVM truststore.
keytool -importkeystore -destkeystore "$CACERT" -srckeystore "$tmp_store" -srcstorepass changeit -deststorepass changeit -noprompt # >/dev/null

# Import the additional certificate into JVM truststore
for i in /certificates/*crt; do
if [ ! -f "$i" ]; then
continue
fi
keytool -import -noprompt -alias "$(basename "$i" .crt)" -file "$i" -keystore "$CACERT" -storepass changeit # >/dev/null
done

ln -sf /etc/pki/ca-trust/extracted/java/cacerts "$CACERT"
# Add additional certificates to the system CA store. This requires write permissions to several system
# locations, which is not possible in a container with read-only filesystem and/or non-root container.
if [ "$(id -u)" -eq 0 ]; then

# Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty.
# The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the
# system location, for whatever reason.
if [ -d /certificates ] && [ "$(ls -A /certificates 2>/dev/null)" ]; then

# UBI/CentOS
if [ -d /usr/share/pki/ca-trust-source/anchors/ ]; then
cp -a /certificates/* /usr/share/pki/ca-trust-source/anchors/
fi

# Ubuntu/Alpine
if [ -d /usr/local/share/ca-certificates/ ]; then
cp -a /certificates/* /usr/local/share/ca-certificates/
fi
fi

# UBI/CentOS
if which update-ca-trust >/dev/null; then
update-ca-trust
fi

# Ubuntu/Alpine
if which update-ca-certificates >/dev/null; then
update-ca-certificates
fi
else
# If we are not root, we cannot update the system truststore. That's bad news for tools like `curl` and `wget`,
# but since the JVM is the primary focus here, we can live with that.
true
fi
fi

# Let's provide a variable with the correct path for tools that want or need to use it
export CACERT

exec "$@"
Loading
Loading