Repository navigation
Description
π€ Generated by the Agentic Engineer
Evidence
CKV_K8S_40 is the largest single check left in #2787's remaining backlog β 11 of 37 findings
(30%), confirmed live this run with scripts/megalinter-scan-counts.sh (checkov 37, kubernetes
framework, secrets cleared by #2893).
The threshold, measured rather than read from docs. Five probe pods at
runAsUser 999 / 1000 / 9999 / 10000 / 65532, scanned together:
| runAsUser | CKV_K8S_40 |
|---|---|
| 999 | fails |
| 1000 | fails |
| 9999 | fails |
| 10000 | passes |
| 65532 | passes |
Control: every probe file produced 13β14 other failing checks, so all five were genuinely scanned
and the two passes are passes, not silent skips. The threshold is runAsUser >= 10000.
Why this is not a "Kyverno injects it later" family
#2787 asks this question once, at the top, for roughly a third of the remainder. For this control
the answer is no, and for a sharper reason than expected: add-security-context does inject
runAsUser, but it injects 1000 β which is on the failing side of the threshold. So the
cluster-wide default cannot satisfy this check, and the findings are not a pre-mutation artifact.
Why every one of the 11 is irreducible, by class
Each site's UID is load-bearing, and the reasons differ β so this needs per-site dispositions with
their own reasons, not one templated skip:
| Class | Sites | Why the UID cannot move |
|---|---|---|
| Vendored upstream release manifests | cdi-operator, kubevirt-operator |
Multi-document upstream bundles; any edit is reverted by the next vendor bump |
| Root by necessity | coredns (docker provider) |
Binds :53; the in-file comment records that NET_BIND_SERVICE is not effective for non-root without ambient capabilities |
| Image-baked identity | openbao Γ3 (UID 100), umami (1001), minio Γ2 (1000) |
The UID is the image's own user; overriding it breaks ownership of paths baked into the image, and vault-snapshots is an existing PVC whose files are owned 100:1000 |
| Measurement subject | userns-headlamp-mapping-probe (100/101) |
The probe exists to read the idmap applied to headlamp's own UID/GID pair; changing it measures something else |
| Ownership-semantics test | userns-longhorn-smoke (1000) |
Asserts PVC ownership under fsGroup: 1000 |
The residual risk, and what actually covers it
The risk CKV_K8S_40 names is host UID collision. On this cluster the mitigation is user
namespaces (hostUsers: false), which remaps container UIDs into an unprivileged host range β
strictly stronger than a high in-container UID, and already rolling out (#2651, #2843). A high UID
would be a weaker substitute for a control we are adopting anyway.
Smallest useful change
Give each of the 11 a scoped disposition carrying its own true reason β never a blanket
skip-check in .checkov.yml, per #2787 β using whichever inline mechanism checkov honours for
the kubernetes framework, verified empirically before use (#2893's CKV_SECRET_6 work showed the
secrets framework keys on the presence of a directive rather than the id it names, so the
mechanism must be probed, not assumed).
Where a site cannot carry an inline directive without hand-editing a vendored file, the disposition
is a path-scoped exclusion with the same stated reason, not an edit to the vendored bundle.
Success signal
checkov's kubernetes framework drops 37 β 26, with CKV_K8S_40 at 0, and no other check's
count moves. Ablation: removing any one disposition restores exactly one finding.
Acceptance criteria
-
CKV_K8S_40reports 0 onk8s/. - Every disposition names the specific reason for that site; no blanket
skip-check. - No vendored upstream manifest is hand-edited.
-
.mega-linter.yml's recorded checkov count is refreshed to the new total. - A control proves the suppressions are doing the work (an unrelated/no-op directive must leave
the findings standing), so an incidental parser perturbation cannot pass as a fix.
Rough size: small. Part of #2787.
Metadata
Metadata
Assignees
Labels
Type
Fields
Projects
- StatusShow more project fieldsβ Done
Activity
coderabbitai commentedon Aug 2, 2026
π Related PRs
#2344 - fix(security-context): pair pod-level runAsUser with injected runAsGroup (
#2292regression) [merged]#2437 - fix(cilium): run the cilium-operator as non-root [merged]
#2455 - fix(security): harden containers with read-only root filesystems (Kubescape C-0017) [merged]
#2569 - feat(policy): opt proven stateless apps into user namespaces [merged]
#2860 - security(k8s): record an explicit service-account-token verdict per workload [merged]
π Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.π§ͺ Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:π¬ Have feedback or questions? Drop into our discord!
devantler commentedon Aug 2, 2026
Scope split after measuring β 9 here, 2 elsewhere
Building this surfaced a constraint the issue body assumed away, so the success signal moves from
37 β 26to37 β 28, withCKV_K8S_40at 2 rather than 0.The mechanism, probed rather than assumed β five Job manifests scanned together, each carrying a
different directive:
checkov.io/skip1: CKV_K8S_40=β¦checkov.io/skip1: CKV_K8S_22=β¦(wrong id)# checkov:skip=CKV_K8S_40:β¦comment formTwo results worth recording. The
kubernetesframework's skip is id-scoped β unlike thesecretsframework, which #2892 measured as keying on the mere presence of a directive. And thecomment form is a no-op here; only the
metadata.annotationsform works, which is what the repoalready uses for
CKV_K8S_38.Why the last two are out of scope. Both sit inside vendored upstream release bundles
(
cdi-operator.yaml,kubevirt-operator.yaml). Each has exactly one commit in its history and therepo already adjusts them through kustomize
patches:rather than editing them, so an inlineannotation would be reverted by the next vendor bump β silently. They also hold 16 of the 37
findings between them across eight check ids, i.e. 43% of #2787's remaining backlog, so the
convention for vendored manifests is worth deciding on its own: #2899.
Delivered in #2900
checkov 37 β 28,CKV_K8S_40 11 β 2, no other check's count moved.Ablation, all nine arms: removing any single disposition restores exactly one finding, at that
site's own resource, and restoring gives 0 again. Each arm asserts the directive was present before
removal and absent after, so an arm that failed to apply cannot pass as "the test does not
discriminate".
Separately, the manifests were shown to be otherwise unchanged: parsed before and after with
annotations stripped, they are identical β and that comparator was ablated against the specific
corruption at risk here (an annotation landing under the wrong parent key β detected; a value change
β detected; a comment-only edit β no false alarm).
Revised acceptance criteria
CKV_K8S_40reports 0 on every site that can carry an inline disposition (9 of 11).skip-check..mega-linter.yml's recorded checkov count refreshed 37 β 28.both leave the finding standing.
security(k8s): withdraw four CKV_K8S_40 skips whose reasons do not hold
devantler commentedon Aug 2, 2026
Correction β this issue's central premise is refuted, and #2900 no longer acts on it.
The title claims "11 findings whose UID is load-bearing" and the table above assigns each site to a class explaining why its UID cannot move. Three of those five classes do not hold, checked against the manifests and the live cluster:
openbao Γ3β "vault-snapshotsis an existing PVC whose files are owned100:1000"config/unseal-keys/tmp/homeplus claim templatesdata/auditβ nevervault-snapshots. That PVC isReadWriteOnce, so it cannot be co-mounted by the StatefulSet at all.userns-headlamp-mapping-probe/proc/self/uid_mapdescribes the pod's user namespace and reads identically for every process in it, whatever UID it runs as. The probe is a separate pod with its own range β as the file's own header comment already states.userns-longhorn-smokefsGroupis a GID and supplies volume access independently ofrunAsUser.The two remaining classes β vendored upstream bundles (
cdi-operator,kubevirt-operator) and CoreDNS binding:53β were not re-examined here and are tracked in #2899 and #2901.What survives is the closing section, not the table. The argument that user namespaces (
hostUsers: false, #2651/#2843) mitigate host UID collision more strongly than a high in-container UID is sound and untouched. But that is one platform-level risk acceptance, not 11 per-site technical constraints β and reconstructing it as per-manifest reasons is what produced four false ones. #2904 carries that decision.Leaving this issue open rather than closing it with #2900: the disposition question is real, only the proposed answer was wrong.
docs(k8s): correct a probe comment that restated the refuted UID claim
docs(ci): drop an over-strong RWO claim and fix the CoreDNS issue attβ¦
devantler commentedon Aug 5, 2026
The
corednsrow of the disposition table is refuted β measured, not arguedThe table classifies
coredns(docker provider) as root by necessity, because"
NET_BIND_SERVICEis not effective for non-root without ambient capabilities". That premise doesnot hold, so this site is not one of the irreducible findings.
Ambient capabilities are not what makes the bind work. The capability is a file capability on
the
/corednsbinary, which an unprivileged process retains as long asNET_BIND_SERVICEis in thecontainer's bounding set β which the container already adds. The deployment's own in-file comment
already says "a file capability on the binary rather than an ambient one"; the table row was not
updated to match.
And it is observable rather than inferred. The prod cluster's CoreDNS runs with no
runAsUser, so it runs as its image's declared user βUser: "nonroot:nonroot", which thatimage's
/etc/passwdmaps tononroot:x:65532:65532. It carries the identicaladd: [NET_BIND_SERVICE] / drop: [ALL]plusreadOnlyRootFilesystem: true, exposes containerPort53 on UDP and TCP, and both pods are ready endpoints of the
kube-dnsService. Deployment is 74days old; the pods have 0 restarts. A non-root CoreDNS binding :53 is therefore not hypothetical β
it is what has been answering cluster DNS all along.
The docker-provider site is the same: it also sets no
runAsUser, so it already runs as 65532.CKV_K8S_40fires only because checkov reads the spec rather than the image.Consequence for this issue
Stating
runAsUser: 65532declares the UID the container already assumes β no behaviour change βand clears both
CKV_K8S_40andCKV_K8S_23for that workload. Verified single-variable: thefailing check-id set diffed across that one file,
CKV_K8S_40onk8s/5 β 4, no new finding.So the count of genuinely irreducible sites is one lower than this issue records. #2974 carries the
site, #2975 the change. The remaining
CKV_K8S_40sites are the two vault-backup jobs anduserns-longhorn-smoke (#2904's withdrawn skips), plus the vault-config Job, which per the
.mega-linter.ymlnote has no demonstrated constraint either β worth checking the same way ratherthan assuming, since the image decides is this backlog's own established method.
docs(lint): credit #2898 for the skip-mechanism matrix
devantler commentedon Aug 10, 2026
Closing as superseded by #2904 β the remaining scope is identical, the framing here is not
This issue's central claim is "11 findings whose UID is load-bearing", and its disposition table
assigns each site a reason. Three of its five classes were refuted on this thread (2026-08-02) and
the
corednsrow on 2026-08-05, so the table can no longer be worked from. #2904 was opened tocarry the corrected framing and the open decision, and it is where the remainder lives.
Where the original 11 actually went, so the arithmetic is not lost with this issue:
userns-longhorn-smokeraised to 65532vault-configscoped skip β image-bakedopenbao:x:100:1000vault-backupwritersTwo results from here are carried forward rather than closed with it. The skip-mechanism matrix
established above β the
kubernetesframework keys on the check id a directive names, unlike thepresence-keyed
secretsframework (#2892) β is now recorded in.mega-linter.ymlwith itsmeasurement, since it is the control every future disposition on this check depends on. And the
threshold measurement (
runAsUser >= 10000) stands unrefuted.One correction to the record: this issue's image-baked class was refuted only in its stated
reason (a claimed shared
vault-snapshotsPVC, which the live StatefulSet does not mount). Theclass label itself turned out to be right for
vault-configon different evidence β the openbaoimage bakes
openbao:x:100:1000::/home/openbaoin its own/etc/passwd. A refuted reason is notthe same as a refuted conclusion, which is worth separating when reading the rest of this thread.
docs(lint): cite the demonstrated image-baked precedent, not the refuβ¦