⚠️ This article has been written and tested on Keycloak v13 and is working with version until 16. For later version, using Quarkus based distribution (v17+), another article will be redacted.
Keycloak is a wonderful piece of software, managed with success by RedHat, to be used as an Identity and Access Management software. RedHat distribute it as a zip package to be run on a machine with a JVM installed or as a container. Nowadays, container is a simpler solution, especially if you are using an orchestrator like Kubernetes.
The Keycloak image is available on the DockerHub or Quay. It provides an important level of configuration through environment variables, which is useful if you are not familiar with WildFly configuration. But, this solution has an important downside, especially for a tool dedicated to security… tags are not maintained at OS level over time and has many vulnerabilities.
You can see below, a lot of vulnerabilities in the latest Keycloak image, especially at the OS level. In some case, you can't choose to rely on so many vulnerabilities and need to fix that, or at least reduce them.
$ trivy image jboss/keycloak:13.0.1
2021-05-26T19:23:14.416+0200 INFO Detected OS: redhat
2021-05-26T19:23:14.416+0200 INFO Detecting RHEL/CentOS vulnerabilities...
2021-05-26T19:23:14.432+0200 INFO Number of PL dependency files: 621
jboss/keycloak:13.0.1 (redhat 8.4)
==================================
Total: 118 (UNKNOWN: 0, LOW: 49, MEDIUM: 67, HIGH: 2, CRITICAL: 0)
...
NOTE: Number of CVEs in an image evolves over time, so reports in this article can be way different if you run it by yourself.
On one side, you can choose to upgrade every packages in the image manually, hoping a fix is available in the official CentOS registry. Another solution is to change the base image to something with less vulnerability like Google Distroless. Those images only contain the runtime for your application and nothing less… no shell, no package manager, nothing… just your runtime. For Keycloak, we will use the Distroless Java image to sanitize our workload.
Crafting the best Dockerfile possible
The original Keycloak image use a lot of bash
scripts to configure the whole system. This is a good idea, but here, we don't have any shell in our Distroless base image, so we will have to extract the application, and the way to launch it from scratch.
Moving Keycloak into Distroless
If we analyse the jboss/keycloak:13.0.1
image with Dive, we can see all Keycloak related files are stored into /opt/jboss/
.
We will copy them into our distroless then, with the following Dockerfile
:
FROM jboss/keycloak:13.0.1 as base
FROM gcr.io/distroless/java:11-nonroot
COPY --chown=nonroot:nonroot --from=base /opt/jboss /opt/jboss
The execution is pretty simple:
$ docker build -t keycloak-distroless .
[+] Building 0.6s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for gcr.io/distroless/java:11-nonroot 0.5s
=> [internal] load metadata for docker.io/jboss/keycloak:13.0.1 0.0s
=> [base 1/1] FROM docker.io/jboss/keycloak:13.0.1 0.0s
=> [stage-1 1/2] FROM gcr.io/distroless/java:11-nonroot@sha256:07d017944 0.0s
=> CACHED [stage-1 2/2] COPY --chown=nonroot:nonroot --from=base /opt/jb 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:06e849f0ab369043be9c071a446484e2a699a114dd988 0.0s
=> => naming to docker.io/library/keycloak-distroless 0.0s
Sadly, if we are launching it like this, we will see the following error:
$ docker run --rm -it -p 8080:8080 keycloak-distroless
Error: -jar requires jar file specification
Usage: java [options] <mainclass> [args...]
(to execute a class)
or java [options] -jar <jarfile> [args...]
(to execute a jar file)
or java [options] -m <module>[/<mainclass>] [args...]
java [options] --module <module>[/<mainclass>] [args...]
(to execute the main class in a module)
or java [options] <sourcefile> [args]
(to execute a single source-file program)
Arguments following the main class, source file, -jar <jarfile>,
-m or --module <module>/<mainclass> are passed as the arguments to
main class.
...
This is because the default ENTRYPOINT
of this distroless image want to launch a (fat) JAR, but keycloak is more complex than this, so we will have to find the right ENTRYPOINT
for our use case.
Generating the ENTRYPOINT
For this one, we will use the original image to see how Keycloak is launched in its natural state. To do that, we will edit the standalone.sh
file to make it more verbose and copy the java
command generated from it. We will follow the official documentation to launch keycloak, but we will log into the container to do our magic trick:
# Starting the container with the minimal configuration and log into it thanks to the custom entrypoint
$ docker run -it --rm -e DB_VENDOR=h2 --entrypoint=bash jboss/keycloak:13.0.1
# From here, we are IN the Keycloak image!
# The following command update the standalone.sh file to be a lot verbose
bash-4.4$ awk -i inplace 'NR==2 {print "set -x"} 1' /opt/jboss/keycloak/bin/standalone.sh
# Finally, we will launch keycloak from here and stop it when we found the line starting with "++ java"
bash-4.4$ /opt/jboss/tools/docker-entrypoint.sh
=========================================================================
Using Embedded H2 database
=========================================================================
+ DEBUG_MODE=false
+ DEBUG_PORT=8787
+ GC_LOG=
+ SERVER_OPTS=
+ '[' 3 -gt 0 ']'
+ case "$1" in
+ SERVER_OPTS=' '\''-Djboss.bind.address=172.17.0.2'\'''
+ shift
+ '[' 2 -gt 0 ']'
+ case "$1" in
+ SERVER_OPTS=' '\''-Djboss.bind.address=172.17.0.2'\'' '\''-Djboss.bind.address.private=172.17.0.2'\'''
+ shift
+ '[' 1 -gt 0 ']'
+ case "$1" in
+ SERVER_OPTS=' '\''-Djboss.bind.address=172.17.0.2'\'' '\''-Djboss.bind.address.private=172.17.0.2'\'' '\''-c=standalone-ha.xml'\'''
+ shift
+ '[' 0 -gt 0 ']'
++ dirname /opt/jboss/keycloak/bin/standalone.sh
+ DIRNAME=/opt/jboss/keycloak/bin
++ basename /opt/jboss/keycloak/bin/standalone.sh
+ PROGNAME=standalone.sh
+ GREP=grep
+ . /opt/jboss/keycloak/bin/common.sh
++ '[' x = x ']'
++ COMMON_CONF=/opt/jboss/keycloak/bin/common.conf
++ '[' -r /opt/jboss/keycloak/bin/common.conf ']'
+ MAX_FD=maximum
+ MALLOC_ARENA_MAX=1
+ export MALLOC_ARENA_MAX
+ cygwin=false
+ darwin=false
+ linux=false
+ solaris=false
+ freebsd=false
+ other=false
+ case "`uname`" in
++ uname
+ linux=true
+ false
++ cd /opt/jboss/keycloak/bin/..
++ pwd
+ RESOLVED_JBOSS_HOME=/opt/jboss/keycloak
+ '[' x/opt/jboss/keycloak = x ']'
++ cd /opt/jboss/keycloak
++ pwd
+ SANITIZED_JBOSS_HOME=/opt/jboss/keycloak
+ '[' /opt/jboss/keycloak '!=' /opt/jboss/keycloak ']'
+ export JBOSS_HOME
+ '[' x = x ']'
+ RUN_CONF=/opt/jboss/keycloak/bin/standalone.conf
+ '[' -r /opt/jboss/keycloak/bin/standalone.conf ']'
+ . /opt/jboss/keycloak/bin/standalone.conf
++ '[' x = x ']'
++ JBOSS_MODULES_SYSTEM_PKGS=org.jboss.byteman
++ '[' x = x ']'
++ JAVA_OPTS='-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true'
++ JAVA_OPTS='-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true'
++ JAVA_OPTS='-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true '
+ '[' false = true ']'
+ '[' x = x ']'
+ '[' x '!=' x ']'
+ JAVA=java
+ true
+ CONSOLIDATED_OPTS='-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true '\''-Djboss.bind.address=172.17.0.2'\'' '\''-Djboss.bind.address.private=172.17.0.2'\'' '\''-c=standalone-ha.xml'\'''
+ for var in $CONSOLIDATED_OPTS
++ echo -Xms64m
++ tr -d \'
+ p=-Xms64m
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -Xmx512m
++ tr -d \'
+ p=-Xmx512m
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -XX:MetaspaceSize=96M
++ tr -d \'
+ p=-XX:MetaspaceSize=96M
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -XX:MaxMetaspaceSize=256m
++ tr -d \'
+ p=-XX:MaxMetaspaceSize=256m
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -Djava.net.preferIPv4Stack=true
++ tr -d \'
+ p=-Djava.net.preferIPv4Stack=true
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -Djboss.modules.system.pkgs=org.jboss.byteman
++ tr -d \'
+ p=-Djboss.modules.system.pkgs=org.jboss.byteman
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo -Djava.awt.headless=true
++ tr -d \'
+ p=-Djava.awt.headless=true
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo ''\''-Djboss.bind.address=172.17.0.2'\'''
++ tr -d \'
+ p=-Djboss.bind.address=172.17.0.2
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo ''\''-Djboss.bind.address.private=172.17.0.2'\'''
++ tr -d \'
+ p=-Djboss.bind.address.private=172.17.0.2
+ case $p in
+ for var in $CONSOLIDATED_OPTS
++ echo ''\''-c=standalone-ha.xml'\'''
++ tr -d \'
+ p=-c=standalone-ha.xml
+ case $p in
+ false
+ false
+ false
+ false
+ '[' x = x ']'
+ JBOSS_BASE_DIR=/opt/jboss/keycloak/standalone
+ '[' x = x ']'
+ JBOSS_LOG_DIR=/opt/jboss/keycloak/standalone/log
+ '[' x = x ']'
+ JBOSS_CONFIG_DIR=/opt/jboss/keycloak/standalone/configuration
+ '[' x = x ']'
+ JBOSS_MODULEPATH=/opt/jboss/keycloak/modules
+ false
+ '[' '' '!=' true ']'
++ echo -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
++ grep '\-d64'
+ JVM_D64_OPTION=
++ echo -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
++ grep '\-d32'
+ JVM_D32_OPTION=
++ echo -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
++ grep '\-server'
+ SERVER_SET=
++ echo -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
++ grep '\-client'
+ CLIENT_SET=
+ '[' x '!=' x ']'
+ '[' x '!=' x ']'
+ false
+ '[' x = x -a x = x ']'
+ false
+ PREPEND_JAVA_OPTS=' -server'
+ setModularJdk
+ java --add-modules=java.se -version
+ MODULAR_JDK=true
+ '[' '' = true ']'
+ setDefaultModularJvmOptions -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
+ setModularJdk
+ java --add-modules=java.se -version
+ MODULAR_JDK=true
+ '[' true = true ']'
++ echo -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true
++ grep '\-\-add\-modules'
+ DEFAULT_MODULAR_JVM_OPTIONS=
+ '[' x = x ']'
+ DEFAULT_MODULAR_JVM_OPTIONS=' --add-exports=java.base/sun.nio.ch=ALL-UNNAMED'
+ DEFAULT_MODULAR_JVM_OPTIONS=' --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED'
+ DEFAULT_MODULAR_JVM_OPTIONS=' --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'
+ JAVA_OPTS='-Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'
+ JAVA_OPTS=' -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'
++ echo -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED
++ grep 'java\.security\.manager'
+ SECURITY_MANAGER_SET=
+ '[' x '!=' x ']'
+ MODULE_OPTS=
+ '[' '' = true ']'
++ echo ''
++ grep '\-javaagent:'
+ AGENT_SET=
+ '[' x '!=' x ']'
+ echo =========================================================================
=========================================================================
+ echo ''
+ echo ' JBoss Bootstrap Environment'
JBoss Bootstrap Environment
+ echo ''
+ echo ' JBOSS_HOME: /opt/jboss/keycloak'
JBOSS_HOME: /opt/jboss/keycloak
+ echo ''
+ echo ' JAVA: java'
JAVA: java
+ echo ''
+ echo ' JAVA_OPTS: -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'
JAVA_OPTS: -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED
+ echo ''
+ echo =========================================================================
=========================================================================
+ echo ''
+ true
+ '[' x1 = x ']'
+ eval '"java"' '-D"[Standalone]"' -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED '"-Dorg.jboss.boot.log.file=/opt/jboss/keycloak/standalone/log/server.log"' '"-Dlogging.configuration=file:/opt/jboss/keycloak/standalone/configuration/logging.properties"' -jar '"/opt/jboss/keycloak/jboss-modules.jar"' -mp '"/opt/jboss/keycloak/modules"' org.jboss.as.standalone '-Djboss.home.dir="/opt/jboss/keycloak"' '-Djboss.server.base.dir="/opt/jboss/keycloak/standalone"' ' '\''-Djboss.bind.address=172.17.0.2'\'' '\''-Djboss.bind.address.private=172.17.0.2'\'' '\''-c=standalone-ha.xml'\''' '&'
+ JBOSS_PID=122
+ trap 'kill -HUP 122' HUP
++ java '-D[Standalone]' -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED -Dorg.jboss.boot.log.file=/opt/jboss/keycloak/standalone/log/server.log -Dlogging.configuration=file:/opt/jboss/keycloak/standalone/configuration/logging.properties -jar /opt/jboss/keycloak/jboss-modules.jar -mp /opt/jboss/keycloak/modules org.jboss.as.standalone -Djboss.home.dir=/opt/jboss/keycloak -Djboss.server.base.dir=/opt/jboss/keycloak/standalone -Djboss.bind.address=172.17.0.2 -Djboss.bind.address.private=172.17.0.2 -c=standalone-ha.xml
+ trap 'kill -TERM 122' INT
+ trap 'kill -QUIT 122' QUIT
+ trap 'kill -PIPE 122' PIPE
+ trap 'kill -TERM 122' TERM
+ '[' x '!=' x ']'
+ WAIT_STATUS=128
+ '[' 128 -ge 128 ']'
+ wait 122
18:08:24,393 INFO [org.jboss.modules] (main) JBoss Modules version 1.11.0.Final
18:08:25,034 INFO [org.jboss.msc] (main) JBoss MSC version 1.4.12.Final
18:08:25,050 INFO [org.jboss.threads] (main) JBoss Threads version 2.4.0.Final
18:08:25,219 INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) starting
18:08:25,412 INFO [org.jboss.vfs] (MSC service thread 1-4) VFS000002: Failed to clean existing content for temp file provider of type temp. Enable DEBUG level log to find what caused this
18:08:26,228 INFO [org.wildfly.security] (ServerService Thread Pool -- 22) ELY00001: WildFly Elytron version 1.15.3.Final
^C
bash-4.4$ exit
$
In the huge starting log, we can see the following command, starting with ++ java
:
java '-D[Standalone]' -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED -Dorg.jboss.boot.log.file=/opt/jboss/keycloak/standalone/log/server.log -Dlogging.configuration=file:/opt/jboss/keycloak/standalone/configuration/logging.properties -jar /opt/jboss/keycloak/jboss-modules.jar -mp /opt/jboss/keycloak/modules org.jboss.as.standalone -Djboss.home.dir=/opt/jboss/keycloak -Djboss.server.base.dir=/opt/jboss/keycloak/standalone -Djboss.bind.address=172.17.0.2 -Djboss.bind.address.private=172.17.0.2 -c=standalone-ha.xml
This is the java
command we will put inside our Dockerfile
, as an ENTRYPOINT
to make Keycloak start.
FROM jboss/keycloak:13.0.1 as base
FROM gcr.io/distroless/java:11-nonroot
COPY --chown=nonroot:nonroot --from=base /opt/jboss /opt/jboss
ENTRYPOINT [ "java", "-D[Standalone]", "-server", "-Xms64m", "-Xmx512m", "-XX:MetaspaceSize=96M", "-XX:MaxMetaspaceSize=256m", "-Djava.net.preferIPv4Stack=true", "-Djboss.modules.system.pkgs=org.jboss.byteman", "-Djava.awt.headless=true", "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED", "--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED", "--add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED", "-Dorg.jboss.boot.log.file=/opt/jboss/keycloak/standalone/log/server.log", "-Dlogging.configuration=file:/opt/jboss/keycloak/standalone/configuration/logging.properties", "-jar", "/opt/jboss/keycloak/jboss-modules.jar", "-mp", "/opt/jboss/keycloak/modules", "org.jboss.as.standalone", "-Djboss.home.dir=/opt/jboss/keycloak", "-Djboss.server.base.dir=/opt/jboss/keycloak/standalone", "-Djboss.bind.address=0.0.0.0", "-Djboss.bind.address.private=1720.0.0.0", "-c=standalone.xml" ]
NOTE: You can tune this command to increase or decrease the memory setup, the private/public bind address of your keycloak instance and many other parameters. Here, we changed the configuration file used (-c=standalone.xml
instead of -c=standalone-ha.xml
for simplicity reasons) and the bound ip adresses (to 0.0.0.0
)
If we build and run this, we will be able to access the Keycloak UI:
$ docker build -t keycloak-distroless .
[+] Building 0.6s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for gcr.io/distroless/java:11-nonroot 0.5s
=> [internal] load metadata for docker.io/jboss/keycloak:13.0.1 0.0s
=> [base 1/1] FROM docker.io/jboss/keycloak:13.0.1 0.0s
=> [stage-1 1/2] FROM gcr.io/distroless/java:11-nonroot@sha256:07d017944 0.0s
=> CACHED [stage-1 2/2] COPY --chown=nonroot:nonroot --from=base /opt/jb 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:100908720c19018f2408bb53a5d78ef3d9eb51391b165 0.0s
=> => naming to docker.io/library/keycloak-distroless 0.0s
$ docker run --rm -it -p 8080:8080 keycloak-distroless
18:15:22,645 INFO [org.jboss.modules] (main) JBoss Modules version 1.11.0.Final
18:15:23,283 INFO [org.jboss.msc] (main) JBoss MSC version 1.4.12.Final
18:15:23,292 INFO [org.jboss.threads] (main) JBoss Threads version 2.4.0.Final
18:15:23,452 INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) starting
18:15:23,694 INFO [org.jboss.vfs] (MSC service thread 1-5) VFS000002: Failed to clean existing content for temp file provider of type temp. Enable DEBUG level log to find what caused this
18:15:24,457 INFO [org.wildfly.security] (ServerService Thread Pool -- 22) ELY00001: WildFly Elytron version 1.15.3.Final
...
...
18:15:44,642 INFO [org.wildfly.extension.undertow] (ServerService Thread Pool -- 66) WFLYUT0021: Registered web context: '/auth' for server 'default-server'
18:15:44,778 INFO [org.jboss.as.server] (ServerService Thread Pool -- 46) WFLYSRV0010: Deployed "keycloak-server.war" (runtime-name : "keycloak-server.war")
18:15:44,886 INFO [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
18:15:44,892 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) started in 22800ms - Started 692 of 977 services (686 services are lazy, passive or on-demand)
18:15:44,896 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
18:15:44,896 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
If we try to access http://localhost:8080/
, we can see the following page 🎉.
This is a good start, but this is just the minimal setup with H2
database, we often want something more robust for production!
Generating the perfect configuration
The jboss/keycloak
image use a lot of environment variables to configure keycloak (and the underlying standalone.xml
) for you… but in our case, we can't use that because:
- We don't have a
shell
to run those scripts. - We don't want to run those scripts at every startup / scale-up.
So, we will have to steal the generated standalone.xml
file from the original container, post start-up, and include it in our container. For this example, I will use PostgreSQL
as our main database.
To do this, I will use two shells side-by-side, one to launch Keycloak, and the other one to fetch the configuration.
# In the first shell
# Creation of a docker network
first-shell$ docker network create keycloak-network
4da77163731b584bef2c6d0b00386b9d62e31fa216204c6c6795f66e109ba1a6
# Launching PostgreSQL linked to the network previously created
first-shell$ docker run --rm -d --name postgres --net keycloak-network \
-e POSTGRES_DB=keycloak \
-e POSTGRES_USER=keycloak \
-e POSTGRES_PASSWORD=password postgres
229816da42707e772542f1b089c616a2333a6fbe1aea2be7efe658d6f2c934a1
first-shell$ docker run -it --rm --name keycloak \
-e DB_ADDR=postgres \
-e DB_USER=keycloak \
-e DB_PASSWORD=password \
-e KEYCLOAK_USER=foo \
-e KEYCLOAK_PASSWORD=bar \
--net keycloak-network jboss/keycloak:13.0.1
=========================================================================
Using PostgreSQL database
=========================================================================
18:32:25,172 INFO [org.jboss.modules] (CLI command executor) JBoss Modules version 1.11.0.Final
18:32:25,279 INFO [org.jboss.msc] (CLI command executor) JBoss MSC version 1.4.12.Final
18:32:25,302 INFO [org.jboss.threads] (CLI command executor) JBoss Threads version 2.4.0.Final
18:32:25,453 INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) starting
...
18:32:59,128 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
18:32:59,129 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
In another shell, while the previous is still running, we will execute the following command to get the standalone.xml
file used to configure Keycloak:
second-shell$ docker cp keycloak:/opt/jboss/keycloak/standalone/configuration/standalone.xml .
second-shell$ ls
standalone.xml
# We can now stop the keycloak container
second-shell$ docker stop keycloak
keycloak
second-shell$
Now, we will start the Distroless Keycloak and mount the standalone.xml
inside the container.
$ docker run --rm -it -e DB_USER=keycloak -e DB_PASSWORD=password --net keycloak-network -v $(pwd)/standalone.xml:/opt/jboss/keycloak/standalone/configuration/standalone.xml -p 8080:8080 keycloak-distroless
19:42:20,707 INFO [org.jboss.modules] (main) JBoss Modules version 1.11.0.Final
19:42:21,317 INFO [org.jboss.msc] (main) JBoss MSC version 1.4.12.Final
19:42:21,329 INFO [org.jboss.threads] (main) JBoss Threads version 2.4.0.Final
19:42:21,470 INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) starting
19:42:21,651 INFO [org.jboss.vfs] (MSC service thread 1-1) VFS000002: Failed to clean existing content for temp file provider of type temp. Enable DEBUG level log to find what caused this
19:42:22,577 INFO [org.wildfly.security] (ServerService Thread Pool -- 20) ELY00001: WildFly Elytron version 1.15.3.Final
...
19:43:58,356 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 13.0.1 (WildFly Core 15.0.1.Final) started in 17828ms - Started 595 of 873 services (584 services are lazy, passive or on-demand)
19:43:58,362 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
19:43:58,363 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
And Voila!
What about security?
The original and main purpose of this manipulation is to reduce the number of CVEs present in our image. We will be able to compare it using trivy again on our newly image.
$ trivy image keycloak-distroless
2021-05-26T21:11:15.959+0200 INFO Detected OS: debian
2021-05-26T21:11:15.959+0200 INFO Detecting Debian vulnerabilities...
2021-05-26T21:11:15.963+0200 INFO Number of PL dependency files: 621
2021-05-26T21:11:15.963+0200 INFO Detecting jar vulnerabilities...
keycloak-distroless (debian 10.9)
=================================
Total: 27 (UNKNOWN: 0, LOW: 23, MEDIUM: 3, HIGH: 1, CRITICAL: 0)
We can see, our image contain fewer vulnerabilities, at LOW
, MEDIUM
or HIGH
level. Again, this depends on when you are doing this analysis. With the solution provided in this article, you'll be able to rebuild your keycloak on a new, up-to-date, Distroless base image without updating keycloak. With the original keycloak image, the keycloak version is tied to the OS version (and security flaws).
NOTE: The jboss/keycloak:13.0.1
was released few hours before the creation of this article while the distroless/java-debian10:non-root
was released 1 month ago. This is the worst comparison scenario possible for the Distroless base image.
Another benefit of this alternative is to create a smaller image for keycloak. The previous dive
reports stated 698 MB
for the official image when our custom image weight only 519 MB
, so around 179 MB
reduction 🏋️♂️, and I'm sure we can remove almost 100MB
by removing all useless binaries in the image (useless drivers, command line tools, documentation…).
Conclusion
With this article, you should be able to build, from the official jboss/keycloak
image a custom one based on the Distroless/java and even fix CVEs by doing it again when a new version of Distroless/java image is released.
I hope you liked it, you can find all the sample files from this article in this GitLab repository: davinkevin/keycloak-distroless.