cicd-hardening — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cicd-hardening (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill treats the pipeline itself as the attack surface, not the code flowing through it. CI/CD compromise is a supply-chain incident: whoever owns the pipeline owns the release artifacts.
Triggers on:
pull_request_target dangerous", "pin all actions to SHA", "can we hit SLSA-L3"..github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile, Azure Pipelines YAML, CircleCI config, reusable workflow definitions, composite actions.supply-chain skill calls in here for build-provenance setup).sast-orchestrator for tool choice and ruleset. This skill only decides that it runs somewhere in the workflow phase.secrets-scanner. Same split.cve-triage and supply-chain. The gate logic is in security-gate.container-hardening. We only cover the workflow that builds it here.k8s-security.iac-security for the resources side, here only the creds flow via OIDC.ir-runbook.Seven phases. Phase 1 (trust model) is the heart — what goes wrong in CI attacks is often a misconception about who is allowed to run what.
GitHub Actions — the `pull_request_target` trap.
pull_request (default) runs workflow code from the PR branch and has no access to the base repo's secrets. Safe for fork PRs.
pull_request_target runs the workflow code from the base branch (so trusted) but with the PR context. Has secrets. Intended for scenarios like "label the PR after a lint check". Extremely dangerous if you execute the PR code (via checkout + test run), because then your base-trusted workflow runs untrusted fork code with secrets on board. This is the attack class GitHub itself calls "poisoned pipeline execution".
Rule: if you use pull_request_target, never check out the PR branch and never execute code from the PR. Do: set labels, post comments, read metadata.
GitLab CI — merge request pipelines.
merge_request_event runs on the source branch with access to variables depending on the protected flag. Protected variables are only available on protected branches — set prod secrets to protected. Cross-project triggers via pipeline trigger tokens are another vector; treat those tokens as secrets.
Protected branches and required checks.
Set main/release branches to protected: no direct push, review-required, status-checks-required, linear history, no force-push. security-gate as a required check. Without branch protection, a compromised developer account can start release pipelines directly.
CODEOWNERS: required review by specific teams for critical paths (.github/workflows/, infra/, deploy/). Without CODEOWNERS enforcement at branch protection, it is advice, not policy.
Third-party Actions are the underestimated supply-chain risk. An owner getting compromised or an action mutating affects everyone using the tag.
uses: actions/checkout@v4 is mutable (the tag can be moved). uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 is immutable. For critical workflows (release, deploy): always SHA-pin.actions/, github/, docker/, aws-actions/, azure/, google-github-actions/). Third-party marketplace Actions with 10 stars and one maintainer are a choice you have to be able to defend.zizmor (Rust, OSS) for GitHub Actions audit — catches pull_request_target misuse, impersonation, expression injection. actionlint for syntax. poutine for cross-platform CI analysis.@main for include: remote: or shared templates.Expression injection. ${{ github.event.pull_request.title }} directly in a run: block is command injection: a PR title with `" ; rm -rf / # runs on the runner. Fix: pass the value via an env variable (env: TITLE: ${{ github.event.pull_request.title }}, then "$TITLE"` in the script). Zizmor catches this.
Long-lived cloud credentials in CI secrets is the classic bad pattern. OIDC fixes it: the CI runner gets a short-lived, audience-bound token from the identity provider, the cloud side accepts that token and hands out temporary IAM creds.
token.actions.githubusercontent.com), IAM role with trust policy on repo:<org>/<repo>:ref:refs/heads/main or environment:<env>. aws-actions/configure-aws-credentials@v4 SHA-pinned.google-github-actions/auth.id_tokens feature (JWT per job) with the equivalent OIDC setup on the cloud side.OIDC trust policies:
refs/heads/main), or better: environments (environment:production). Not on repo:<org>/<repo>:*.sts.amazonaws.com or cloud equivalent. Default is wide; you want it narrow.AWS_SECRET_ACCESS_KEY for now". That key does not exist anymore, if all is well.Remaining secrets (third-party API keys that do not support OIDC):
environment: production with protected flag and required reviewers). Or GitLab protected variables on a protected branch.set -x / --debug in steps that touch secrets.The default GITHUB_TOKEN in GitHub Actions was historically write-all. Since 2023 the default is read-only in new repos, but old repos do not flip automatically. Set explicitly:
permissions:
contents: read # default for most workflows
jobs:
deploy:
permissions:
contents: read
id-token: write # for OIDC
packages: write # for ghcr push, only in deploy job
...Workflow-level permissions set a ceiling; job-level can refine downward. For jobs that need push rights, scope strictly to that job.
GitLab CI equivalent: CI_JOB_TOKEN permissions via Project → Settings → CI/CD → Token access, with an allowlist of projects your token may use.
GitHub-hosted runners are ephemeral (fresh VM per job), good default. Cost: free for public repos, paid for private.
Self-hosted runners are persistent unless you re-provision them every run. Risks:
If you must self-host: ephemeral runners (actions-runner-controller on Kubernetes, or --ephemeral flag), non-root service account, runner scope per repo or per org and not cluster-wide, network-egress policy, no mount of docker.sock (gives a breakout).
Scale-set runners with firecracker/KVM isolation (GitLab) or runner groups with labels (GitHub) for further segmentation.
The place where supply-chain, secrets-scanner, cve-triage, sast-orchestrator, and security-gate land as CI steps.
/security-gate as a required check, which internally runs secrets-scanner + sast-orchestrator + cve-triage.slsa-framework/slsa-github-generator. See supply-chain phase 2.Log CI events to SIEM: who ran which workflow, which secrets were touched, which artifacts produced. Without an audit trail, a compromised-CI incident is not reconstructable.
Layer 1: scope (all workflow files covered? self-hosted runners inventoried? OIDC applied everywhere possible?), assumptions ("we pin to SHA" only if you have actually read the uses: lines), gaps (pull_request_target carefully checked? expression-injection checks done?), consistency (permissions scope matches what the job actually does).
Layer 2: Action SHAs and publisher identities are correct, OWASP CICD Top 10 mapping correct, no invented zizmor/actionlint rule IDs, SLSA-level claims supported.
CI/CD hardening review — <repo/pipeline>
Platform: <GitHub Actions | GitLab CI | Jenkins | Azure | ...>
Trust model:
Triggers used: <list pull_request/pull_request_target/push/etc>
pull_request_target risk:<clean | finding with context>
Protected branches: <config summary>
CODEOWNERS active: <yes/no>
Action pinning:
% Actions SHA-pinned: <N/M>
Third-party from unknown orgs: <list>
Expression injection: <clean | findings>
Credentials:
OIDC to cloud: <AWS/GCP/Azure, trust scope>
Long-lived secrets: <N, which and why still>
Environment-protected: <yes/no per critical environment>
Permissions:
Workflows with default write-all: <N>
permissions: empty or too broad: <list>
Runners:
Self-hosted in use: <yes/no + scope>
Ephemeral: <yes/no>
Fork-PR access to self-hosted: <blocked | open>
Supply-chain gates:
SAST in pipeline: <sast-orchestrator handoff>
Secret scan: <secrets-scanner handoff>
Dep scan: <cve-triage handoff>
SBOM + provenance: <supply-chain handoff>
Scorecard: <score + weakest checks>
Findings (severity-sorted, follow security-review format)
Verification-loop: ...supply-chain.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.