Keycloak on Distroless
Tags: keycloak, docker, security, java
Published: 2021-05-28 06:04:52

---
title: Keycloak on Distroless
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsc5upw5ys3d5h6tt52p.jpeg
tags: keycloak,docker,security,java
series: Keycloak on Distroless
published: true
---

**⚠️** 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](https://kubernetes.io/).

The **Keycloak** image is available on the [DockerHub](https://hub.docker.com/r/jboss/keycloak/) or [Quay](https://quay.io/repository/keycloak/keycloak). 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.

```shell
$ 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](https://github.com/GoogleContainerTools/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](https://github.com/GoogleContainerTools/distroless/tree/main/java) image to sanitize our workload.

![Nothing in distroless](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qjax5xrl53le9h72piu.jpeg)

# 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](https://github.com/wagoodman/dive), we can see all **Keycloak** related files are stored into `/opt/jboss/`.

![dive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kyd201hv91x9uww7sku.png)

We will copy them into our distroless then, with the following `Dockerfile`:

```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:

```shell
$ 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:

```shell
$ 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](https://hub.docker.com/r/jboss/keycloak/) to launch keycloak, but we will log into the container to do our magic trick:

```shell
# 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.

```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

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:

```shell
$ 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 🎉.

![keycloak-ui-from-distroless](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4lk0ikvc8fpzw4yl1uby.png)

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.

```shell
# 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**:

```shell
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.

```shell
$ 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**!

![keycloak-auth](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l8o9no0uv4590wnqpnra.png)
![keycloak-login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jky6d1pt4milf243xcup.png)
![keycloak-ui](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6stjtu120ximoqrklovd.png)

# 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](https://github.com/aquasecurity/trivy) again on our newly image.

```shell
$ 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.

![dive-distroless](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xg69znq0rbkaxirtjxky.png)

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](https://gitlab.com/davinkevin/keycloak-distroless).