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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions