Cicd hardening
47 Claude skills for security tradecraft across AppSec, Pentest, Blue Team, and GRC. NL/EU regulatory-anchored. Pattern-level discipline.
npx -y skills add roodlicht/accans-sec-skills --skill cicd-hardeningAssembled 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.
What its author says it does
Copied from the file, not written here
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).
SKILL.md
13.7 KB, 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_targetdangerous", "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-chainskill calls in here for build-provenance setup).
When NOT (handoff)
- SAST in the pipeline →
sast-orchestratorfor 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-triageandsupply-chain. The gate logic is insecurity-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-securityfor 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@v4is mutable (the tag can be moved).uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683is 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 — catchespull_request_targetmisuse, impersonation, expression injection.actionlintfor syntax.poutinefor cross-platform CI analysis. - GitLab equivalent: pin included templates to a fixed version, no
@mainforinclude: 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 onrepo:<org>/<repo>:ref:refs/heads/mainorenvironment:<env>.aws-actions/configure-aws-credentials@v4SHA-pinned. - GitHub → GCP: Workload Identity Federation,
google-github-actions/auth. - GitHub → Azure: federated identity credentials on the App Registration.
- GitLab → cloud:
id_tokensfeature (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 onrepo:<org>/<repo>:*. - Audience claim explicit on
sts.amazonaws.comor 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_KEYfor 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: productionwith 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/--debugin 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-gateas a required check, which internally runssecrets-scanner+sast-orchestrator+cve-triage. - Per-build (artifact-related): generate SBOM (syft), sign attestation (cosign), SLSA provenance via
slsa-framework/slsa-github-generator. Seesupply-chainphase 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
- GitHub Actions Security Hardening — https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions. Primary GitHub docs on workflow security.
- GitHub Actions OIDC — https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect.
- GitLab CI Security — https://docs.gitlab.com/ee/ci/pipelines/ and https://docs.gitlab.com/ee/ci/secrets/.
- OWASP Top 10 CI/CD Security Risks — https://owasp.org/www-project-top-10-ci-cd-security-risks/. Canonical categorization.
- NSA/CISA "Defending CI/CD Environments" — https://media.defense.gov/2023/Jun/28/2003249466/-1/-1/0/CSI_DEFENDING_CI_CD_ENVIRONMENTS.PDF. 2023 guidance.
- SLSA — https://slsa.dev/. Build-provenance framework, see also
supply-chain. - OpenSSF Scorecard — https://github.com/ossf/scorecard. Self-assessment of project hygiene.
- zizmor — https://github.com/woodruffw/zizmor. Static analysis for GitHub Actions.
- actionlint — https://github.com/rhysd/actionlint. Workflow syntax linter.
- slsa-github-generator — https://github.com/slsa-framework/slsa-github-generator. SLSA-L3 provenance in GitHub Actions.
- Trail of Bits — "Publishing Python packages from GitHub Actions" threat model (https://blog.trailofbits.com/2023/05/23/trusted-publishing-a-new-benchmark-for-packaging-security/). Good reference work for OIDC-trust setup.
Categories
- appsec