From e07fd22d8145389d510e5c6a192d74dad86a1c11 Mon Sep 17 00:00:00 2001
From: Alex Szczuczko
Date: Mon, 6 Feb 2023 16:28:30 -0700
Subject: [PATCH] Minimize the RPM content of the Quarkus container
Even though we use `ubi8-minimal` as the parent of our container, it
still has many RPMs installed that aren't necessary to run the Keycloak
server. Also, since the JDK RPM (that we install on top of
`ubi8-minimal`) is designed for general use, it pulls in more dependency
RPMs than it strictly needs to, like cups and avahi. Keycloak will never
need to access a printer itself!
Trimming down these excess RPMs will improve our CVE statistics with
automated scanners, and therefore let us perform fewer CVE rebuilds.
`ubi8-null.sh` uses the low-level `rpm` command to identify and forcibly
remove dependencies and operating system files that are not required to
boot our Quarkus-based server. This includes `microdnf` and `rpm`
itself! I have preserved bash however, so it's still possible to debug
the container from a shell.
I've created an initial set of allow/disallow lists, that seems to pass
a smoke test (server boots, admin console works). This leaves 37
packages installed, with 96 removed relative to `ubi8-minimal`. We could
go more minimal than this, or less minimal if required. Trial and error
is required.
Closes #16902
---
.../controllers/KeycloakDeployment.java | 36 ++++---
.../integration/KeycloakDeploymentTest.java | 4 +-
quarkus/container/Dockerfile | 17 ++--
quarkus/container/ubi8-null.sh | 95 +++++++++++++++++++
.../it/utils/DockerKeycloakDistribution.java | 2 +
5 files changed, 125 insertions(+), 29 deletions(-)
create mode 100644 quarkus/container/ubi8-null.sh
diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java
index 8b5757201d90..0e81227ff38f 100644
--- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java
+++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java
@@ -20,8 +20,9 @@
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
-import io.fabric8.kubernetes.api.model.ExecActionBuilder;
+import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
@@ -399,26 +400,23 @@ private StatefulSet createBaseDeployment() {
var tlsConfigured = isTlsConfigured(keycloakCR);
var userRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY);
var kcRelativePath = (userRelativePath == null) ? "" : userRelativePath;
- var protocol = !tlsConfigured ? "http" : "https";
+ var protocol = !tlsConfigured ? "HTTP" : "HTTPS";
var kcPort = KeycloakService.getServicePort(keycloakCR);
- var baseProbe = new ArrayList<>(List.of("curl", "--head", "--fail", "--silent"));
-
- if (tlsConfigured) {
- baseProbe.add("--insecure");
- }
-
- var readyProbe = new ArrayList<>(baseProbe);
- readyProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/ready");
- var liveProbe = new ArrayList<>(baseProbe);
- liveProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/live");
-
- container
- .getReadinessProbe()
- .setExec(new ExecActionBuilder().withCommand(readyProbe).build());
- container
- .getLivenessProbe()
- .setExec(new ExecActionBuilder().withCommand(liveProbe).build());
+ container.getReadinessProbe().setHttpGet(
+ new HTTPGetActionBuilder()
+ .withScheme(protocol)
+ .withPort(new IntOrString(kcPort))
+ .withPath(kcRelativePath + "/health/ready")
+ .build()
+ );
+ container.getLivenessProbe().setHttpGet(
+ new HTTPGetActionBuilder()
+ .withScheme(protocol)
+ .withPort(new IntOrString(kcPort))
+ .withPath(kcRelativePath + "/health/live")
+ .build()
+ );
return baseDeployment;
}
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
index 903bc8b94c1d..5de704a1428b 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java
@@ -493,7 +493,7 @@ public void testHttpRelativePathWithPlainValue() {
.list()
.getItems();
- assertTrue(pods.get(0).getSpec().getContainers().get(0).getReadinessProbe().getExec().getCommand().stream().collect(Collectors.joining()).contains("foobar"));
+ assertTrue(pods.get(0).getSpec().getContainers().get(0).getReadinessProbe().getHttpGet().getPath().contains("foobar"));
} catch (Exception e) {
savePodLogs();
throw e;
@@ -529,7 +529,7 @@ public void testHttpRelativePathWithSecretValue() {
.list()
.getItems();
- assertTrue(pods.get(0).getSpec().getContainers().get(0).getReadinessProbe().getExec().getCommand().stream().collect(Collectors.joining()).contains("barfoo"));
+ assertTrue(pods.get(0).getSpec().getContainers().get(0).getReadinessProbe().getHttpGet().getPath().contains("barfoo"));
} catch (Exception e) {
savePodLogs();
throw e;
diff --git a/quarkus/container/Dockerfile b/quarkus/container/Dockerfile
index d965cd3435e6..f7af70ad7cbc 100644
--- a/quarkus/container/Dockerfile
+++ b/quarkus/container/Dockerfile
@@ -1,9 +1,9 @@
-FROM registry.access.redhat.com/ubi8-minimal AS build-env
+FROM registry.access.redhat.com/ubi8 AS ubi-micro-build
ENV KEYCLOAK_VERSION 999-SNAPSHOT
ARG KEYCLOAK_DIST=https://github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.tar.gz
-RUN microdnf install -y tar gzip
+RUN dnf install -y tar gzip
ADD $KEYCLOAK_DIST /tmp/keycloak/
@@ -14,17 +14,18 @@ RUN (cd /tmp/keycloak && \
rm /tmp/keycloak/keycloak-*.tar.gz) || true
RUN mv /tmp/keycloak/keycloak-* /opt/keycloak && mkdir -p /opt/keycloak/data
-
RUN chmod -R g+rwX /opt/keycloak
-FROM registry.access.redhat.com/ubi8-minimal
+ADD ubi8-null.sh /tmp/
+RUN bash /tmp/ubi8-null.sh java-17-openjdk-headless glibc-langpack-en
+
+FROM registry.access.redhat.com/ubi8-micro
ENV LANG en_US.UTF-8
-COPY --from=build-env --chown=1000:0 /opt/keycloak /opt/keycloak
+COPY --from=ubi-micro-build /tmp/null/rootfs/ /
+COPY --from=ubi-micro-build --chown=1000:0 /opt/keycloak /opt/keycloak
-RUN microdnf update -y && \
- microdnf install -y --nodocs java-17-openjdk-headless glibc-langpack-en && microdnf clean all && rm -rf /var/cache/yum/* && \
- echo "keycloak:x:0:root" >> /etc/group && \
+RUN echo "keycloak:x:0:root" >> /etc/group && \
echo "keycloak:x:1000:0:keycloak user:/opt/keycloak:/sbin/nologin" >> /etc/passwd
USER 1000
diff --git a/quarkus/container/ubi8-null.sh b/quarkus/container/ubi8-null.sh
new file mode 100644
index 000000000000..73654f8e3a0e
--- /dev/null
+++ b/quarkus/container/ubi8-null.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+set -euo pipefail
+#set -x
+
+dir="/tmp/null"
+rm -rf "$dir"
+mkdir "$dir"
+cd "$dir"
+
+# Add all arguments as the initial core packages
+printf '%s\n' "$@" > keep
+# Packages required for a shell environment
+cat >>keep <disallow < Installing packages into chroot" >&2
+
+set -x
+# Install requirements for this script (xargs and cmp)
+dnf install -y findutils diffutils
+# Install core packages to chroot
+rootfs="$(realpath rootfs)"
+mkdir -p "$rootfs"
+/dev/null
+
+echo "==> Building dependency tree" >&2
+# Loop until we have the full dependency tree (no new packages found)
+touch old
+while ! cmp -s keep old
+do
+ # 1. Get requirement names (not quite the same as package names)
+ # 2. Filter out any install-time requirements
+ # 3. Query which packages are being used to satisfy the requirements
+ # 4. Keep just their package names
+ # 5. Remove packages that are on the disallow list
+ # 6. Store result as an allowlist
+ new
+
+ # Safely replace the keep list, appending the new names
+ mv keep old
+ cat old new > keep
+ # Sort and deduplicate so cmp will eventually return true
+ sort -u keep -o keep
+done
+
+# Determine all packages that need to be removed
+rpm -r "$rootfs" -qa | sed -r 's/^(.*)-.*-.*$/\1/' | sort -u > all
+# Set complement (all - keep)
+grep -vxF -f keep all > remove
+
+echo "==> $(wc -l remove | cut -d ' ' -f1) packages to erase:" >&2
+cat remove
+echo "==> $(wc -l keep | cut -d ' ' -f1) packages to keep:" >&2
+cat keep
+echo "" >&2
+
+echo "==> Erasing packages" >&2
+# Delete all packages that aren't needed for the core packages
+set -x
+/dev/null
+
+echo "" >&2
+echo "==> Packages erased ok!" >&2
diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java
index 6fd9d1eb43e6..f900ddfb8036 100644
--- a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java
+++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java
@@ -30,6 +30,7 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
private File distributionFile = new File("../../dist/target/keycloak-" + Version.VERSION + ".tar.gz");
private File dockerFile = new File("../../container/Dockerfile");
+ private File dockerScriptFile = new File("../../container/ubi8-null.sh");
private GenericContainer> keycloakContainer = null;
private String containerId = null;
@@ -48,6 +49,7 @@ private GenericContainer getKeycloakContainer() {
return new GenericContainer(
new ImageFromDockerfile("keycloak-under-test", false)
.withFileFromFile("keycloak.tar.gz", distributionFile)
+ .withFileFromFile("ubi8-null.sh", dockerScriptFile)
.withFileFromFile("Dockerfile", dockerFile)
.withBuildArg("KEYCLOAK_DIST", "keycloak.tar.gz")
)