SSDF Attestation in CI: A Step-by-Step Guide
Angle: Turn the OMB M-24-04/CISA secure-software attestation into code by wiring SSDF 1.1 CI/CD controls, software provenance, and SBOM in builds directly into your pipeline—so Legal can file confidently and Engineering keeps shipping.

Who this is for: Engineering leaders, platform teams, and DevSecOps owners who need a secure software attestation that’s reproducible from CI signals—without building a whole new stack.
What you’ll build in 20 minutes
- SSDF 1.1 gates for: secure build environments, dependency hygiene, signing/provenance, and release reviews—with artifacts auto-exported for the attestation form.
- SBOM in builds (CycloneDX) + vuln scanning that fails on critical issues.
- Software provenance (SLSA-style) + artifact signing (cosign) + verify-at-deploy.
- An immutable evidence bucket and a tiny script that maps pipeline checks to the attestation questions.
Step 1 — Lock down the build environment (ephemeral, minimal, signed)
SSDF 1.1 CI/CD emphasizes trustworthy builds. Use ephemeral runners and least-privilege tokens, then sign everything your pipeline emits.
# .github/workflows/secure-build.yml
name: secure-build
on: [push]
permissions:
contents: read
id-token: write # for OIDC -> cloud
packages: read
security-events: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 1 } # no untrusted history in workspace
- name: Harden runner
run: |
set -euo pipefail
sudo sysctl -w kernel.unprivileged_bpf_disabled=1
sudo mount -o remount,ro /boot || true
- name: Set up Go/Node (example)
uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Build
run: npm ci && npm run build
Why it matters: Predictable, ephemeral builds reduce the chance of cross-build contamination, a key pillar for secure software attestation.
Step 2 — Generate an SBOM in builds (CycloneDX)
Create an SBOM in builds and keep it next to your artifact.
sbom:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: Install CycloneDX Node tool
run: npm i -g @cyclonedx/cyclonedx-npm
- name: Make SBOM (CycloneDX JSON)
run: cyclonedx-npm --output-file sbom.cdx.json --spec-version 1.5
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cdx.json
Step 3 — Scan dependencies & fail on critical vulns
Treat “critical” as build-failing and keep the report for attestation evidence.
deps-scan:
runs-on: ubuntu-latest
needs: sbom
steps:
- uses: actions/download-artifact@v4
with: { name: sbom }
- name: Install grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Scan SBOM
run: grype sbom:sbom.cdx.json -o json > grype.json || true
- name: Fail on CRITICAL
run: |
jq -e '.matches | any(.vulnerability.severity=="Critical")' grype.json && \
{ echo "Critical vulns found"; exit 1; } || echo "No criticals"
- uses: actions/upload-artifact@v4
with:
name: grype-report
path: grype.json
Quick external posture snapshot (to seed fixes)
Before release, run a fast check of your public assets and attach the result to your evidence:
Free Website Vulnerability Scanner Landing Page:

