Files
home-network-k3s-impl/platform/kyverno
jakman85andClaude Opus 5 c5b5b8672a
gate / gate (pull_request) Failing after 1m51s
gate / conformance (pull_request) Has been skipped
feat(pm-agent): an in-cluster AI Program Manager that turns Gitea issues into pull requests
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]>
2026-10-05 22:28:10 -05:00
..

platform/kyverno — L0 policy-as-code gate & admission enforcer (T-0.5)

Kyverno is the platform's L0 static/policy gate (in CI, no cluster) and its in-cluster admission enforcer (ADR-0011, docs/14, docs/07). The policies here encode spec invariants as code: they run in CI as a merge gate today (Phase 0, no cluster) and the same policies are wired to run in-cluster as an admission controller later (Phase 2, admission/).

Enforcement complements, never replaces, behavior tests. A policy admitting a manifest is not proof the runtime behaves (R-TEST-01). The L2/L3 Chainsaw behavior + fault-injection tests remain mandatory. See admission/README.md.

Layout

platform/kyverno/
├── README.md                     # this file: policy→requirement map + CI entrypoint
├── labels.md                     # label/annotation convention (PROPOSED contract — escalated)
├── policies/                     # ClusterPolicies (authored once; CI + admission)
│   ├── require-default-deny-baseline.yaml   # R-NET-01 (generate baseline)
│   ├── validate-default-deny-shape.yaml     # R-NET-01 (reject malformed baseline)
│   ├── disallow-plaintext-secrets.yaml      # R-SEC-01 (CI-only: sops block required)
│   ├── restrict-host-namespaces.yaml        # R-SEC-02 (+R-NET-01 bypass guard)
│   ├── require-egress-class-label.yaml      # R-NET-05
│   ├── require-auth-mode-label.yaml         # R-IDP-02
│   └── label-cdi-worker-pods.yaml           # R-NET-05+R-IDP-02 (mutate: CDI's own worker pods)
├── servicedefinition/            # ServiceDefinition shape validation (kyverno-json)
│   ├── validate-servicedefinition.yaml      # R-GITOPS-02
│   ├── payloads/ (valid-* / invalid-*)      # pass & fail fixtures
│   └── README.md
├── tests/                        # `kyverno test` suite — runnable L0 tests NOW, no cluster
│   ├── kyverno-test.yaml                     # policies × fixtures × expected results
│   ├── resources/ (good-* / bad-* fixtures + generated golden)
│   └── controller-worker-pods/               # the CDI-worker mutation, proven BOTH ways
│       ├── kyverno-test.yaml                 #   2 Tests: cdi-sa requester vs ordinary requester
│       ├── userinfo-cdi-sa.yaml / userinfo-ordinary.yaml
│       └── resources/ + patched/             #   real + FORGED importer pods; expected patch
├── admission/                    # in-cluster enforcing bundle (scaffolding, T-2.7)
│   ├── namespace.yaml · helmrelease.yaml · kustomization.yaml
│   ├── secret-provenance.yaml                # R-SEC-01 admission variant (provenance)
│   └── README.md
└── scripts/run-l0.sh             # the single-command CI entrypoint for the gate

Policy → requirement map

Policy / artifact Rule(s) Requirement Spec source Behaves
require-default-deny-baseline generate-default-deny R-NET-01 docs/03 default-deny baseline Every app namespace auto-gets a deny-all (ingress+egress, DNS-only) NetworkPolicy
validate-default-deny-shape baseline-must-deny-both-directions R-NET-01 docs/03 A baseline that leaves a direction open is rejected
disallow-plaintext-secrets secrets-must-be-sops-encrypted R-SEC-01 docs/07, docs/05, ADR-0007 Committed Secrets must be SOPS-encrypted (sops block); plaintext rejected. CI-only
restrict-host-namespaces disallow-host-namespaces, disallow-privileged-containers R-SEC-02 (+R-NET-01) docs/07 least reach App pods can't use hostNetwork/PID/IPC or privileged (would bypass default-deny)
require-egress-class-label require-egress-class R-NET-05 docs/03, docs/13 App pods must declare egress-class ∈ {internet,vpn}
require-auth-mode-label require-auth-mode R-IDP-02 docs/13, docs/15 App pods must declare auth-mode ∈ {oidc,forward-auth,native,none}
label-cdi-worker-pods (mutate) add-worker-pod-labels R-NET-05, R-IDP-02 docs/03, docs/13 CDI's controller-generated worker pods (importer-*/uploader-*/cloner-*, created by SA cdi/cdi-sa) get egress-class: internet + auth-mode: none added, so they satisfy the two policies above instead of being exempted from them. Matched on the generating ServiceAccount, never a pod-name prefix
validate-servicedefinition (kyverno-json) shape / dependencies / storage R-GITOPS-02 docs/13 (+03,15) ServiceDefinition is schema/invariant valid before render; public⇒not none auth
admission/secret-provenance secrets-must-be-flux-managed R-SEC-01 docs/07 Admission-only variant: app-ns Secrets must come from Flux/SOPS (see below)

