fix(keycloak): group 1000 instead of the image's root group
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Keycloak Helm Chart / Publish Keycloak Helm Chart (push) Successful in 7s
SonarQube Analysis / SonarQube Analysis (push) Successful in 28s

The first Trivy run on this chart reported one finding out of 96 tests:
AVD-KSV-0116, "Runs with a root primary or supplementary GID", because the pod
context carried runAsGroup: 0 - copied from what the image expects.

Checked instead of suppressed, and the check says the group is not needed.
Counted across the image layer of quay.io/keycloak/keycloak:26.8.0: 521 of 522
entries are owned by 1000:0, and every mode present (drwxrwxr-x, -rwxrwxr-x,
-rw-rw-r--) grants the OWNER full access. The one remaining entry is /opt
itself, which needs only traverse. So uid 1000 gets everything it reads and
executes through the owner bits, whatever group it is in.

Group 0 is upstream's OpenShift arbitrary-uid pattern - root group plus a
group-writable tree, so that ANY uid landing in group 0 can write. It buys
nothing once the owning uid is pinned, which this chart does. Writes all go
into the three emptyDirs, which fsGroup 1000 makes writable.

So runAsGroup is 1000, the finding is gone rather than excepted, and no entry
was added to ci/trivy/trivyignore.yaml - which matters more than one LOW
finding: that file scopes by chart, so an AVD-KSV-0116 exception for keycloak
would also have silenced it for the serving container, on a check that is
making a fair point about an identity provider.

The condition under which this goes back to 0 is written at the value: a path
uid 1000 does not own. There is none in this image today.

Part of #83
This commit was merged in pull request #87.
This commit is contained in:
jklein authored and Justin Klein committed 2026-10-04 18:00:20 +00:00
1 parent 320257c742
commit 28567842f3
3 files changed
+27 -12

No files matched your search

+1 -1
View File
@@ -353,7 +353,7 @@ chart's defaults, not the repo standard - see the uid/gid note.
{{- $defaults := dict
"runAsNonRoot" true
"runAsUser" 1000
"runAsGroup" 0
"runAsGroup" 1000
"fsGroup" 1000
"fsGroupChangePolicy" "Always"
"seccompProfile" (dict "type" "RuntimeDefault") -}}
+1 -1
View File
@@ -248,7 +248,7 @@
}
},
"securityContext": {
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, which deviate from the repo's usual 20000/1000 pair - the published image declares keycloak:x:1000:0 in /etc/passwd, puts that user in the ROOT GROUP and copies the whole install tree with --chown=1000:0, so uid 1000 / gid 0 is what the image expects and what makes the mounted build volume writable. Every container in the pod, both init containers included, uses the same container context: readOnlyRootFilesystem: true holds throughout, which is the reason the chart has two init containers rather than one writable one.",
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, which deviate from the repo's usual 20000/1000 pair: the published image declares keycloak:x:1000:0 in /etc/passwd and copies the whole install tree with --chown=1000:0, so uid 1000 is the OWNER of everything the process reads. The GROUP is 1000 and not the image's own 0 - counted across the image layer, 521 of 522 entries are owned by 1000:0 with every mode granting the owner full access, so group 0 (upstream's OpenShift arbitrary-uid pattern) buys nothing once the owning uid is pinned, and leaving it out keeps a root GID out of the pod. Every container in the pod, both init containers included, uses the same container context: readOnlyRootFilesystem: true holds throughout, which is the reason the chart has two init containers rather than one writable one.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
+25 -10
View File
@@ -418,18 +418,33 @@ keycloak:
securityContext:
pod:
runAsNonRoot: true
# 1000 AND GROUP 0, which is not this repo's 20000/1000 default and not a
# free choice: the published image declares "keycloak:x:1000:0" in
# /etc/passwd, adds that user to the ROOT GROUP in /etc/group, and copies
# the whole install tree with --chown=1000:0. Everything under
# /opt/keycloak is therefore owned by 1000:0 and group-writable, and the
# emptyDir mounted over /opt/keycloak/lib/quarkus has to be writable by
# the same pair or the build writes nothing.
# uid 1000, NOT this repo's 20000, and that is the image's requirement
# rather than a free choice: it declares "keycloak:x:1000:0" in
# /etc/passwd and copies the whole install tree with --chown=1000:0, so
# 1000 is the OWNER of everything the process reads and executes. Read
# off the image rather than guessed - `USER 1000` and the chown are in
# the config blob's history of quay.io/keycloak/keycloak:26.8.0.
#
# Read off the image rather than guessed: `USER 1000` and the chown are
# in the config blob's history of quay.io/keycloak/keycloak:26.8.0.
# GROUP 1000 AND NOT 0, WHICH IS WHERE THIS CHART DIVERGES FROM THE
# IMAGE'S OWN CONVENTION - deliberately, and measured before deciding.
# Upstream puts the keycloak user in the ROOT GROUP and makes the install
# tree group-writable (mode 775/664, owner 1000, group 0). That is the
# OpenShift arbitrary-uid pattern: it lets ANY uid that happens to be in
# group 0 write there. We do not need it, because we pin the uid that
# OWNS the files - counted across the image layer, 521 of 522 entries are
# owned by 1000:0 and every mode grants the owner full access, so uid
# 1000 gets what it needs through the owner bits whatever its group is.
# The one remaining entry is /opt itself, which needs only traverse.
#
# What that buys: no root primary or supplementary GID in this pod, i.e.
# no AVD-KSV-0116 finding and no suppression for it. A suppression would
# have had to cover the whole chart, serving container included, to
# silence a check that is making a fair point.
#
# If this is ever changed back to 0, the reason has to be a path that
# uid 1000 does NOT own - and there is none in this image today.
runAsUser: 1000
runAsGroup: 0
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
seccompProfile: