# Serves: R-SEC-02 (CI holds no broad cluster access) # # THIS POLICY IS THE SECURITY BOUNDARY. The RBAC in conformance/incluster/rbac.yaml is not. # # Kubernetes places NO `use` check on a Pod/Job's `serviceAccountName`. Any principal that can # `create jobs` in namespace `conformance` can therefore create a Job that runs as # `conformance-runner` — a privileged SA (nodes/patch, pods/exec, namespaces/patch) — with an # arbitrary command. So the CI trigger credential, however narrow its own Role looks, IS the # privileged identity unless something constrains the SHAPE of what it may create. # # That is what this does: for Jobs created by `conformance-trigger` ONLY, admit exactly the # committed conformance run and deny everything else. The operator chose this model # (docs/plans/l2-l3-conformance-ci-wiring.md §4, primary model) over the zero-credential # GitOps-native alternative. # # ENFORCE AND FAIL-CLOSED, NOT SUBSTITUTED. Note this does NOT use # ${KYVERNO_VALIDATION_ACTION} like the other admission policies: a security boundary that an # environment overlay can downgrade to Audit is not a boundary. `failurePolicy: Fail` means a # Kyverno outage blocks conformance Jobs rather than silently admitting unconstrained ones. # Blocking CI is the correct failure direction here; admitting arbitrary privileged code is not. # # SCOPE: it constrains ONLY `conformance-trigger`. Jobs created by humans, by Flux, or by any # other SA are untouched — so a broken policy cannot wedge the rest of the cluster. # # IMAGE PINNING — HONEST LIMIT, READ BEFORE RELYING ON THIS. The plan specifies pinning the # exact `CONFORMANCE_RUNNER_IMAGE` digest. That image does not exist yet, so this enforces the # enforceable half: the image must live under the conformance repository path AND be # digest-pinned (`@sha256:`), never a mutable tag. Residual surface: anyone who can PUSH to that # registry path could publish a different digest. TIGHTEN THIS when the image is built by # replacing the `image` pattern below with the literal digest — a one-line change, reviewed in a # PR, which is exactly where a change to the security boundary belongs. apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: conformance-trigger-job-shape annotations: policies.kyverno.io/title: Constrain what the conformance CI trigger may create policies.kyverno.io/category: Supply chain policies.kyverno.io/severity: critical homelab.keithjackson.dev/requirements: R-SEC-02 homelab.keithjackson.dev/scope: admission-only spec: validationFailureAction: Enforce failurePolicy: Fail background: false rules: - name: trigger-may-create-only-the-committed-conformance-run # resources AND subjects in the SAME filter entry — that is how Kyverno ANDs them. # As two separate `all` entries it is a schema error ("match resources not specified") # and the whole policy loads as INVALID, which the CLI reports as a SKIP. match: all: - resources: kinds: - Job namespaces: - conformance subjects: - kind: ServiceAccount name: conformance-trigger namespace: conformance validate: message: >- conformance-trigger may create ONLY the committed conformance run: a single container, digest-pinned conformance-runner image, serviceAccountName conformance-runner, command ["./run.sh"], no host namespaces, non-root with privilege escalation disabled, and only the `workspace` and `results` volumes. See platform/kyverno/admission/conformance-trigger-job-shape.yaml. pattern: spec: template: spec: serviceAccountName: conformance-runner =(hostNetwork): "false" =(hostPID): "false" =(hostIPC): "false" containers: - image: "gitea.home.keithjackson.dev/home-network/conformance-runner@sha256:*" command: - "./run.sh" securityContext: runAsNonRoot: true allowPrivilegeEscalation: false =(privileged): "false" - name: trigger-volumes-must-be-workspace-or-results # resources AND subjects in the SAME filter entry — that is how Kyverno ANDs them. # As two separate `all` entries it is a schema error ("match resources not specified") # and the whole policy loads as INVALID, which the CLI reports as a SKIP. match: all: - resources: kinds: - Job namespaces: - conformance subjects: - kind: ServiceAccount name: conformance-trigger namespace: conformance validate: message: >- conformance-trigger Jobs may mount only the `workspace` and `results` volumes. An extra volume is how a constrained pod reaches a host path, a secret, or another SA's token. # foreach, NOT a `pattern` list: a Kyverno array pattern asserts "at least one element # matches", which would happily admit a Job carrying `workspace` PLUS a hostPath. This # asserts EVERY volume is one of the two allowed names. foreach: - list: "request.object.spec.template.spec.volumes" deny: conditions: all: - key: "{{ element.name }}" operator: AnyNotIn value: ["workspace", "results"] - name: trigger-args-must-be-the-committed-flags # resources AND subjects in the SAME filter entry — that is how Kyverno ANDs them. # As two separate `all` entries it is a schema error ("match resources not specified") # and the whole policy loads as INVALID, which the CLI reports as a SKIP. match: all: - resources: kinds: - Job namespaces: - conformance subjects: - kind: ServiceAccount name: conformance-trigger namespace: conformance validate: message: >- conformance-trigger Job args may contain only the conformance run's own flags (--layer, --requirements, --include-scope, --strict-coverage) and their plain values. Anything else — a shell, a path, a redirect — is denied. foreach: - list: "request.object.spec.template.spec.containers[0].args" deny: conditions: all: - key: "{{ regex_match('^(--layer|--requirements|--include-scope|--strict-coverage|[A-Za-z0-9][A-Za-z0-9,._-]*)$', element) }}" operator: Equals value: false # ========================================================================================= # ADDED 2026-09-29 — THE THREE RULES ABOVE WERE BYPASSABLE VIA `initContainers`. # # Every rule above reads `spec.template.spec.containers` (rule 1's pattern, rule 3's # `containers[0].args`) or `volumes` (rule 2). NONE of them read `initContainers`. So the # exact escalation this policy exists to stop — the privileged `conformance-runner` SA # executing an arbitrary command, because Kubernetes places no `use` check on # `serviceAccountName` — was reachable one field over, with `containers[0]` left fully # compliant. MEASURED with the pinned CLI (kyverno v1.19.0) before this rule existed: # # a Job with serviceAccountName conformance-runner and an initContainer # `docker.io/library/alpine:latest`, `privileged: true`, running `kubectl get secrets -A` # -> pass: 3, fail: 0, warn: 0, error: 0, skip: 0 # ADMITTED # # `=(privileged): "false"` in rule 1 only ever applied to `containers[0]`. # # WHY THIS PINS A PATH IN THE IMAGE RATHER THAN VETTING A SCRIPT. An init container IS # needed — the suite has to be cloned at the commit under test. But while the clone logic # lived inline as `command: ["/bin/bash","-c"]` + `args: [