Coverage of the T-0.5 requirement set: R-TEST-01 (behavior-tagged, reproducible tests — this whole suite), R-NET-01, R-SEC-01, R-SEC-02, R-IDP-02, R-NET-05, R-GITOPS-02. Every policy carries its R-* IDs in the homelab.keithjackson.dev/requirements annotation so the coverage report can pick them up automatically.

CI entrypoint (the L0 gate)

CI (WS-GITOPS .gitea/workflows, then Gitea Actions) runs the single command:

platform/kyverno/scripts/run-l0.sh

which performs, with pinned tool versions (KYVERNO_VERSION, KYVERNO_JSON_VERSION):

# 1) ClusterPolicy tests — offline, no cluster
kyverno test platform/kyverno/tests

# 2) ServiceDefinition validation — offline, no cluster
kyverno-json scan --policy platform/kyverno/servicedefinition/validate-servicedefinition.yaml \
  --payload <each catalog/ ServiceDefinition and the valid-* fixtures>   # must pass
#   plus each invalid-* fixture must be REJECTED (script asserts non-zero exit)

kyverno test emits pass/fail per policy×rule×resource and supports JUnit output for the coverage/merge gate:

kyverno test platform/kyverno/tests --output junit > results/kyverno-l0.junit.xml

Exit non-zero fails the merge gate (docs/14 gating rules). Deterministic, hermetic, self-contained — no cluster, no external state (reproducibility contract, R-TEST-02).

Notes / handoffs

  • Label keys are a proposed contract (labels.md) shared with WS-NET / WS-SEC / WS-GITOPS. Values are spec-fixed; keys need owner sign-off (coordination-protocol §2).
  • Controller-generated pods are a cold-start hazard. A policy that matches any Pod in an app namespace also matches pods that no manifest in git authors. Prod hit this on 2026-09-06: require-auth-mode-label + require-egress-class-label denied CDI's own importer-prime-* pod, so DataVolume unifi/unifi-rootdisk stuck in ImportScheduled and the UniFi VM had no disk. Fix: policies/label-cdi-worker-pods.yaml mutates those pods to carry truthful labels (matched on the generating ServiceAccount cdi/cdi-sa), rather than excluding them. virt-launcher-* pods need nothing — they inherit the VM pod-template labels. KubeVirt hotplug (hp-volume-*) and export (virt-export-*) pods do not inherit them and are unused here; extend the same policy if either is adopted.
  • PolicyException is NOT usable in this install: the Kyverno admission/background/reports controllers all run --enablePolicyException=false (chart default, verified on prod 2026-09-06). The CRD exists (from the pre-applied CRD bundle) so a PolicyException would APPLY cleanly and be silently ignored — a policy hole that looks like a fix and isn't.
  • Secrets CI vs admission divergence is deliberate and documented in admission/README.md and policies/disallow-plaintext-secrets.yaml.
  • The ServiceDefinition JSON Schema (WS-GITOPS T-0.3) is the complementary structural contract; this Kyverno policy encodes the invariants from docs/13.
  • Tool availability: neither kyverno nor kyverno-json is installed on the WS-TEST workstation, so this suite was authored but not executed here; the CI agent (T-0.2/T-0.4 harness) runs scripts/run-l0.sh as the gate.