Security — Supply Chain, Runtime, Attestation Chain¶
TL;DR
Agents that commit, build, and deploy collapse three historically-separate threat boundaries (author, builder, operator) into one. The security contract: every step from intent to production produces a cryptographically verifiable artifact, any missing artifact rejects admission, every artifact is bound to a traceable identity. Wired via Sigstore (keyless signing) + SLSA v1.0 L3 provenance + Kyverno admission + Cilium L7 egress + CaMeL trust separation.
For runtime safety (guards, stuck detection, hallucination mitigation), see safety.md.
The threat model in one diagram¶
graph TB
classDef threat fill:#ffcccc,color:#000
classDef defense fill:#c8e6c9,color:#000
T1[Stolen agent OIDC token]:::threat
T2[Prompt injection via issue]:::threat
T3[Malicious dependency]:::threat
T4[Fabricated image]:::threat
T5[Hallucinated package]:::threat
T6[Agent compromise]:::threat
T7[Human credential leak]:::threat
D1[Ephemeral Fulcio certs]:::defense
D2[CaMeL separation + egress ACL]:::defense
D3[Socket.dev + SBOM + allowlist mirror]:::defense
D4[Cosign + Kyverno verify]:::defense
D5[Ephemeral install sandbox]:::defense
D6[Two-attestor T3 policy]:::defense
D7[Workload identity + short TTL]:::defense
T1 -.->|mitigated by| D1
T2 -.->|mitigated by| D2
T3 -.->|mitigated by| D3
T4 -.->|mitigated by| D4
T5 -.->|mitigated by| D5
T6 -.->|contained by| D6
T7 -.->|mitigated by| D7
Each defense is cumulative. None alone is sufficient; together they raise the attacker's cost meaningfully.
The attestation chain¶
Every change produces an attestation at each step, indexed by either artifact digest or git SHA, signed by the identity responsible for that step, logged in a transparency log (Rekor). Kyverno rejects anything reaching production that breaks the chain.
| Step | Attestation | Signed by | Registry |
|---|---|---|---|
| 1. Issue triage | agent-plan (issue ID, model, prompt hash, TaskSpec) |
Agent OIDC via Fulcio | Rekor |
| 2. Commit | gitsign signature |
Agent OIDC via Fulcio | Rekor (via git tag) |
| 3. PR review | review-attestation (reviewer identity, verdict, commit SHA) |
Human OIDC or Review-E | Rekor |
| 4. Build | SLSA v1.0 Provenance | GitHub Actions isolated builder | Rekor + GHCR |
| 5. Image | cosign signature on digest |
GitHub Actions builder identity | GHCR |
| 6. Deploy | human-cosign (for T3) |
Human OIDC via Fulcio | Rekor |
| 7. Admission | Kyverno verdict log | Cluster audit | K8s events |
Kyverno rejects at step 7 if any of steps 1-6 is missing, expired, or identity-mismatched.
Why this chain is novel
No production system publicly demonstrates the full chain for AI-authored code as of early 2026. Individual pieces exist (Sigstore, in-toto, SLSA, Kyverno); wiring them to agent identity as a unified policy is the contribution.
Sigstore: keyless signing as the foundation¶
Sigstore is the 2026 answer to "how do we sign without managing long-lived private keys?" Four components:
- Cosign — image and blob signing
- Fulcio — issues short-lived (10-minute) x509 certs bound to OIDC identity
- Rekor — append-only transparency log for signatures
- Gitsign — git commit signing using Fulcio + Rekor
Image signing at build¶
Every GitHub Actions build job adds one step:
- name: Sign image
run: cosign sign --yes ghcr.io/dashecorp/${{ github.repository }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: 1
The Fulcio cert binds the signature to the GitHub Actions identity (repo:dashecorp/foo:ref:refs/heads/main). No KMS to manage, no secrets to rotate.
SLSA v1.0 Provenance via slsa-github-generator¶
Target: SLSA Build L3 — isolated builder where user-controlled steps can't touch signing keys. The reusable workflow from slsa-framework/slsa-github-generator produces L3 because the signing job runs in a separate isolated workflow the build code can't influence.
- uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
A real attestation is an in-toto v1.0 Statement (predicateType https://slsa.dev/provenance/v1) containing builder.id, buildType, invocation.configSource, materials (source commit SHA), signed by Fulcio, logged in Rekor.
Reproducibility (L4-equivalent) is a separate, much harder property and not required for any SLSA v1.0 level. We don't pursue it.
Gitsign for agent commits¶
Agents sign commits via gitsign instead of gpg:
export GITSIGN_OIDC_ISSUER="https://token.actions.githubusercontent.com"
git config gpg.x509.program gitsign
git config gpg.format x509
git commit -S -m "feat: add event type X"
Each commit carries a signature bound to the agent's OIDC identity, with the Rekor entry as non-repudiable record.
Gitsign vs GitHub 'Verified' badge
GitHub does not display gitsign commits as Verified — GitHub's trust root doesn't include Sigstore and the certs expire in minutes. Branch protection rules requiring Verified commits will reject gitsign-signed commits.
Workaround: don't use GitHub's Verified badge as a trust signal. A post-merge CI check runs gitsign verify and posts a status; branch protection requires the CI status, not the UI badge. The rig's verification is authoritative; the UI is cosmetic.
Admission: Kyverno over OPA Gatekeeper¶
Kyverno's advantages for a small team:
- YAML CRDs (no Rego language learning curve)
- Native cosign/notary verification (first-class, not an external tool)
- Operational simplicity — one controller, one CRD family, policies version-controlled in git
- OpenReports output — policies emit standardized reports consumable by dashboards
Gatekeeper wins only if you already operate OPA for other reasons. For us, we don't.
The T3 two-attestor policy¶
The centerpiece:
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
name: t3-two-attestor
spec:
validationActions: [Deny]
matchConstraints:
namespaceSelector:
matchLabels: { blast-radius: t3 }
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
attestors:
- name: agent-identity
cosign:
keyless:
identities:
- subject: "https://github.com/dashecorp/.+/.github/workflows/release\\.ya?ml@.+"
issuer: "https://token.actions.githubusercontent.com"
ctlog: { url: "https://rekor.sigstore.dev" }
- name: human-approval
cosign:
keyless:
identities:
- subject: "repo:dashecorp/prod-approvals:environment:t3-approve"
issuer: "https://token.actions.githubusercontent.com"
ctlog: { url: "https://rekor.sigstore.dev" }
attestations:
- name: slsa-provenance
intoto:
type: "https://slsa.dev/provenance/v1"
validations:
- expression: |
images.containers.map(i,
verifyImageSignatures(i, [attestors.'agent-identity']) &&
verifyImageSignatures(i, [attestors.'human-approval'])
).all(e, e)
- expression: |
images.containers.map(i,
verifyAttestationSignatures(i, attestations.'slsa-provenance', [attestors.'agent-identity'])
).all(e, e > 0)
Translation: manifests targeting namespace=prod-auth, prod-payments, prod-db-migrations (any namespace labeled blast-radius=t3) must carry both an agent Sigstore signature and a human co-sign, plus valid SLSA provenance. The human co-sign comes from a dedicated prod-approvals repo whose workflow environment: t3-approve is gated by GitHub's required-reviewer protection.
Other Kyverno policies¶
require-signed-images-all-ns— global: no unsigned image admitsdeny-privileged-pods— container security baselinerequire-network-policy— every pod must be in a namespace with aNetworkPolicyforbid-default-sa-in-prod— default service accounts are denied in production namespacesrequire-resource-limits— DoS mitigationforbid-latest-tag-in-prod— reproducibilityrequire-slsa-provenance-prod— all prod images carry SLSA L3 attestationt1-canary-required— T1 deploys must beCanaryresources, not rawDeploymentst2-interface-approval-required— T2 manifests must carry aninterface-approvalattestation
Catalog maintained in dashecorp/rig-gitops/policies/kyverno/.
What Kyverno cannot enforce¶
- Rate limits — e.g., "max 3 agent deploys per hour." Stateless per-admission policy. Handled by a dedicated Flux webhook that reads deploy events from a counter.
- Semantic correctness — policy can say "manifest carries an SLSA attestation" but not "the diff is semantically correct." Semantic correctness is Review-E + tests + canary.
- Cross-repo policy — e.g., "this PR touches service A, so service B's error budget must also be checked." Handled by Conductor-E's projection logic, not admission.
Cilium L7 egress — the single biggest ROI against prompt injection¶
Standard Kubernetes NetworkPolicy is L3/L4 (IP + port). Cilium's CiliumNetworkPolicy adds L7 filtering (FQDN matching and HTTP rules) via Envoy. This is the defense that closes "malicious README tells agent to POST secrets to attacker.com."
Per-agent egress allowlist¶
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: dev-e-egress
namespace: dev-e
spec:
endpointSelector:
matchLabels: { agent: dev-e }
egress:
- toFQDNs:
# Primary LLM provider (Anthropic default). Add api.openai.com,
# generativelanguage.googleapis.com, or openrouter.ai when the
# agent uses alternative providers — see provider-portability.md.
- matchName: api.anthropic.com
- matchName: api.github.com
- matchPattern: "*.githubusercontent.com"
- matchName: pkg.dashecorp.com
toPorts:
- ports: [{ port: "443", protocol: TCP }]
rules:
http:
- method: "GET"
path: "/.*"
- method: "POST"
path: "/v1/messages"
- method: "POST"
path: "/repos/.*/issues/.*/comments"
- method: "POST"
path: "/repos/.*/pulls"
- toEndpoints:
- matchLabels: { app: conductor-e }
toPorts:
- ports: [{ port: "8080", protocol: TCP }]
- toServices:
- k8sService: { serviceName: kube-dns, namespace: kube-system }
Default-deny: anything not explicitly allowed is blocked. The policy is per-namespace per-agent; Review-E's policy is stricter (no write paths on GitHub API).
What this blocks¶
- Exfiltration to attacker-controlled domains: DNS doesn't even resolve.
- Unintended HTTP methods or paths:
DELETE /repos/*from Dev-E is denied even against github.com. - Lateral movement within the cluster: Dev-E cannot reach Review-E's pod, Conductor-E's Postgres, or the host network.
What this does not block¶
- DNS exfiltration via DNS-over-HTTPS (agent embeds data in DoH queries to allowed domains). Mitigation: deny DNS egress to anything except cluster DNS; force all DNS through CoreDNS which we control.
- Exfiltration to allowed domains (e.g., opening a GitHub Gist to dump secrets). Mitigation: tool scoping — Dev-E has no tool for creating gists; the API surface exposed to the agent is minimized.
Secrets: SOPS + age in git, everything runtime-fetched is deferred¶
Corrected — three rounds of retraction
This section has been rewritten three times. Original whitepaper promised Vault; first retraction dropped Vault in favor of SealedSecrets+ESO; second retraction flipped SOPS to primary "replacing" SealedSecrets. Third retraction (2026-04-17): grep-verified the repo — zero SealedSecrets references, zero bitnami-labs image pulls. SOPS + age was always the deployed pick; there was never any SealedSecrets to replace or migrate from. Full three-round log in tool-choices.md.
Actual deployed state (verified 2026-04-17)¶
Verified against apps/ in the rig-gitops repo:
.sops.yamlat repo root withcreation_rulescovering everyapps/*/*.sops.yaml- Cluster-scoped age key in
flux-system/sops-ageSecret - Every app Kustomization sets
decryption.provider: sops+secretRef.name: sops-age - Encrypted manifests live per-app:
apps/dev-e/dev-e-secrets.sops.yaml,apps/review-e/review-e-secrets.sops.yaml,apps/conductor-e/conductor-e-secrets.sops.yaml,apps/cloudflared/tunnel-token.sops.yaml - Zero SealedSecret objects anywhere
- Flux decrypts inline — no separate secrets controller pod
Target state (same as deployed state, plus deferred additions)¶
- Primary encryption for secrets in git: SOPS + age, decrypted inline by Flux's
kustomize-controller --decryption-provider=sops. This is the current live pattern — see above. - GitHub App tokens — each agent pod mints an installation token on startup (1 h TTL) via the GitHub App API. No Vault required — GitHub itself does the short-lived-credential minting.
- Postgres (Conductor-E) credentials — static service account with narrow grants +
NetworkPolicy+ yearly rotation. At 3 agent classes on one Postgres, a static narrow-grant account is enough; the security delta from dynamic per-agent users is marginal vs. the operational cost of running Vault. - LLM API keys — long-lived (neither Anthropic nor OpenAI nor Google issues OIDC-ephemeral keys today). Stored as SOPS-encrypted secrets in git. Per-agent virtual keys in the LiteLLM proxy provide isolation at the application layer regardless of underlying provider — see provider-portability.md.
- Other static secrets (Cloudflare tokens, Discord webhooks) — SOPS + age in git.
- Certificates —
cert-manager+trust-manager. Non-controversial. - Deferred: External Secrets Operator + GCP Secret Manager if/when we outgrow git-at-rest (multi-cluster, compliance audit, runtime-fetched secrets). See tool-choices.md for the full evaluation.
Why this stack, not Vault¶
| Use case | Pick | Why |
|---|---|---|
| Git-at-rest encrypted static secrets | SOPS + age | MPL-2.0 / CNCF governance forever. Flux decrypts inline — no controller pod. Age keys are simpler than GPG. |
| Backend-agnostic runtime secret fetch (deferred) | External Secrets Operator | The reversibility insurance for if we move away from git-at-rest. Backend-agnostic CRDs. |
| Cloud KMS-backed managed secrets (deferred) | GCP Secret Manager (free tier) | We're already on GCP. Free tier covers us. Pick this when git-at-rest stops scaling. |
| Short-lived GitHub credentials | GitHub App installation tokens | Native 1-h TTL. Minted by SDK. No extra infrastructure. |
| Certificate lifecycle | cert-manager + trust-manager | CNCF-graduated. ~86% of prod clusters use it. |
The whole stack runs without Vault. See tool-choices.md for the full evaluation and comparison to SOPS, Infisical, 1Password Connect, CSI Secrets Store, and OpenBao.
Operational reference¶
For how-to mechanics (bootstrapping an encrypted secret, rotation procedure, key management), see docs/sops.md.
When to re-evaluate (adopt OpenBao or ESO)¶
A concrete trigger — not vibes:
- Second K8s cluster or second Postgres instance deployed
- Compliance requirement that mandates audit log on secret access
- Actual credential compromise where rotation scope pain became real
- Team grows past ~5 operators, making human secret-handling the bottleneck
- Need for short-lived AWS/GCP cloud credentials per agent
getsops.ioproject stalls or is archived — fork or migrate
When runtime-fetched secrets become necessary, adopt ESO + GCP Secret Manager first. When dynamic secrets become necessary, adopt OpenBao (LF-maintained Vault fork, MPL 2.0, API-compatible) — not Vault itself, because the BSL license and IBM pricing playbooks make the fork the right pick.
Dependency integrity¶
Defenses, layered¶
- GitHub Dependabot malware detection (March 2026): detects npm malware against the GitHub Advisory Database's malware feed, separate from CVE alerts. Enable for all repos.
- Socket.dev score on every dependency addition: PR check fails if Socket score < threshold.
- Package-age policy (Datadog's pattern): block deps < 14 days old without explicit approval. Catches typosquatting account-takeovers (Axios March 2026 compromise, shai-hulud September 2025 — 200+ package supply chain attack).
- Lockfile hash pinning:
npm ci --ignore-scriptsin agent sandboxes. - Ephemeral install sandbox:
npm installruns in a throwaway pod with no network beyond the package registry. If the install executespostinstallscripts, they execute only in the sandbox, which is discarded. - SBOM generation and scanning: Syft generates SBOM per build, Grype scans, results uploaded with the build attestation. Dependency CVEs surface in the admission chain.
Known limits¶
- All defenses operate on metadata (age, score, lockfile hash). A deeply-embedded malicious package with a 30-day history and a high Socket score passes through. Defense-in-depth via the L7 egress allowlist — even a compromised dep cannot exfiltrate.
Prompt injection in the security layer¶
Full treatment is in safety.md; from the security-layer perspective:
- CaMeL-style separation is the only defense with a formal guarantee.
- L7 egress allowlist (this document) closes the exfiltration path.
- Tool scoping per agent role minimizes capability given a compromised agent.
- Non-bypassable dangerous-command guard catches shell-level escape attempts.
- CVE-specific mitigations — see the CVE mitigation table in safety.md for specific CVE-to-defense mappings.
Mandatory vs over-engineered¶
For a non-hosted, non-customer-facing small-team rig:
Mandatory (the floor)¶
- Keyless image signing via cosign at every build (5 lines of YAML per workflow).
- Kyverno admission that rejects anything without a valid Sigstore signature bound to our org's GHA workflows.
- SLSA L3 build provenance via
slsa-github-generator. - Gitsign on agent commits + out-of-band CI verification.
- Cilium L7 egress allowlist per agent namespace.
- Short-lived credentials where native: GitHub App installation tokens (1 h TTL), no long-lived
ghp_*in cluster. Postgres: narrow-grant static service account (dynamic via OpenBao only if triggers from above fire). - Two-attestor policy for T3 — human OIDC co-sign required.
- CaMeL-style architectural separation on untrusted input (formal prompt-injection guarantee).
Over-engineered at our scale¶
- Reproducible builds — valuable for an OS distribution, marginal for our threat model. SLSA L3 provenance is enough.
- SLSA L4 — doesn't exist in v1.0; L3 is the spec ceiling.
- OPA Gatekeeper — Kyverno covers 95% with less operational cost.
- Full zero-trust service mesh (Istio) — Cilium L7 gets 80% of the value at 10% of the cost; skip Istio unless we need mTLS to external services.
- Air-gapped build — not meaningful when agents talk to the primary LLM provider (api.anthropic.com by default; same reasoning for OpenAI, Gemini, or OpenRouter when configured) by design. Local Ollama is the only fully air-gapped option and only covers tasks small enough for a local model — see provider-portability.md.
- HSM-backed signing keys — keyless Sigstore is better for our threat model, not worse. HSMs solve key-custody; we solve key-custody by not having long-lived keys.
- Full SAST + DAST + IAST suite — Dependabot + Socket + Kyverno + property tests catch the failure modes relevant to our scale.
The security dashboard¶
Metrics the dashboard surfaces (observability.md):
admission_reject_total{policy="..."}— Kyverno rejections per policysignature_verify_fail_total— cosign/rekor verification failuresslsa_provenance_missing_total— builds that would fail L3egress_deny_total{agent="...", destination="..."}— Cilium L7 denialsvault_credential_issued_total{agent="...", type="db|github"}— short-lived credential churndependency_reject_total{reason="..."}— Socket / age / malware rejectionst3_cosign_latency_seconds— human approval turnaround
High-signal alert set: spikes in admission_reject_total or egress_deny_total indicate compromised agent or misconfigured policy; high t3_cosign_latency_seconds indicates humans are the bottleneck on T3 throughput.
The 2026 attack landscape¶
Partial catalog of real incidents informing these choices:
| Incident | Lesson |
|---|---|
| CVE-2025-54794/95 (InversePrompt against Claude) | Prompting alone is not a prompt-injection defense |
| CVE-2025-59536 / CVE-2026-21852 (Claude Code project-file RCE) | Don't clone untrusted repos into agent workspaces; strip hook/MCP configs on clone |
| CVE-2025-68143-45 (Anthropic Git MCP server RCE) | MCP servers need their own network-policy scoping |
| Oasis "Claudy Day" (Claude.ai file exfil) | Files API access requires egress policy scoping |
| Axios March 2026 (account-takeover typosquat) | Package age policy catches most of these |
| shai-hulud September 2025 (200+ package supply chain attack) | SBOM + Socket + age policy + ephemeral install sandbox |
| Cloudflare Dec 5 2025 outage (emergency fast path bypassed canary) | No emergency fast path — all mutable surfaces flow through the same gated pipeline |
See OWASP Top 10 for Agentic Applications (2026) for the canonical list of agent-specific attack classes.
Evolving the policies¶
Changes to Kyverno policies are themselves a meta-T3 operation (T3 change to the enforcement of T3). They go through:
- PR to
dashecorp/rig-gitops/policies/kyverno/ - Review-E review with policy-specific criteria
- Human co-sign required
- Flux rolls the policy into the cluster
- Audit: after deployment, a projection checks that no previously-admitted resource would now be rejected (grandfathering check); existing rejected resources are reported as alerts
See also¶
- index.md
- principles.md — principle 5 (attestable) operationalized here
- trust-model.md — Kyverno enforces the tier model
- safety.md — runtime-layer defenses, complementary
- self-healing.md — the deploy pipeline the security layer gates
- observability.md — how the security metrics surface
- limitations.md — what security does not cover