Audit Logs, Provenance, and Supply Chain Security
A pipeline that builds and deploys reliably still has two open questions: who did what, and can you prove an artifact is what your workflow actually built rather than something tampered with along the way? Audit logs answer the first question. Provenance and supply chain controls answer the second. Together they turn a working CI/CD pipeline into one you can defend during an incident review or a security audit.
Overview / How it works
An audit log is a record of actions taken against your repository, organization, or workflows: who triggered a run, who changed a secret, who edited branch protection, who approved a deployment. GitHub Actions run logs show what a workflow did, but they only cover the workflow’s own execution. The organization-level audit log (Settings > Audit log, or the REST API) covers administrative and security-relevant events across the whole org, and by default is retained for a limited window on GitHub.com — typically 90 days for most event types — so long-lived investigations require exporting it to external storage.
Provenance answers a narrower but harder question: given an artifact (a binary, a container image), was it built by the workflow you expect, from the source you expect, without being altered afterward? GitHub Actions can generate build provenance attestations that follow the SLSA (Supply-chain Levels for Software Artifacts) model. These attestations are signed using a short-lived OIDC token that GitHub issues to the workflow run, verified through Sigstore’s keyless signing, and recorded in a public transparency log (Rekor). No long-lived signing key ever touches your repository.
Supply chain security is the practice of controlling everything your pipeline pulls in: third-party GitHub Actions, base container images, and package dependencies. Each of those is code you did not write, running with access to your build environment and sometimes your secrets. A compromised or hijacked dependency is one of the most common ways CI/CD pipelines get breached.
Syntax or workflow structure
Provenance and SBOM generation rely on a specific permissions block and a small set of actions:
permissions:
contents: read # checkout the repo, nothing more
id-token: write # request the OIDC token used for keyless signing
attestations: write # publish the attestation to the repo
packages: write # push to GitHub Container Registry, if used
steps:
- uses: actions/attest-build-provenance@
with:
subject-name: ghcr.io/OWNER/REPO
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
id-token: write is the permission most teams forget. Without it, the workflow cannot request the OIDC token that Sigstore uses to sign the attestation, and the step fails. Granting it does not expose a secret — the token is minted per run, scoped to this repository and workflow, and expires in minutes.
Examples
Example 1: Generate a software bill of materials (SBOM). An SBOM lists every dependency in a build, which is what lets you answer "are we affected by this CVE" without a manual audit.
name: SBOM
on:
push:
branches: [main]
permissions:
contents: read
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.7
- name: Generate SBOM
uses: anchore/sbom-action@0d445cb63d3a5b3e4b2bb7b6f6f6b0a1e0f2a9ec # v0.17.0
with:
format: cyclonedx-json
output-file: sbom.cdx.json
- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6a1 # v4.4.0
with:
name: sbom
path: sbom.cdx.json
retention-days: 90
Expected behavior: every push to main produces a downloadable sbom.cdx.json artifact listing every package and version pulled into the build, kept for 90 days for later comparison against new vulnerability disclosures.
Example 2: Build a container image and attach a signed provenance attestation.
name: Build and attest image
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
attestations: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.7
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Attest provenance
uses: actions/attest-build-provenance@8d8b5a3f4a3fac4cdd0b0ff5c1d5e4c8f0a3e9b6 # v1.4.3
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Expected behavior: the image is pushed by digest, GitHub records a signed statement that ties that exact digest to this repository, commit, and workflow run, and the attestation is visible under the repository’s Attestations tab and via the registry.
Example 3: Gate a deployment on provenance verification. Building an attestation is only useful if something checks it before deploying.
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
attestations: read
steps:
- name: Verify provenance before deploying
run: |
gh attestation verify \
oci://ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }} \
--owner ${{ github.repository_owner }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy verified image
run: ./scripts/deploy.sh ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
Expected behavior: gh attestation verify exits non-zero and stops the job if the image digest has no matching, correctly signed attestation from this repository owner — so a tampered or unverified image never reaches the production environment.
Step by step
- The workflow requests an OIDC token from GitHub’s identity provider, scoped to this run only (needs
id-token: write). - The build step produces an artifact and records its content digest, not just a mutable tag.
attest-build-provenanceuses the OIDC token to obtain a short-lived signing certificate from Sigstore’s Fulcio, signs a statement binding the digest to the source repo and workflow, and publishes it to Rekor’s transparency log.- The signed attestation is stored alongside the repository and, optionally, in the container registry.
- Before deployment, a separate job fetches the attestation for the exact digest being deployed and verifies the signature and the expected repository owner.
- Deployment proceeds only if verification succeeds; audit log entries record who approved the environment, if a protected environment with required reviewers is configured.
- Periodically, export the organization audit log to external storage (a SIEM, a log bucket) since GitHub’s built-in retention window is limited.
Common Mistakes
Mistake 1: Pinning actions to a mutable version tag. A tag like @v4 can be moved by the action’s maintainer — or by an attacker who compromises that maintainer’s account — to point at different code without changing the tag name your workflow references.
jobs:
build:
steps:
- uses: actions/checkout@v4
Correction: pin to the full commit SHA and keep the version as a comment for readability. This makes the action’s exact code immutable for your workflow, at the cost of needing to update the SHA manually (or via a bot such as Dependabot) when you want new features or patches.
jobs:
build:
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.7
Mistake 2: Granting broad permissions to make attestation "just work". Some teams hit a permissions error and respond by widening scope far past what’s needed.
permissions: write-all
Correction: grant only the specific permissions the job uses. A workflow with write-all can modify issues, pull requests, packages, and more — far beyond what attestation requires, and a serious liability if any step in the job is later compromised by a malicious dependency.
permissions:
contents: read
id-token: write
attestations: write
Best Practices
- Pin every third-party action to a full commit SHA, not a tag or branch, and comment the version for readability.
- Set the minimal explicit
permissions:block per job rather than relying on repository defaults or blanket write access. - Prefer OIDC-based keyless signing over long-lived signing keys or registry passwords stored as secrets.
- Reference and deploy container images by digest, not by mutable tag, so "what I tested" and "what I deployed" are provably the same bytes.
- Generate an SBOM on every build so you can answer vulnerability-disclosure questions without re-auditing dependencies from scratch.
- Verify provenance attestations as a required step before any protected-environment deployment, and fail closed if verification fails.
- Treat every third-party action and base image as untrusted code with access to your build environment; review or vendor actions you depend on heavily.
- Export the organization audit log to external, immutable storage on a schedule, since GitHub’s retention window does not cover long-term investigations.
- Require reviewers or CODEOWNERS approval for changes to workflow files themselves, since a modified workflow can bypass every other control on this list.
Practice Exercises
- Take an existing workflow that references actions by tag (for example
@v4) and rewrite it to pin each action to a commit SHA, adding a version comment for each. - Add an SBOM generation step to a build workflow and upload the result as a workflow artifact.
- Add a build provenance attestation step to an image-publishing workflow, using the minimal
permissions:block shown above, and confirm the attestation appears in the repository’s Attestations tab. - Write a deployment job that runs
gh attestation verifyagainst the image digest before deploying, and confirm the job fails when you point it at an unattested digest. - List the third-party actions used across your repository’s workflows and identify which ones are still referenced by a mutable tag.
Summary
Audit logs tell you who did what and when, at the repository and organization level, and need external retention for anything beyond GitHub’s default window. Provenance attestations, generated through GitHub’s OIDC-backed, SLSA-aligned tooling, prove that a specific artifact digest came from a specific workflow run and hasn’t been altered since. Supply chain security ties both together by minimizing what your pipeline trusts blindly: pin actions to commit SHAs, scope permissions tightly, reference artifacts by digest, and verify provenance before every deployment rather than assuming a green checkmark means the artifact is safe.
