Skip to content

sign-and-attest composite (Step C-1 of pin-reduction)#13

Merged
Cre-eD merged 1 commit into
mainfrom
feat/step-c-sign-and-attest-and-image-release
May 17, 2026
Merged

sign-and-attest composite (Step C-1 of pin-reduction)#13
Cre-eD merged 1 commit into
mainfrom
feat/step-c-sign-and-attest-and-image-release

Conversation

@Cre-eD

@Cre-eD Cre-eD commented May 16, 2026

Copy link
Copy Markdown
Contributor

What

A composite action that encapsulates the four-step attestation pipeline that ships in every SC release workflow today (push.yaml, branch-preview.yaml, build-staging.yml):

  1. CycloneDX SBOM generation via syft scan registry:<image-ref>
  2. cosign keyless sign (Fulcio + Rekor)
  3. cosign attest the SBOM (--type cyclonedx, registry slot)
  4. SLSA Build L3 provenance via actions/attest-build-provenance@v4 (GitHub attestation API slot, deliberately NOT registry — see SC SECURITY.md for the cross-attestation contract)
# Consumer-side AFTER this lands (replaces ~50 lines per image)
- name: Build and push
  id: build
  uses: docker/build-push-action@<sha>
  with: { ... }

- name: Sign + attest
  uses: simple-container-com/actions/sign-and-attest@<sha>
  with:
    image-ref: ${{ env.IMAGE_REPO }}@${{ steps.build.outputs.digest }}
    image-name: caddy
    subject-name: ${{ env.IMAGE_REPO }}
    subject-digest: ${{ steps.build.outputs.digest }}
    soft-fail: 'true'  # default during Phase 2 bake-in

Per-step outcomes (sbom-outcome, sign-outcome, attest-outcome, slsa-outcome) are surfaced as outputs so the consumer can aggregate them into a single ::warning:: summary the same way push.yaml does today.

Step C in context

Step What Status
A setup-sc-tooling composite PR #12 (open)
B install-attest-tools composite (promoted from api-local) PR #12 (open)
C-1 sign-and-attest composite (this PR) here
C-2 sc-image-release.yml reusable workflow that wraps setup + docker/build-push-action + sign-and-attest + notify-telegram into a single-call build-and-publish flow Next session

Tests — real integration

.github/workflows/self-test-sign-and-attest.yml is not a static check. It:

  1. Builds a tiny alpine-based image inline (Dockerfile generated in the step)
  2. Pushes to ghcr.io/simple-container-com/actions/sign-and-attest-test:run-<id>-attempt-<n>
  3. Invokes the composite against the fresh digest
  4. Runs the three verification commands a real consumer would run:
    • cosign verify
    • cosign verify-attestation --type cyclonedx
    • gh attestation verify --repo simple-container-com/actions
      …all against the identity regex ^https://github\.com/simple-container-com/actions/.
  5. Asserts all four composite outputs are success and the SBOM file exists on disk

Uses soft-fail: false so a transient sigstore-public outage surfaces as clear CI failure. Wall-clock ~2 min per run.

Caller responsibilities (documented in the composite description)

  • Set permissions: on the calling job:
    permissions:
      id-token: write       # OIDC for cosign keyless + attest-build-provenance
      attestations: write   # actions/attest-build-provenance API
  • Pass image-ref WITH digest (@sha256:…). Tag-only references are TOCTOU-unsafe.
  • Set install-tools: false if the calling job already invoked install-attest-tools (idempotent, but saves ~10s).

Verification

  • actionlint clean
  • shellcheck clean
  • bash semgrep-scan/run-tests.sh — 0 findings full-repo
  • Self-test workflow passes on the PR (wall-clock ~2 min)

Follow-up after merge

  • New api PR refactors push.yaml / branch-preview.yaml / build-staging.yml to use sign-and-attest@<sha> instead of the inline 5-step block per image. Replaces ~50 LOC per image with ~7.
  • Step C-2 (sc-image-release.yml) tracked in HARDENING.md for the next session.

