--- name: cicd-security description: "Harden GitHub Actions, GitLab CI, and similar pipelines against supply-chain attacks, secret exfiltration, and pwn-request style abuses — Applies to: when authoring or reviewing CI/CD workflow files; when adding a third-party action / image / script to a pipeline; when wiring cloud or registry credentials into CI; when triaging a suspected pipeline compromise" --- # CI/CD Pipeline Security Harden GitHub Actions, GitLab CI, and similar pipelines against supply-chain attacks, secret exfiltration, and pwn-request style abuses ## ALWAYS - Pin every third-party GitHub Action by **commit SHA** (full 40-char), not by tag — tags can be re-pushed. Same applies to GitLab CI `include:` references and reusable workflows. Renovate / Dependabot can keep the SHA pins fresh. - Declare `permissions:` at the workflow or job level and default to `contents: read` only. Grant additional scopes (`id-token: write`, `packages: write`, etc.) job-by-job, never workflow-wide. - Use **OIDC** (`id-token: write` + cloud provider trust policy) for short-lived cloud credentials. Never store long-lived AWS / GCP / Azure keys as GitHub Secrets. - Treat `pull_request_target`, `workflow_run`, and any `pull_request` job that uses `actions/checkout` with `ref: ${{ github.event.pull_request.head.ref }}` as **trusted-context-on-untrusted-code**. Either don't run them, or run with no secrets and no write tokens. - Echo every untrusted expression (`${{ github.event.* }}`) through an environment variable first; never interpolate it directly into `run:` body — that's the canonical GitHub Actions script-injection sink. - Sign release artifacts (Sigstore / cosign) and publish SLSA provenance attestations. Verify provenance in any consumer pipeline that pulls the artifact. - Set `runs-on` to a hardened runner image and pin the runner version. Audit-mode StepSecurity Harden-Runner (or equivalent egress firewall) for any workflow handling secrets is recommended. - Treat `npm install`, `pip install`, `go install`, `cargo install`, and `docker pull` invoked in CI as untrusted code execution. Run with `--ignore-scripts` (npm/yarn), pinned lockfiles, registry allowlists, and per-job least-privilege tokens. ## NEVER - Pin a third-party action by floating tag (`@v1`, `@main`, `@latest`). The tj-actions/changed-files March 2025 incident exfiltrated secrets from 23,000+ repositories specifically because consumers used floating tags. - `curl | bash` (or `wget -O- | sh`) any installer script in CI. The 2021 Codecov bash-uploader compromise exfiltrated env vars to an attacker for ~10 weeks because thousands of pipelines ran `bash <(curl https://codecov.io/bash)`. Always download, checksum, then execute. - Echo secrets to logs, even on failure. Use `::add-mask::` for any computed-at-runtime secret, and double-check with the GitHub workflow-log search. - Allow workflows to run on forked PRs with `pull_request_target` if any job touches a write-scoped token or secret. The combination is the canonical "pwn request" pattern documented by GitHub Security Lab. - Cache mutable state (e.g. `~/.npm`, `~/.cargo`, `~/.gradle`) keyed only on `os`. A cache hit cross-job is a cross-tenant attack surface — key on a lockfile hash and scope to the workflow ref. - Trust artifact downloads from arbitrary workflow runs without verifying the source workflow + commit SHA. Build-cache poisoning works through unscoped artifact reuse. - Store secrets in repository variables (`vars.*`) — they are plaintext to anyone with read access. Only `secrets.*` are gated by the secret scanning + scope rules. ## KNOWN FALSE POSITIVES - First-party actions in the same organization that you mirror or fork in-house may legitimately be pinned by tag if the org enforces signed tags + branch-protection on the action repo. - Public-data pipelines that handle no secrets and produce no signed artifact (e.g. nightly link-checkers) don't need OIDC or SLSA provenance, and may use floating tags without practical impact. - `pull_request_target` is legitimate for label / triage bots that only call the GitHub API with the minimal scopes needed, do not check out PR code, and don't expose secrets in env.