Harden GitHub Actions, GitLab CI, and similar pipelines against supply-chain attacks, secret exfiltration, and pwn-request abuse. Use when authoring or reviewing workflow files, adding a third-party action, image, or script, wiring cloud or registry credentials into CI, or triaging a suspected pipeline compromise.
22 stars
0 votes
0 copies
1 view
Added September 6, 2026
devopsrustgoshellbashexpressdockerawsgcpazuregit
Works with
api
Security analysis
F1/100
criticalPipes output to a shell interpreter
mediumUses curl or wget to download content
criticalDownloads and executes remote scripts — classic supply chain attack
criticalDownloads and executes remote scripts — classic supply chain attack
mediumInstalls packages at runtime which could introduce malicious dependencies
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Cicd Security?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-cicd-security)
---
name: cicd-security
description: "Harden GitHub Actions, GitLab CI, and similar pipelines against supply-chain attacks, secret exfiltration, and pwn-request abuse. Use when authoring or reviewing workflow files, adding a third-party action, image, or script, wiring cloud or registry credentials into CI, or triaging a suspected pipeline compromise."
---
<!-- Native skill bundle for agent-skills (cross-tool convention). Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/cicd-security/SKILL.md. -->
# CI/CD Pipeline Security
Harden GitHub Actions, GitLab CI, and similar pipelines against supply-chain attacks, secret exfiltration, and pwn-request abuse. Use when authoring or reviewing workflow files, adding a third-party action, image, or script, wiring cloud or registry credentials into CI, or triaging a suspected pipeline compromise.
## ALWAYS
- Pin every third-party GitHub Action by **commit SHA** (full 40-char), never by a floating tag (`@v1`, `@main`, `@latest`) — tags can be re-pushed. The tj-actions/changed-files March 2025 incident exfiltrated secrets from 23,000+ repositories specifically because consumers used floating tags. 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** — the "pwn request" pattern documented by GitHub Security Lab. Either don't run them, or run with no secrets and no write tokens. A fork's GitLab merge-request pipeline is the same shape — it runs the fork's own `.gitlab-ci.yml` on your runners: isolate them, expose no protected variables.
- 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. The GitLab sink has the same shape: `$CI_*` / `$TRIGGER_*` interpolated into `bash -c` or `eval`, where merge-request metadata carries the shell metacharacters.
- Sign release artifacts (Sigstore / cosign) and publish SLSA provenance attestations. Verify provenance in any consumer pipeline that pulls the artifact.
- Pin `runs-on` to a dated runner image (`ubuntu-24.04`), not a floating label (`ubuntu-latest`) — the label moves underneath you. On GitLab, use ephemeral Docker-executor runners with `privileged: false`; a shared shell runner with `privileged: true` gives a compromised job root on the host. An egress firewall on runners handling secrets (StepSecurity Harden-Runner or equivalent) is defense-in-depth on top of that, not a substitute.
- Require a human reviewer on a dependency-bump PR when `supply-chain-security` raises a **trust-boundary** change — a maintainer or ownership change, a new or changed install hook, a registry or source transition, a provenance regression. Gate on that signal, not on the version number: a patch release that changes maintainer warrants more review than a major bump that does not.
- Consult `supply-chain-security` before any dependency install step (`npm install`, `pip install`, `docker pull`) — in CI it is untrusted code execution on a runner holding your credentials. It owns install-script risk (`postinstall`, `setup.py`, `build.rs`) and registry pinning; `container-security` names the command that fixes the lockfile half (`npm ci`, `--frozen-lockfile`) rather than adding flags to `npm install`.
## NEVER
- `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.
- 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. GitLab's equivalent: a CI/CD variable without `protected: true` + `masked: true` is exposed to every feature-branch pipeline.
## 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.
## Reference files
Read these only when the task calls for them.
- `references/verifying-findings.md`