Skip to content

End-to-end attestation: bind CI build attestations → CP register → container policy.json #128

@posix4e

Description

@posix4e

Summary

Close the loop from "CI built this artifact" to "this VM is running exactly that artifact" across two surfaces:

  1. CP /register verifies the registering agent's MRTD against a GitHub build attestation on this repo (PR ci(release): publish a signed attestation for every devopsdefender binary #125 already publishes them for every devopsdefender binary).
  2. Container pulls inside the guest (via our dd-podman wrapper) use a policy.json that requires sigstoreSigned signatures backed by GitHub's OIDC workflow identity — so podman refuses to run any image we didn't attest.

Today we have:

  • ✅ CI publishes actions/attest-build-provenance@v2 attestations over the built devopsdefender musl binary (PR ci(release): publish a signed attestation for every devopsdefender binary #125, merged).
  • ✅ CP receives an ITA-verified TDX quote with the MRTD at /register (src/cp.rs:287, ita_claims.mrtd).
  • ❌ CP does nothing with that MRTD beyond logging it. Any ITA-verified VM with the right owner/env_label gets a tunnel.
  • ❌ Guest's policy.json is insecureAcceptAnything — any image digest runs (see apps/podman-bootstrap/workload.json).

So the infrastructure exists to prove "this binary came from CI" and "this TDX VM is running some binary" — but nothing checks they're the same binary, and nothing checks the containers running inside are ones we signed.

Proposed scope

Phase 1 — CP verifies build attestation on /register

  • Add a CP config DD_TRUSTED_REPO (default devopsdefender/dd) and DD_TRUSTED_WORKFLOW_REF (default refs/heads/main).
  • In src/cp.rs::register, after ITA verify:
    • Extract mrtd from ita_claims (already parsed).
    • Query GitHub REST: GET /repos/{trusted_repo}/attestations/sha256:{mrtd_hex}.
    • Parse the returned Sigstore bundle; verify the certificate's SAN matches the expected workflow identity (https://github.com/{trusted_repo}/.github/workflows/release.yml@{trusted_ref}).
    • Reject with 403 if no attestation matches.
  • Cache verified MRTDs per process lifetime (in-memory map).
  • Library: try the sigstore Rust crate. If maturity's thin, shell out to gh attestation verify at runtime.

Prereqs: a stable, reproducible MRTD for each release. actions/attest-build-provenance signs the musl binary's sha256, but MRTD is over the VM's launch memory image (kernel + initrd + rootfs + QEMU params), not just the binary. Either:

  • (a) Also attest the EE image (qcow2) — requires CI integration with the easyenclave repo's release pipeline.
  • (b) Derive MRTD deterministically offline from the EE image bytes, attest that.
  • (c) Launch-and-measure: spin up a throwaway TDX VM in CI, pull MRTD from an ITA round-trip, attest.

(c) is simplest if we can get a TDX self-hosted runner; (a) is cleanest long-term. Pick one; rollout order forces it to happen before the /register verify is turned on.

Phase 2 — Guest policy.json requires sigstoreSigned images

Replace apps/podman-bootstrap/workload.json's {"default":[{"type":"insecureAcceptAnything"}]} with:

{
  "default": [{"type": "reject"}],
  "transports": {
    "docker": {
      "ghcr.io/devopsdefender": [{
        "type": "sigstoreSigned",
        "fulcioCert": "<Fulcio root CA>",
        "identity": {
          "exactRepo": "devopsdefender/dd",
          "exactWorkflow": ".github/workflows/release.yml"
        }
      }]
    }
  }
}

Plus a CI step that signs published container images (e.g. a thin ollama+openclaw wrapper image we publish to ghcr.io/devopsdefender) with cosign sign --identity-token=$GITHUB_TOKEN ghcr.io/....

Upstream ollama/ollama:latest isn't signed by us, so any workload that wants to run a third-party image needs an explicit policy exception. For the canonical path, publish our own image layered on top of theirs — one-time work, gives us signing authority.

Phase 3 — Per-workload attestation on /health

Agent already returns ita_token + deployments[]. Extend so each deployment entry includes:

  • image_digest (from podman inspect)
  • attestation_status (green / yellow / red / unknown — result of a sigstore verify pass)

Dashboard shows a per-workload badge. The fleet view at /agent/{id} surfaces which workloads are backed by a signed image vs. running a best-effort unverified pull.

Out of scope / later

  • Drop PAT from /register entirely (plan file discussed this). Once MRTD attestation lands, GitHub ownership via PAT becomes redundant — the signed MRTD IS the identity.
  • Dropping ITA in favour of pure sigstore: nope — ITA proves Intel hardware, sigstore proves our code, both matter.
  • TUF / update server with attestation bundles — the GitHub-attestation REST endpoint is already a hosted verification service, no extra infra.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions