Pipeline security and supply chain
Least-privilege tokens, pinning actions by SHA, secret scanning, dependency and container scanning, provenance, and running untrusted pull request code safely.
Least privilege and pinned dependencies
# the token is read-only unless a job asks for more
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read # explicit, so a later default cannot widen it
steps:
# pin by full commit SHA: a tag can be moved to point at new code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: { node-version: 22, cache: npm }
- run: npm ci --ignore-scripts
- run: npm test
publish:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # only this job can sign
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: ./publish.sh- A tag is a mutable pointer. Pinning a third-party action to a commit SHA is the only way to know what code will run next month.
npm ci --ignore-scriptsstops a dependency from running post-install code, which is a common supply-chain attack path.- Grant write permissions only to the job that genuinely needs them — publishing, releasing or commenting.
- Never echo a secret. Masked values in logs can still leak through base64, a URL or a diff, so scan the full output.
Running untrusted code
| Situation | Safe approach | Why |
|---|---|---|
| Pull request from a fork | pull_request with no secrets | The workflow runs untrusted code |
| Needing secrets on a PR | workflow_run after approval, or a label gate | Keeps credentials out of the untrusted context |
| Building images from a PR | Push to a scratch namespace, never a deploy tag | Prevents replacing production |
| Running on self-hosted runners | Only for private repositories | A runner is a machine an attacker can own |
| Third-party action | Pin the SHA and review the code | It runs with your token |
# untrusted workflow: no secrets, read-only token, no writes
on:
pull_request_target: # runs in the base repo, so be careful
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# NEVER check out the pull request head here: it would run untrusted code
# with a write token. Comment on metadata only.
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
...context.repo, issue_number: context.issue.number, labels: ['needs-review']
});⚠️
pull_request_target with a checkout of the pull request head is the single most exploited pattern in CI. It hands a write token and your secrets to code the attacker controls. If you must run untrusted code, do it in a separate workflow with no secrets after a maintainer approves.Scanning and provenance
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with: { fetch-depth: 0 }
- name: Secret scan
uses: gitleaks/gitleaks-action@v2
- name: Dependency review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
- name: Container scan
uses: aquasecurity/[email protected]
with:
image-ref: ghcr.io/example/app:sha-${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: '1'
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/example/app
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: trueScanning is only useful with a rule for what happens on a finding. Decide in advance which severities fail the build, which open a ticket, and which are accepted with a documented reason — otherwise the output becomes noise nobody reads.
FAQ
Do I have to pin every action by SHA?
Pin every third-party action. Actions from the platform vendor in a first-party namespace are lower risk but still benefit from pinning; a mutable tag on any action is a dependency you do not control.
How do I rotate a leaked secret?
Treat it as compromised the moment it appears in a log or a commit: revoke it, issue a new one, and audit what the old one could reach. Removing the commit is not remediation.
Related
Building and publishing container images in CI Infrastructure as code in the pipeline
Last refreshed 2026-09-18.