Step 4 — Sign your artifact and emit SLSA-style provenance
Use cosign for signing and a provenance attestation that records who built what, when, and from which commit.
sign-and-attest:
runs-on: ubuntu-latest
needs: build
env:
IMAGE: ghcr.io/acme/app:${{ github.sha }}
steps:
- uses: actions/checkout@v4
- name: Build container
run: docker build -t $IMAGE .
- name: Login to GHCR
run: echo $CR_PAT | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
env:
CR_PAT: ${{ secrets.CR_PAT }}
- name: Push image
run: docker push $IMAGE
- name: Install cosign
run: curl -sSfL https://raw.githubusercontent.com/sigstore/cosign/main/install.sh | sh -s -- -b /usr/local/bin
- name: Keyless sign (OIDC)
run: cosign sign $IMAGE --yes
- name: Generate provenance (in-toto statement)
run: |
cat > provenance.json <<'JSON'
{
"_type": "https://in-toto.io/Statement/v0.1",
"subject": [{"name": "${{ env.IMAGE }}"}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"builder": {"id": "github-actions"},
"buildType": "container",
"invocation": {"parameters": {"sha": "${{ github.sha }}"}}
}
}
JSON
- name: Attach provenance (cosign attach)
run: cosign attest --attachment-tag=provenance $IMAGE --predicate provenance.json --yes
- uses: actions/upload-artifact@v4
with: { name: provenance, path: provenance.json }
Step 5 — Verify at deploy (policy gate)
Only deploy images that are signed and have valid provenance.
# .github/workflows/deploy.yml (snippet)
name: deploy
on: [workflow_dispatch]
jobs:
verify-and-deploy:
runs-on: ubuntu-latest
env:
IMAGE: ghcr.io/acme/app:${{ inputs.sha }}
steps:
- name: Install cosign
run: curl -sSfL https://raw.githubusercontent.com/sigstore/cosign/main/install.sh | sh -s -- -b /usr/local/bin
- name: Verify signature
run: cosign verify $IMAGE --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp "https://github.com/acme/.+"
- name: Verify provenance (attestation)
run: cosign verify-attestation $IMAGE --type slsaprovenance --certificate-oidc-issuer https://token.actions.githubusercontent.com
- name: Deploy (only if both checks passed)
run: ./scripts/deploy.sh $IMAGE
Step 6 — Release review + evidence capture
Enforce a release review and persist the decision (who approved, what was reviewed, links to SBOM, scan, provenance).
release-review:
runs-on: ubuntu-latest
needs: [deps-scan, sign-and-attest]
steps:
- uses: actions/checkout@v4
- name: Require code owners + 2 approvals
run: |
[[ "${{ github.event.pull_request.mergeable_state }}" != "blocked" ]] || \
{ echo "Approvals missing"; exit 1; }
- name: Compose release-evidence.json
run: |
jq -n --arg sha "${{ github.sha }}" \
--arg approver "${{ github.actor }}" \
--slurpfile sbom sbom.cdx.json \
--slurpfile grype grype.json \
--slurpfile prov provenance.json \
'{commit:$sha, approver:$approver, sbom:$sbom[0], scan:$grype[0], provenance:$prov[0]}' \
> release-evidence.json
- uses: actions/upload-artifact@v4
with: { name: release-evidence, path: release-evidence.json }
Step 7 — Store artifacts in an immutable evidence bucket
Keep your secure software attestation proofs safe with S3 Object Lock + versioning.
# evidence_store.tf (Terraform)
resource "aws_s3_bucket" "evidence" {
bucket = "org-secure-evidence"
object_lock_enabled = true
}
resource "aws_s3_bucket_versioning" "evidence" {
bucket = aws_s3_bucket.evidence.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "evidence" {
bucket = aws_s3_bucket.evidence.id
rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } }
}
resource "aws_s3_bucket_object_lock_configuration" "evidence" {
bucket = aws_s3_bucket.evidence.id
rule {
default_retention { mode = "COMPLIANCE" days = 365 }
}
}
CI upload example:
publish-evidence:
runs-on: ubuntu-latest
needs: [release-review]
steps:
- uses: actions/download-artifact@v4
with: { path: artifacts }
- name: Upload to S3 (immutable)
run: |
aws s3 cp artifacts s3://org-secure-evidence/${{ github.run_id }}/ --recursive --sse AES256
Map pipeline checks → attestation questions (auto-export)
Export a tiny JSON summary the compliance team can drop into the form.
# attest_summary.py
import json, pathlib
def emit():
sbom_exists = pathlib.Path("sbom.cdx.json").exists()
grype = json.loads(pathlib.Path("grype.json").read_text()) if pathlib.Path("grype.json").exists() else {}
criticals = [m for m in grype.get("matches", []) if m.get("vulnerability",{}).get("severity")=="Critical"]
out = {
"ssdf_1_1": True,
"secure_build_env": True,
"sbom_in_builds": sbom_exists,
"vuln_policy": "fail_on_critical",
"criticals_found": len(criticals),
"provenance_emitted": pathlib.Path("provenance.json").exists(),
"artifact_signed": True,
"release_review": pathlib.Path("release-evidence.json").exists()
}
print(json.dumps(out, indent=2))
if __name__ == "__main__":
emit()
Add to CI:
attest-export:
runs-on: ubuntu-latest
needs: [release-review]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with: { path: . }
- name: Emit attestation summary
run: python attest_summary.py > attestation-summary.json
- uses: actions/upload-artifact@v4
with: { name: attestation-summary, path: attestation-summary.json }
One-file blueprint: SSDF 1.1 CI/CD starter
Drop this reusable workflow into .github/workflows/ssdf11.yml and call it from your apps.
name: ssdf11-starter
on:
workflow_call:
inputs:
image_name: { required: true, type: string }
secrets:
CR_PAT: { required: true }
permissions:
contents: read
id-token: write
security-events: write
packages: write
jobs:
build-sbom-scan-sign:
uses: ./.github/workflows/secure-build.yml
verify-release:
uses: ./.github/workflows/deploy.yml
with:
sha: ${{ github.sha }}
“Attach evidence” playbook for Legal & Sales
- Bundle:
sbom.cdx.json,grype.json,provenance.json, cosign verification output,release-evidence.json, andattestation-summary.json. - Store in the immutable bucket (Object Lock).
- Reference the evidence bundle in vendor questionnaires and secure software attestation filings to cut turnaround time.
Sample vulnerability report—to check Website Vulnerability:

Where Cyber Rely helps next
- Need a gap view before you wire the SSDF gates? Book a Risk Assessment to prioritize quick wins:
👉 Risk Assessment Services - Have findings from SBOM/vuln scans or missing provenance? We’ll help remediate with developers:
👉 Remediation Services
For more developer-ready playbooks, explore the Cyber Rely blog and these recent reads:
- OWASP GenAI Top 10: 10 Proven Dev Fixes (prompt injection, tool allow-lists, CI gates).
- 5 Blazing Steps to a SEC Item 1.05 Pipeline (Cyber 8-K) (materiality signals, evidence store).
- 7 Powerful Steps: Add an ASVS 5.0 Gate to CI/CD (SAST/DAST + “evidence that sticks”).
Wrap-up
You don’t need a new platform to satisfy secure software attestation—you need SSDF 1.1 CI/CD gates that produce software provenance and evidence as a by-product of normal shipping. Use the workflows above, attach your evidence bundle, and you’re ready for CISA’s attestation form—and vendor due diligence—without slowing delivery.
If you want a second set of eyes on your plan, talk to us at Cyber Rely: https://www.cybersrely.com/ or run a quick snapshot now: https://free.pentesttesting.com/.
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about SSDF 1.1 CI/CD attestation.