| name | github-actions-security |
| description | Apply a comprehensive GitHub Actions security checklist to audit, harden, and fix CI/CD workflows against supply chain attacks. Use this skill whenever the user mentions GitHub Actions security, workflow hardening, CI/CD supply chain risks, secret exposure in pipelines, pinning actions, OIDC vs static secrets, pull_request_target risks, script injection in workflows, self-hosted runner security, or artifact/cache poisoning. Also trigger when the user shares a workflow YAML file and wants it reviewed, audited, or improved for security. Even if the user only asks a narrow question like "is my workflow safe?" or "how do I pin actions?", use this skill to provide structured, checklist-backed guidance.
|
GitHub Actions Security Skill
A practical skill for auditing and hardening GitHub Actions workflows against supply
chain attacks, secret theft, poisoned pipeline execution, and excessive token permissions.
Source reference
Based on: https://corgea.com/learn/github-actions-security-checklist
Also informed by: GitHub's 2026 Actions security roadmap, OpenSSF, OWASP CI/CD Top 10,
and GitHub Security Lab guidance on preventing pwn requests.
When the user shares a workflow file
- Read the YAML carefully.
- Check it against all 10 checklist areas below.
- Report findings grouped by risk area with severity (Critical / High / Medium / Low).
- Provide concrete fixed snippets for every finding.
- Summarize with a prioritized remediation list.
The five controls to always check first
Before anything else, verify these five — they cover the highest-impact failures:
| # | Control | Why it matters |
|---|
| 1 | GITHUB_TOKEN permissions set to read-only by default | Limits blast radius of any compromised job |
| 2 | Third-party actions pinned to full commit SHA | Tags are mutable and can be hijacked |
| 3 | No pull_request_target for public repos or fork PRs | Runs privileged context on untrusted code |
| 4 | All PR/issue/commit metadata treated as untrusted input | Branch names and PR titles can inject shell commands |
| 5 | OIDC used for cloud access instead of static secrets | Short-lived credentials sharply reduce secret theft value |
Full checklist (10 areas)
1. Organization and repository defaults
2. Explicit workflow permissions
Minimal safe pattern:
permissions: {}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-commit-sha>
Trusted-build / privileged-publish split pattern:
Run untrusted code in a pull_request workflow (no secrets, read-only), then publish
only after CI passes via a workflow_run workflow that holds credentials:
on: [pull_request]
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npm test
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
id-token: write
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npm publish
3. Dangerous triggers and untrusted execution paths
workflow_run guard pattern: A workflow_run job runs with the base repo's full
permissions even when the upstream workflow was triggered by a fork PR. Always check the
upstream trigger and repo before taking any privileged action:
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Abort if triggered by a fork PR
if: |
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository.full_name != github.repository
run: |
echo "Refusing to run privileged job for fork PR"
exit 1
- run: ./deploy.sh
4. Script injection prevention
Untrusted values include: branch names, PR titles/bodies, issue titles/bodies, labels,
comments, commit messages, artifact contents from untrusted workflows.
Never interpolate untrusted context directly into run: blocks:
- run: echo "Branch is ${{ github.head_ref }}"
- run: echo "Branch is $BRANCH"
env:
BRANCH: ${{ github.head_ref }}
Checklist:
5. Third-party action pinning
Example:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
6. Secret hygiene
7. OIDC for cloud credentials
AWS IAM trust policy — scoped to a specific repo and environment:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub":
"repo:myorg/myrepo:environment:production",
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
⚠️ Avoid StringLike with wildcards unless you explicitly need flexibility.
repo:myorg/* trusts every repository in the org — a compromised repo becomes a
path to production credentials. Prefer exact repo:org/repo:environment:name claims.
8. Runner hardening
9. Artifacts, caches, and release workflows
Cache poisoning: Cache keys are deterministic (e.g. based on hashFiles('**/package-lock.json')),
so a fork PR can compute the same key, write a poisoned cache entry first, and have it
restored by your privileged release workflow. Mitigate by scoping cache keys to protected
branches and never restoring caches in release jobs:
- uses: actions/cache@<sha>
with:
path: ~/.npm
key: ${{ runner.os }}-${{ github.ref }}-${{ hashFiles('**/package-lock.json') }}
10. Continuous detection
Rollout order for hardening many repos
- Inventory — workflows with write permissions, secrets, release jobs, self-hosted runners, public fork triggers
- Org defaults — read-only token permissions, action restrictions, CODEOWNERS, PR approval protections
- High-risk patterns —
pull_request_target, workflow_run, untrusted interpolation, GITHUB_ENV, broad secret exposure
- Pin actions — convert mutable references to full SHAs, add Dependabot/Renovate
- OIDC migration — start with production deploy and package publish workflows
- Monitoring — workflow linting, dependency alerts, audit logs, runner registration alerts
Severity classification guide
| Severity | Examples |
|---|
| Critical | pull_request_target checking out fork head; secrets in run: args; unpinned third-party action on a release job |
| High | Script injection via ${{ github.head_ref }} in run:; write-all permissions; self-hosted runner on public repo |
| Medium | Missing permissions: block; secrets: inherit; no CODEOWNERS on workflows |
| Low | Broad artifact upload; missing persist-credentials: false; stale secrets not rotated |
Key external references