agentsclimarketplace

Cicd hardening

Skill roodlicht/accans-sec-skills/skills/cicd-hardening

CI/CD pipeline hardening for GitHub Actions and GitLab CI — trust-model (pull_request_target vs pull_request), action pinning to SHA, OIDC-based cloud access, permissions minimization, runner isolation, and supply-chain gates (SLSA provenance, signing).From its SKILL.md

Install
npx -y skills add roodlicht/accans-sec-skills --skill cicd-hardening

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

13.7 KB, ~3.3k tokens by cl100k_base, as published. Nobody here has run it

CI/CD Hardening

When to use

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:

  • A question like "review our workflow files for security", "set up OIDC between GitHub and AWS", "why is pull_request_target dangerous", "pin all actions to SHA", "can we hit SLSA-L3".
  • New or changed .github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile, Azure Pipelines YAML, CircleCI config, reusable workflow definitions, composite actions.
  • An incident or near-miss: a forked PR could use secrets, a third-party action had a compromise, a self-hosted runner was abused.
  • A compliance audit asking for SSDF evidence or SLSA-level attestation.
  • A supply-chain moment where the pipeline is the last link before release (supply-chain skill calls in here for build-provenance setup).

When NOT (handoff)

  • SAST in the pipeline → sast-orchestrator for tool choice and ruleset. This skill only decides that it runs somewhere in the workflow phase.
  • Secret scanning in the pipeline → secrets-scanner. Same split.
  • Dependency scanning and CVE triage → cve-triage and supply-chain. The gate logic is in security-gate.
  • Container build itself (Dockerfile, base image) → container-hardening. We only cover the workflow that builds it here.
  • Kubernetes deployment by a GitOps agent (Argo CD, Flux) → k8s-security.
  • Infrastructure provisioning (Terraform apply from CI) → iac-security for the resources side, here only the creds flow via OIDC.
  • IR for an actually compromised pipeline → ir-runbook.

Approach

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.

1. Trust model and workflow triggers

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.

2. Action pinning and provenance

Third-party Actions are the underestimated supply-chain risk. An owner getting compromised or an action mutating affects everyone using the tag.

  • Pin to SHA, not to 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.
  • Configure Renovate/Dependabot to update SHA pins with changelog review. Without auto-updates you run on stale, vulnerable Actions.
  • Verified publishers and Actions from trusted orgs (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.
  • Lint tools: zizmor (Rust, OSS) for GitHub Actions audit — catches pull_request_target misuse, impersonation, expression injection. actionlint for syntax. poutine for cross-platform CI analysis.
  • GitLab equivalent: pin included templates to a fixed version, no @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.

3. Secrets and creds: OIDC first

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.

  • GitHub → AWS: OIDC provider in AWS (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.
  • GitHub → GCP: Workload Identity Federation, google-github-actions/auth.
  • GitHub → Azure: federated identity credentials on the App Registration.
  • GitLab → cloud: id_tokens feature (JWT per job) with the equivalent OIDC setup on the cloud side.

OIDC trust policies:

  • Scope as narrowly as possible. Trust only specific branches (refs/heads/main), or better: environments (environment:production). Not on repo:<org>/<repo>:*.
  • Audience claim explicit on sts.amazonaws.com or cloud equivalent. Default is wide; you want it narrow.
  • No fallback to static keys. If OIDC for whatever reason does not work, fail the pipeline — no "oh well, let's use the old 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-scoped in GitHub Actions (environment: production with protected flag and required reviewers). Or GitLab protected variables on a protected branch.
  • Remove after use where possible; some tooling (HashiCorp Vault integration) generates just-in-time creds.
  • Never echo. A bug where a secret accidentally lands in log output: GitHub auto-masks but is not infallible. Disable set -x / --debug in steps that touch secrets.

4. Permissions: minimal per workflow

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.

5. Runners: isolation and choice

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:

  • Persistent state: a previous run can leave secrets/artifacts behind for the next run, cross-job contamination.
  • Fork-PR exposure: a forked PR running on a self-hosted runner has access to the entire host. Default advice: self-hosted runners only for workflows that cannot be triggered by forked PRs. Private repos or internal-only workflows.
  • Network positioning: a self-hosted runner in a VPC can reach things you do not want it to.

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.

6. Supply-chain gates in the pipeline

The place where supply-chain, secrets-scanner, cve-triage, sast-orchestrator, and security-gate land as CI steps.

  • Per-PR (blocking): /security-gate as a required check, which internally runs secrets-scanner + sast-orchestrator + cve-triage.
  • Per-build (artifact-related): generate SBOM (syft), sign attestation (cosign), SLSA provenance via slsa-framework/slsa-github-generator. See supply-chain phase 2.
  • Per-release (additional): image scan with Trivy/Grype, signing verify, provenance verify. Fail-fast on unsigned or unresolved-critical.
  • OpenSSF Scorecard as a periodic self-assessment of pipeline hygiene: pinned Actions, branch-protection, SAST present, etc. Reports a score plus per-check detail.

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.

7. Verification-loop

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.

Output

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

References

Categories

  • appsec

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.