Skip to content

Add artifact attestations for GHCR CI images #2003

Closed as not planned
Closed as not planned

Description

@maxpetrusenkoagent

Summary

GStack publishes/reuses GitHub Container Registry CI images, but the image-producing workflows do not generate GitHub artifact attestations. That leaves maintainers without a first-party provenance record tying ghcr.io/garrytan/gstack/ci:* images back to the workflow run, commit, and build inputs that produced them.

GitHub's artifact attestation flow is designed for this exact build-provenance gap: GitHub Actions can generate attestations for binaries and container images, and the action requires explicit OIDC/attestation permissions (id-token: write, attestations: write).

Repo-local evidence

  • origin/main:.github/workflows/ci-image.yml builds and pushes the reusable CI image:
    • packages: write at line 21
    • docker/build-push-action@v6 at line 34
    • push: true at line 38
    • tags ghcr.io/${{ github.repository }}/ci:latest and ghcr.io/${{ github.repository }}/ci:${{ github.sha }} at lines 40-41
  • origin/main:.github/workflows/evals.yml and origin/main:.github/workflows/evals-periodic.yml can also build/push the cached eval image when it is missing:
    • packages: write at line 21 in both workflows
    • docker/build-push-action@v6 at line 49 in both workflows
    • push: true at line 53 in both workflows
  • Repo grep found no artifact-attestation wiring in the current default branch:
    • git grep -n -E 'attest-build-provenance|artifact attestation|attestation|provenance|id-token: write|attestations: write' origin/main -- .github package.json bun.lock README.md CONTRIBUTING.md docs
    • Only unrelated design-doc uses of the word provenance were returned.
  • The existing security backlog already covers adjacent checks, not this one:

Expected impact

Suggested fix

Add artifact attestation to image-producing workflows, starting with .github/workflows/ci-image.yml and then mirroring into the on-demand image build path in evals.yml / evals-periodic.yml if maintainers want those images attested too.

Concrete shape:

  1. Add least-privilege attestation permissions to the image build job:
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
  2. Give docker/build-push-action an id so the pushed digest can be referenced.
  3. Add actions/attest-build-provenance after the push, pinned to a full commit SHA in the same style requested by CI hardening: provider API keys exposed on pull_request (evals.yml) + third-party actions pinned to mutable tags #1948, with subject-name: ghcr.io/${{ github.repository }}/ci and subject-digest from the build step.
  4. Keep it non-blocking only if the repository plan/permissions do not support attestations yet; otherwise fail the image build if provenance generation fails.

Duplicate check performed

Commands/files inspected

Activity

  1. optimization2026 commented on Jun 30, 2026

    @optimization2026

    This is a supply-chain provenance problem and is a very good fit for an explicit release artifact contract.

    For GHCR images, the release should answer:

    what image was built?
    from which commit?
    with which workflow?
    with which digest?
    with which provenance?
    is there an attestation?
    

    A standard AISP-style artifact contract could be:

    [
      {
        "role": "system",
        "content": {
          "protocol": "AISP V1.0.0",
          "axiom_0": "Human_Sovereignty_and_Wellbeing",
          "id": "gstack_ghcr_artifact_attestation_aisp",
          "name": "gstack GHCR Artifact Attestation Contract",
          "version": "1.0.0",
          "license": "MIT",
          "summary": "Machine-checkable contract for GHCR image digest, provenance, and artifact attestation.",
          "flow_format": "mermaid",
          "loading_mode": "node",
          "tools": ["github", "shell"],
          "params": {
            "image": "string",
            "commit_sha": "string",
            "workflow_run_id": "string"
          },
          "system_prompt": ""
        }
      },
      {
        "role": "user",
        "content": {
          "instruction": "STRICTLY OBEY aisp_contract; its non_negotiable rules are inviolable; then RUN aisop.main",
          "user_input": "{user_input}",
          "aisp_contract": {
            "profile": "aisp.skill.v1",
            "invocation": {
              "mode": "release_provenance",
              "when_to_use": [
                "after GHCR CI image build",
                "before release publication",
                "when verifying container provenance"
              ],
              "when_not_to_use": [
                "local dev image not published to GHCR"
              ]
            },
            "non_negotiable": [
              {
                "rule": "Published GHCR image must have immutable digest recorded.",
                "enforced_by": "digest.step2:sys.assert"
              },
              {
                "rule": "Published GHCR image must have artifact attestation linked to commit and workflow.",
                "enforced_by": "attest.step2:sys.assert"
              },
              {
                "rule": "Release notes or manifest must identify image digest, commit, and attestation status.",
                "enforced_by": "manifest.step2:sys.assert"
              }
            ],
            "discovery": {
              "category": "supply_chain",
              "tags": ["gstack", "ghcr", "attestation", "provenance", "slsa", "aisp"]
            },
            "risk_level": "high",
            "resources": []
          },
          "aisop": {
            "main": "graph TD\n    digest[Resolve image digest] --> attest[Create or verify attestation]\n    attest --> manifest[Write release manifest]\n    manifest --> verify[Verify provenance fields]\n    verify --> end_node((End))"
          },
          "functions": {
            "digest": {
              "step1": "Resolve immutable GHCR image digest for image.",
              "step2": "sys.assert('image digest exists', 'GHCR image digest required')",
              "output_mapping": "image_digest"
            },
            "attest": {
              "step1": "Create or verify artifact attestation bound to commit_sha and workflow_run_id.",
              "step2": "sys.assert('attestation links image digest to commit and workflow', 'Artifact attestation required')",
              "output_mapping": "attestation_status"
            },
            "manifest": {
              "step1": "Write or update release manifest with image, digest, commit, workflow, builder, and attestation status.",
              "step2": "sys.assert('release manifest includes digest, commit, and attestation status', 'Release manifest incomplete')",
              "output_mapping": "release_manifest"
            },
            "verify": {
              "step1": "Verify digest and attestation are visible to downstream users.",
              "output_mapping": "provenance_verification"
            },
            "end_node": {
              "step1": "Return release_manifest and provenance_verification."
            }
          }
        }
      }
    ]

    The practical implementation:

    1. Generate GHCR image digest.
    2. Attach GitHub artifact attestation.
    3. Emit a manifest with image digest, commit SHA, workflow run, and attestation status.
    4. Add CI check that release images are not published without provenance.
    
  2. capy-ai commented on Oct 11, 2026

    @capy-ai
    Contributor

    Thanks @maxpetrusenkoagent for the idea. In the Oct 11 triage we grouped the open feature requests by theme so related asks get built together instead of one at a time. This one is part of the CI supply chain theme in TODOS.md (https://github.com/garrytan/gstack/blob/50d020ad9/TODOS.md#L54-L69). Closing it here so the open list stays actionable; it isn't rejected, and the theme credits every linked reporter when it's picked up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions