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-scripts stops 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

SituationSafe approachWhy
Pull request from a forkpull_request with no secretsThe workflow runs untrusted code
Needing secrets on a PRworkflow_run after approval, or a label gateKeeps credentials out of the untrusted context
Building images from a PRPush to a scratch namespace, never a deploy tagPrevents replacing production
Running on self-hosted runnersOnly for private repositoriesA runner is a machine an attacker can own
Third-party actionPin the SHA and review the codeIt 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/trivy-action@0.28.0
        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: true

Scanning 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.

Building and publishing container images in CI Infrastructure as code in the pipeline

Last refreshed 2026-09-18.