From project-orchestration-skills
Hardens GitHub Actions CI/CD workflows against supply-chain attacks by SHA-pinning actions, enforcing least-privilege token permissions, verifying toolchain installs, and adding OpenSSF Scorecard/SLSA provenance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/project-orchestration-skills:harden-github-actionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Harden GitHub Actions CI/CD workflows against supply-chain attack: pin what runs, minimise what it can do, and verify what it fetches.
Harden GitHub Actions CI/CD workflows against supply-chain attack: pin what runs, minimise what it can do, and verify what it fetches.
GitHub-specific by design. Unlike most orchestration skills, this one is deliberately bound to one forge. The hardening controls below are not portable concepts wearing GitHub syntax — they are properties of the GitHub Actions execution model itself: third-party actions resolved by mutable git ref, an ambient
GITHUB_TOKENwith repository-wide default scopes, and OIDC-backed SLSA provenance. It has a sibling,harden-gitlab-ci, which is not a translation of this skill: GitLab's risks sit in different places (include:/CI-Catalog components, and aCI_JOB_TOKENthat defaults to own-project-only — so there the work is keeping it scoped, the opposite posture from here). Forgejo Actions is Actions-compatible in shape but resolves actions against its instance's configured registry, so pinning guidance does not transfer unchanged (roadmap H9).
setup-pre-commit).github/workflows/ before making a repository publicsupply-chain skill) flags CI hardening gapsNot for: GitLab CI — use harden-gitlab-ci. Not for Forgejo/Gitea Actions — the controls do not carry over unchanged. Say so rather than approximating.
This skill hardens CI workflows. validate-quality-config only reads CI to confirm parity with local hooks — it does not check pinning, permissions, or provenance. The two are complementary.
.github/workflows/*.ymlpublish_results and OSSF publishing)A CI job runs third-party code (actions, installed tools) with a GITHUB_TOKEN and often OIDC. A mutable action tag (@v4) can be repointed to malicious code; an over-privileged token can push commits, publish packages, or exfiltrate secrets; an unverified curl | sh install can be swapped upstream. Hardening closes all three: pin, least-privilege, verify.
Find every workflow and the hardening gaps in it:
ls .github/workflows/
# Mutable action refs (should be zero — all must be SHA-pinned):
grep -rnE 'uses:.*@(v[0-9]|main|master|latest)' .github/workflows/
# Workflows missing a top-level permissions block:
for f in .github/workflows/*.yml; do grep -q '^permissions:' "$f" || echo "no permissions: $f"; done
# Floating tool versions:
grep -rnE 'version:\s*latest|@latest' .github/workflows/
Aim for a clean 3-way split: ci.yml (lint/test/build on push + PR), scorecard.yml (OpenSSF Scorecard on a schedule), and — only if the project ships artifacts — release.yml (SBOM + SLSA provenance + signing).
Pin every uses: to a full 40-character commit SHA with a trailing # vX.Y.Z comment for readability. Never rely on a mutable tag.
# Good — immutable, human-readable, Dependabot-updatable
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
- uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
# Bad — mutable tag, can be repointed upstream
- uses: actions/checkout@v4
Resolve a tag to its commit SHA:
gh api repos/actions/checkout/commits/v4.2.2 --jq '.sha'
Keep pins current automatically with a Dependabot github-actions update block (see bootstrap-project / the dependency-update config). Dependabot preserves the # vX.Y.Z comment when it bumps a SHA.
The one sanctioned exception: reusable workflows that verify their own release tag — notably slsa-framework/slsa-github-generator — must be referenced by semantic version tag, not SHA (see Step 7). Document the exception in an ADR and in a comment on the line.
Set a read-only default at the top of every workflow, then elevate per job only where a job genuinely needs to write.
# top of the workflow
permissions:
contents: read
Elevate narrowly, in the specific job:
jobs:
# OpenSSF Scorecard
analysis:
permissions:
security-events: write # upload the SARIF result
id-token: write # publish results to the OSSF API
contents: read
# Release provenance / publish
provenance:
permissions:
actions: read # read the workflow path
id-token: write # mint the OIDC token for signing
contents: write # attach provenance to the release
# packages: write # add only if publishing to GHCR / a registry
Rule of thumb: default contents: read; add id-token: write only for OIDC signing/publish; add contents: write only for jobs that create releases/tags; add packages: write only for registry publish; add security-events: write only for SARIF upload.
On any workflow that runs with elevated permissions or handles releases (Scorecard, release), stop the checkout from leaving credentials on disk:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
Every tool a job installs is attack surface. In order of preference:
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: { distribution: temurin, java-version: "21" }
- uses: xu-cheng/texlive-action@22c04326a5d855880f9d39bb955138bf11c6df80 # v3
- run: |
curl -fsSL -o tool.tar.gz "https://example.com/tool/v1.2.3/tool.tar.gz"
echo "abc123... tool.tar.gz" | sha256sum -c -
tar xzf tool.tar.gz
Anti-patterns to eliminate (all seen in real workflows):
- run: luarocks install luacheck # no version pin
- run: apt-get install -y -qq pandoc # unversioned distro package
with: { version: latest } # floating action release
- run: curl -fsSL https://x/install.sh | sh # unverified remote script — never
Digest-pin any container image referenced in a run/service step (image@sha256:…, not :latest); base-image and artifact-checksum hardening for the images themselves belongs to setup-container-security.
Add a Scorecard workflow to continuously grade the repo's supply-chain posture. Canonical scorecard.yml:
name: Scorecard
on:
branch_protection_rule:
schedule:
- cron: "26 7 * * 1" # weekly, Mondays
workflow_dispatch:
permissions: read-all
jobs:
analysis:
runs-on: ubuntu-latest
permissions:
security-events: write # upload the SARIF result
id-token: write # publish results to the OSSF API
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true # requires a PUBLIC repo; keep false while private
- uses: github/codeql-action/upload-sarif@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
with:
sarif_file: results.sarif
On a private repo set publish_results: false and flip it to true at public release.
For projects that publish artifacts/images/packages, attach SLSA build provenance at release. Hash the build outputs, then hand off to the generator — referenced by tag (the sanctioned exception to Step 2):
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
# slsa-github-generator MUST be tag-pinned (it verifies its own release tag) — the
# documented exception to the SHA-pin rule; record it in an ADR.
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
base64-subjects: ${{ needs.build.outputs.hashes }}
upload-assets: true
Provenance, SBOM (syft), and keyless signing (cosign) at release time are owned by wrapup-sprint's release step — this skill wires the workflow permissions and the tag-pin exception that make them safe.
dependency-review:
if: github.event_name == 'pull_request'
steps:
- uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
concurrency block per workflow:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
timeout-minutes: (a runaway job holds a token).retention-days: 5 on upload-artifact.step-security/harden-runner (SHA-pinned) to audit/block network egress.codeql-action/init + analyze) for compiled/scripted languages — note: uploading a Scorecard SARIF is not code scanning.This skill produces/updates:
uses: in .github/workflows/* SHA-pinned with # vX.Y.Z comments (grep for @v[0-9]/@main returns nothing except the SLSA generator)permissions: (read-only default) on every workflow, with narrow per-job elevationpersist-credentials: false on release/Scorecard checkoutscurl | sh, no version: latest.github/workflows/scorecard.yml.github/workflows/release.yml provenance job (if the project ships artifacts)concurrency + timeout-minutes on workflows/jobs# No mutable action refs remain (SLSA generator is the only allowed tag ref):
grep -rnE 'uses:.*@(v[0-9]|main|master|latest)' .github/workflows/ | grep -v slsa-github-generator
# Every workflow declares permissions:
for f in .github/workflows/*.yml; do grep -q '^permissions:' "$f" || echo "MISSING permissions: $f"; done
# No unverified remote installs:
grep -rnE 'curl.*\|\s*(sh|bash)' .github/workflows/
Optionally run actionlint for workflow-syntax validation, and check the repo's OpenSSF Scorecard once the workflow has run.
harden-gitlab-ci — the sibling for GitLab CI. Same intent, different execution model; neither is a translation of the other (GitLab's risks sit in include:/CI-Catalog components and the CI_JOB_TOKEN allowlist)setup-pre-commit — local quality gates that CI mirrorsvalidate-quality-config — verifies CI/local parity (reads CI; does not harden it)wrapup-sprint — release-time SBOM, SLSA provenance, and signingsetup-container-security — base-image digest pinning and image scanningsetup-adrs — record the SHA-pin policy and the SLSA-generator exceptionnpx claudepluginhub jrjsmrtn/jrjsmrtn-skills --plugin project-orchestration-skillsHardens GitHub Actions workflows against supply chain attacks, credential theft, and privilege escalation with SHA pinning, token minimization, script injection prevention, and required reviewers for changes.
Reviews GitHub Actions workflows for supply chain risks: enforces SHA pinning, rates third-party actions, scopes permissions, checks triggers and inputs, and recommends updates.
Hardens GitHub Actions workflows against supply chain attacks, credential theft, and script injection. Covers SHA pinning, token permissions, and injection prevention.