Skip to content

security(k8s): disposition CKV_K8S_40 β€” 11 findings whose UID is load-bearingΒ #2898

Closed as not planned
1 of 1 issue completed
Closed as not planned

Description

@devantler

πŸ€– 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_40 reports 0 on k8s/.
  • 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.

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. coderabbitai commented on Aug 2, 2026

    @coderabbitai
    Contributor
    πŸ”— Related PRs

    #2344 - fix(security-context): pair pod-level runAsUser with injected runAsGroup (#2292 regression) [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 plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    πŸ§ͺ 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:

    issue_enrichment:
      auto_enrich:
        enabled: false

    πŸ’¬ Have feedback or questions? Drop into our discord!

  3. moved this to πŸƒπŸ»β€β™‚οΈ In Progress in 🌊 Project Boardon Aug 2, 2026
  4. moved this from πŸƒπŸ»β€β™‚οΈ In Progress to πŸ‘€ In Review in 🌊 Project Boardon Aug 2, 2026
  5. devantler commented on Aug 2, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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 β†’ 26 to 37 β†’ 28, with CKV_K8S_40 at 2 rather than 0.

    The mechanism, probed rather than assumed β€” five Job manifests scanned together, each carrying a
    different directive:

    arm directive CKV_K8S_40
    A none fails
    B checkov.io/skip1: CKV_K8S_40=… cleared
    C checkov.io/skip1: CKV_K8S_22=… (wrong id) still fails β€” and CKV_K8S_22 cleared instead
    D an arbitrary unrelated annotation still fails
    E # checkov:skip=CKV_K8S_40:… comment form still fails

    Two results worth recording. The kubernetes framework's skip is id-scoped β€” unlike the
    secrets framework, which #2892 measured as keying on the mere presence of a directive. And the
    comment form is a no-op here; only the metadata.annotations form works, which is what the repo
    already 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 the
    repo already adjusts them through kustomize patches: rather than editing them, so an inline
    annotation 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

  6. devantler commented on Aug 2, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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:

    Class in the table Status
    Image-baked identity β†’ openbao Γ—3 β€” "vault-snapshots is an existing PVC whose files are owned 100:1000" Refuted as a constraint. The live OpenBao StatefulSet mounts config/unseal-keys/tmp/home plus claim templates data/audit β€” never vault-snapshots. That PVC is ReadWriteOnce, so it cannot be co-mounted by the StatefulSet at all.
    Measurement subject β†’ userns-headlamp-mapping-probe Refuted. /proc/self/uid_map describes 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.
    Ownership-semantics test β†’ userns-longhorn-smoke Refuted. fsGroup is a GID and supplies volume access independently of runAsUser.

    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.

  7. devantler commented on Aug 5, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    The coredns row of the disposition table is refuted β€” measured, not argued

    The table classifies coredns (docker provider) as root by necessity, because
    "NET_BIND_SERVICE is not effective for non-root without ambient capabilities". That premise does
    not 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 /coredns binary, which an unprivileged process retains as long as NET_BIND_SERVICE is in the
    container'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 that
    image's /etc/passwd maps to nonroot:x:65532:65532. It carries the identical
    add: [NET_BIND_SERVICE] / drop: [ALL] plus readOnlyRootFilesystem: true, exposes containerPort
    53 on UDP and TCP, and both pods are ready endpoints of the kube-dns Service. Deployment is 74
    days 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_40 fires only because checkov reads the spec rather than the image.

    Consequence for this issue

    Stating runAsUser: 65532 declares the UID the container already assumes β€” no behaviour change β€”
    and clears both CKV_K8S_40 and CKV_K8S_23 for that workload. Verified single-variable: the
    failing check-id set diffed across that one file, CKV_K8S_40 on k8s/ 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_40 sites are the two vault-backup jobs and
    userns-longhorn-smoke (#2904's withdrawn skips), plus the vault-config Job, which per the
    .mega-linter.yml note has no demonstrated constraint either β€” worth checking the same way rather
    than assuming, since the image decides is this backlog's own established method.

  8. added a commit that references this issue on Aug 10, 2026
  9. devantler commented on Aug 10, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    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 coredns row on 2026-08-05, so the table can no longer be worked from. #2904 was opened to
    carry 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:

    Outcome Sites Where
    Vendored bundles, resource-scoped annotations 2 #2899
    Dispositioned per-image (minio Γ—2 raised, umami scoped skip) 3 #2901
    CoreDNS raised to 65532 1 #2901
    Headlamp mapping probe removed 1 #2959
    userns-longhorn-smoke raised to 65532 1 #3047
    vault-config scoped skip β€” image-baked openbao:x:100:1000 1 #3051
    Remaining β€” the two vault-backup writers 2 #2904

    Two results from here are carried forward rather than closed with it. The skip-mechanism matrix
    established above β€” the kubernetes framework keys on the check id a directive names, unlike the
    presence-keyed secrets framework (#2892) β€” is now recorded in .mega-linter.yml with its
    measurement, 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-snapshots PVC, which the live StatefulSet does not mount). The
    class label itself turned out to be right for vault-config on different evidence β€” the openbao
    image bakes openbao:x:100:1000::/home/openbao in its own /etc/passwd. A refuted reason is not
    the same as a refuted conclusion, which is worth separating when reading the rest of this thread.

  10. moved this from πŸ‘€ In Review to βœ… Done in 🌊 Project Boardon Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Fields

No fields configured for Security.

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions