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
This commit is contained in:
parent
6e1a58adc6
commit
610e3044ad
5 changed files with 125 additions and 29 deletions
|
@ -20,8 +20,9 @@ import io.fabric8.kubernetes.api.model.Container;
|
|||
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 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -493,7 +493,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
.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 class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
.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;
|
||||
|
|
|
@ -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
|
||||
|
|
95
quarkus/container/ubi8-null.sh
Normal file
95
quarkus/container/ubi8-null.sh
Normal file
|
@ -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 <<EOF
|
||||
bash
|
||||
coreutils-single
|
||||
EOF
|
||||
|
||||
# Disallow list to block certain packages and their dependencies
|
||||
cat >disallow <<EOF
|
||||
alsa-lib
|
||||
copy-jdk-configs
|
||||
cups-libs
|
||||
chkconfig
|
||||
info
|
||||
gawk
|
||||
platform-python
|
||||
platform-python-setuptools
|
||||
python3-libs
|
||||
python3-pip-wheel
|
||||
python3-setuptools-wheel
|
||||
p11-kit
|
||||
sqlite-libs
|
||||
|
||||
EOF
|
||||
|
||||
sort -u keep -o keep
|
||||
|
||||
echo "==> 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"
|
||||
<keep xargs dnf install -y --installroot "$rootfs" --releasever 8 --setopt install_weak_deps=false --nodocs
|
||||
dnf --installroot "$rootfs" clean all
|
||||
rm -rf "$rootfs"/var/cache/* "$rootfs"/var/log/dnf* "$rootfs"/var/log/yum.*
|
||||
{ set +x; } 2>/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
|
||||
<keep xargs rpm -r "$rootfs" -q --requires | sort -Vu | cut -d ' ' -f1 \
|
||||
| grep -v -e '^rpmlib(' \
|
||||
| xargs -d $'\n' rpm -r "$rootfs" -q --whatprovides \
|
||||
| sed -r 's/^(.*)-.*-.*$/\1/' \
|
||||
| grep -vxF -f disallow \
|
||||
> 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
|
||||
<remove xargs rpm -r "$rootfs" --erase --nodeps --allmatches
|
||||
{ set +x; } 2>/dev/null
|
||||
|
||||
echo "" >&2
|
||||
echo "==> Packages erased ok!" >&2
|
|
@ -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 @@ public final class DockerKeycloakDistribution implements KeycloakDistribution {
|
|||
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")
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue