A service that reads this repository's Gitea issues, decides what to do about each thread as the Program Manager persona, and runs one worker agent per assignment that authors a change, runs the merge gate and opens a pull request. It reacts to issue creation and to comments; the assignment and all correspondence travel through the issue thread and the PR. It authenticates with an Anthropic subscription token. It never pushes to main and never merges. Three choices carry the design, with alternatives recorded in docs/plans/: - it POLLS rather than receiving webhooks (Gitea's webhook SSRF guard, lost deliveries, and no inbound reach needed at all); - it keeps NO state: the trigger is "the newest thing in this thread was not written by me", so the issue thread is the state — no PVC, no backup obligation; - the model holds NO tools: its reply is one JSON object this code validates, so nothing in the path executes text. The boundary is platform/kyverno/admission/pm-agent-job-shape.yaml (Enforce, failurePolicy: Fail), not the RBAC: Kubernetes places no `use` check on serviceAccountName, so "may create Jobs" is "may run arbitrary code as the namespace's most privileged identity". Six rules admit only the committed shape, and the worker identity is bound to nothing with no token mounted, so there is nothing to escalate to. 28/28 offline Kyverno assertions. Nothing has run: the image has never been built or pushed, and both the image digest and the two credentials are deliberate placeholders. See the handoff entry's "What I did NOT verify". Refs CLAUDE.md §1/§3/§6, R-SEC-02, R-GITOPS-02. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
110 lines
3.9 KiB
YAML
110 lines
3.9 KiB
YAML
# R-SEC-02 — the PM agent dispatcher cannot escalate to arbitrary privileged execution.
|
|
#
|
|
# The dispatcher's one privileged act is `create jobs` in its own namespace, and Kubernetes
|
|
# places no `use` check on serviceAccountName — so without
|
|
# platform/kyverno/admission/pm-agent-job-shape.yaml that single verb IS arbitrary code
|
|
# execution in the namespace, holding a Gitea write token and an Anthropic subscription
|
|
# token. These rows are what prove the policy actually refuses each route to it.
|
|
#
|
|
# user-info.yaml supplies the requesting identity. WITHOUT it the policy's `subjects` match
|
|
# never fires, every case trivially passes, and this file becomes coverage theatre.
|
|
#
|
|
# A NOTE ON WHICH ROWS NAME WHICH FIXTURES: a `foreach` over an absent list produces no
|
|
# result at all, so a fixture with no `volumes` (or no `env`) is NOT reported by the rule
|
|
# that iterates them and must not be listed for it. That is why the volume and env rows name
|
|
# fixtures individually instead of listing everything.
|
|
apiVersion: cli.kyverno.io/v1alpha1
|
|
kind: Test
|
|
metadata:
|
|
name: pm-agent-job-shape
|
|
policies:
|
|
- ../../admission/pm-agent-job-shape.yaml
|
|
resources:
|
|
- resources.yaml
|
|
userinfo: user-info.yaml
|
|
results:
|
|
# ---------------- the committed run is ADMITTED ----------------
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-must-be-the-committed-shape
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-must-be-one-container-with-no-args
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-volumes-must-be-workspace-or-tmp-emptydirs
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-may-not-declare-init-or-ephemeral-containers
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
# Only `real-template` and `injected-env` set env at all, so they are the only fixtures the
|
|
# env rule reports on.
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-env-names-must-be-the-committed-set
|
|
kind: Job
|
|
resources: [real-template]
|
|
result: pass
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-may-not-use-envfrom
|
|
kind: Job
|
|
resources: [good-run, real-template]
|
|
result: pass
|
|
|
|
# ---------------- THE escalation, and the shape rule's other refusals ----------------
|
|
# `evil-command` is the case the whole model exists for: right namespace, right creator,
|
|
# arbitrary command. The rest are each a single field away from compliant.
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-must-be-the-committed-shape
|
|
kind: Job
|
|
resources:
|
|
- evil-command
|
|
- evil-image
|
|
- tag-not-digest
|
|
- worker-as-dispatcher-sa
|
|
- token-mounted
|
|
- writable-rootfs
|
|
- host-namespaces
|
|
- restart-onfailure
|
|
- second-container
|
|
result: fail
|
|
|
|
# ---------------- args, and the second container ----------------
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-must-be-one-container-with-no-args
|
|
kind: Job
|
|
resources: [injected-args, second-container]
|
|
result: fail
|
|
|
|
# ---------------- an extra volume, or an allowed name over a hostPath ----------------
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-volumes-must-be-workspace-or-tmp-emptydirs
|
|
kind: Job
|
|
resources: [extra-volume, hostpath-named-workspace]
|
|
result: fail
|
|
|
|
# ---------------- the same escalation via the other two container lists ----------------
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-may-not-declare-init-or-ephemeral-containers
|
|
kind: Job
|
|
resources: [evil-initcontainer, evil-ephemeral-container]
|
|
result: fail
|
|
|
|
# ---------------- and via env, and via envFrom ----------------
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-env-names-must-be-the-committed-set
|
|
kind: Job
|
|
resources: [injected-env]
|
|
result: fail
|
|
- policy: pm-agent-job-shape
|
|
rule: worker-may-not-use-envfrom
|
|
kind: Job
|
|
resources: [envfrom-whole-secret]
|
|
result: fail
|