Encapsulates the four-step attestation pipeline that ships in every
SC release workflow today (push.yaml, branch-preview.yaml,
build-staging.yml):

  1. Generate CycloneDX SBOM (syft scan on the image's registry layout).
  2. Sign with cosign keyless (Fulcio + Rekor).
  3. Attest the SBOM via `cosign attest --type cyclonedx`.
  4. Publish SLSA Build L3 provenance via `actions/attest-build-provenance@v4`
     (subject-name + subject-digest), deliberately NOT pushing the
     attestation to the registry — that would silently overwrite the
     `cosign attest cyclonedx` slot under the `.att` tag. Provenance
     stays in the GH attestation API; SBOM stays in the registry. This
     cross-attestation contract is documented in SC's SECURITY.md.

Per-step outcomes are surfaced as outputs (`sbom-outcome`,
`sign-outcome`, `attest-outcome`, `slsa-outcome`) so the calling
workflow can aggregate into a single ::warning:: summary the same way
push.yaml does today. `soft-fail: true` (default during Phase 2 bake-
in) makes each step `continue-on-error`; consumers flip to `false`
after 14 days of clean post-publish verification.

Caller responsibilities (documented in the composite description):
- Set `permissions: id-token: write` + `attestations: write` on the
  calling job (OIDC for cosign keyless + the GitHub attestation API).
- Pass `image-ref` WITH digest (`@sha256:…`). Verification by tag is
  unsafe (TOCTOU) and the composite rejects nothing — the caller must
  ensure digest-pinning at the upstream `docker/build-push-action`
  step.
- Set `install-tools: false` if the calling job has already invoked
  `install-attest-tools` (idempotent, but saves ~10s).

## Step C in context

This is **Step C-1** of the consolidation plan tracked in HARDENING.md
"Consumer-side pin reduction":
  - Step A (PR #12, open): setup-sc-tooling composite
  - Step B (PR #12, open): install-attest-tools composite (promoted
    from api-local)
  - **Step C-1 (this PR)**: sign-and-attest composite — encapsulates
    the inner signing/attestation flow that consumer build workflows
    repeat per-image.
  - Step C-2 (next session): `sc-image-release.yml` reusable workflow
    that wraps `setup-sc-tooling` + `docker/build-push-action` +
    `sign-and-attest` + `notify-telegram` into a one-call build-and-
    publish-one-image flow. Driven by a matrix on the consumer side.

## Tests

`.github/workflows/self-test-sign-and-attest.yml` is a real
end-to-end integration test, not a static check:

  1. Build a tiny alpine-based test image (Dockerfile generated inline).
  2. Push to ghcr.io/${{ github.repository }}/sign-and-attest-test
     under a unique `run-<id>-attempt-<n>` tag.
  3. Invoke the composite against the freshly-pushed digest.
  4. Run the three verification commands a real consumer would run:
       - `cosign verify`
       - `cosign verify-attestation --type cyclonedx`
       - `gh attestation verify --repo simple-container-com/actions`
     against the identity-regex `^https://github\.com/simple-container-com/actions/`.
  5. Assert all four composite outputs (`sbom-outcome` /
     `sign-outcome` / `attest-outcome` / `slsa-outcome`) are
     `success`, and that the SBOM file is on disk.

The test uses `soft-fail: false` so a transient sigstore-public outage
surfaces as a clear CI failure instead of being swallowed. Wall-clock
~2 min per run.

## Coverage

- actionlint clean
- shellcheck clean
- `bash semgrep-scan/run-tests.sh` — 0 findings full-repo

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@github-actions

Copy link
Copy Markdown

Security Scan Results

Repository: actions | Commit: 6063f2c

Check Status Details
✅ Secret Scan Pass No secrets detected
⏩ Dependencies Skipped -

Scanned at 2026-05-16 21:03 UTC

@github-actions

Copy link
Copy Markdown

Semgrep Scan Results

Repository: actions | Commit: 6063f2c

Check Status Details
✅ Semgrep Pass 0 total findings (no error/warning)

Scanned at 2026-05-16 21:03 UTC

@Cre-eD
Cre-eD merged commit a374c76 into main May 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant