DEV Community

Cover image for Keycloak on Distroless
Kevin Davin for Stack Labs

Posted on • Edited on

Keycloak on Distroless

⚠️ 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)
...
Enter fullscreen mode Exit fullscreen mode

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.

Nothing in distroless

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/.

dive

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
...
Enter fullscreen mode Exit fullscreen mode

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
$ 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If we try to access http://localhost:8080/, we can see the following page 🎉.

keycloak-ui-from-distroless

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 
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And Voila!

keycloak-auth
keycloak-login
keycloak-ui

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)
Enter fullscreen mode Exit fullscreen mode

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.

dive-distroless

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.

Top comments (4)

Collapse
 
nfrankel profile image
Nicolas Fränkel

Thanks for the post. Are you aware that Jib (made by Google) is moving away from distroless?

I seem to remember distroless won't be actively maintained anymore but I failed to find where I read it.

Collapse
 
johnniepop profile image
Ivan Popov

Any ideas for what they will replace it with?

Collapse
 
nfrankel profile image
Nicolas Fränkel

Nope

Collapse
 
johnniepop profile image
Ivan Popov

Hi,

Any progress on the Quarkus version of Keycloak ... if you still have interest in it?

I myself tried with Keycloak 19.0.3 (and 20.0.0 at some point) but I got stuck on the entrypoint step ...
From their own example it gets clear what the entrypoint is but after adding it to the Dockerfile (the way they show) the container gets built but doesn't run. docker run exits with just one line of Go error:

standard_init_linux.go:219: exec user process caused: no such file or directory

Looking at docker's events is not a single bit more helpful. I tried a few more things with no result and my ideas start to get exhausted ...