GitHub Actions security hardening, configuration best practices, and vulnerability detection. Covers workflow syntax, trigger security, permission management, secrets handling, OIDC federation, supply chain protection, self-hosted runner hardening, attack pattern recognition, and security scanning tool rules. 60% security/hardening content, 40% implementation/configuration guidance. Use this skill when users need to: (1) Harden GitHub Actions workflows against injection, supply chain, or privilege escalation attacks (2) Configure workflow permissions, secrets, OIDC, or environment protection rules securely (3) Understand dangerous workflow patterns (pull_request_target + checkout, workflow_run artifact poisoning, script injection via ${{ }}) (4) Choose or configure security scanning tools (zizmor, scorecard, actionlint, poutine, harden-runner, Raven) (5) Respond to supply chain incidents (tj-actions, reviewdog, compromised action tags) (6) Audit workflows for OWASP CI/CD risks, CIS benchmark compliance, or OpenSSF Scorecard checks (7) Write or review workflow YAML (triggers, matrix, reusable workflows, composite actions, caching, artifacts, environments) (8) Secure self-hosted runners (ephemeral patterns, network egress, persistence detection, runner groups)
Resources
1Install
npx skillscat add swannysec/robot-tools/gha-hardening Install via the SkillsCat registry.
GitHub Actions Hardening
Security hardening and configuration best practices for GitHub Actions.
Help — Topic Navigator
Use this table to find the right reference file. Load references only when
needed for the user's specific question.
| Topic | Reference File | Covers |
|---|---|---|
| Workflow syntax & config | references/workflow-configuration.md |
Triggers, matrix, environments, reusable workflows, composite actions, caching, artifacts, runners |
| Security hardening | references/security-hardening.md |
Permissions, secrets, OIDC, GITHUB_TOKEN, push protection, governance policies |
| Attack patterns | references/attack-patterns.md |
Injection, pwn requests, workflow_run escalation, GITHUB_ENV, artifact poisoning, repo jacking |
| Supply chain security | references/supply-chain-security.md |
SHA pinning, tj-actions incident, typosquatting, Dependabot, compromised action detection |
| Runner security | references/runner-security.md |
Self-hosted threats, ephemeral patterns, ARC, persistence techniques, runner groups |
| Detection tools | references/detection-tools.md |
Zizmor 30+ rules, Scorecard 18 checks, Poutine, Actionlint, Harden-Runner, Snyk scanner, Raven |
| Incident reference | references/incident-reference.md |
CVE database, tj-actions timeline, SecLab advisories, CISA alerts, real-world exploits |
Top 10 Security Rules
These are the highest-priority hardening measures. Apply them to every
GitHub Actions workflow.
1. Never interpolate untrusted input into run: blocks
Route all github.event.*, github.head_ref, and user-supplied values
through env: variables — never use ${{ }} directly in run: steps.
# DANGEROUS
- run: echo "${{ github.event.pull_request.title }}"
# SAFE
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"Dangerous fields include anything ending in: body, title, ref, name,message, email, label, page_name.
2. Pin all third-party actions to full commit SHAs
Tags are mutable — the tj-actions/changed-files attack (CVE-2025-30066)
compromised 23,000+ repos by redirecting version tags.
# VULNERABLE — tag can be silently redirected
- uses: actions/checkout@v4
# SECURE — immutable SHA with version comment
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Use Dependabot (package-ecosystem: github-actions) to keep SHA pins current.
3. Set explicit minimal permissions:
Always declare permissions: at the workflow level. When present, all
unspecified scopes default to none.
permissions:
contents: read
jobs:
deploy:
permissions:
contents: read
id-token: write # only when OIDC is neededRepos created before Feb 2, 2023 may still default to read/write — audit them.
4. Use OIDC instead of long-lived cloud secrets
OIDC tokens are ephemeral — no secret to rotate, leak, or exfiltrate.
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@<SHA>
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubRole
aws-region: us-east-1Scope trust policies using sub (with environment) + aud claims.
5. Never use self-hosted runners on public repositories
Any user can open a PR and trigger code execution on internal infrastructure.
Use ephemeral (JIT) runners, restrict egress, and isolate by runner group.
6. Never combine pull_request_target with code checkout
pull_request_target runs with secrets and write access — checking out
the PR head gives attacker code full privileged execution.
# DANGEROUS — RCE with secrets
on: pull_request_target
steps:
- uses: actions/checkout@<SHA>
with:
ref: ${{ github.event.pull_request.head.ref }} # attacker code
# SAFE — no checkout, just label
on: pull_request_target
permissions:
pull-requests: write
contents: none
steps:
- uses: actions/labeler@<SHA>7. Treat all workflow_run artifacts as untrusted
Artifacts from fork PRs cross a trust boundary. Never execute artifact
content — extract to /tmp, validate as structured data only.
8. Scope environment secrets with required reviewers
Use named environments (dev/staging/prod) with required reviewer approval
for deployment credentials. This provides a human gate between merge and
deploy.
9. Evaluate third-party actions before adoption
Check for: verified creator badge, source audit, unpinnable dynamic deps,
maintenance status. Enforce org-level allowed actions policy.
10. Use ::add-mask:: for dynamic secrets
For runtime-generated tokens not stored as GitHub Secrets:
- run: |
TOKEN=$(generate-token)
echo "::add-mask::$TOKEN"
echo "TOKEN=$TOKEN" >> "$GITHUB_OUTPUT"Call add-mask before any step that could log the value.
Dangerous Patterns — Quick Reference
Script Injection via ${{ }}
Trigger: Any run: step interpolating attacker-controlled context.
Fix: Use env: mapping. See Rule 1 above.
Pwn Request (pull_request_target + checkout)
Trigger: pull_request_target trigger + actions/checkout withref: pointing to PR head.
Fix: Use pull_request (read-only for forks) for code execution.
Use pull_request_target only for label/comment ops without checkout.
workflow_run Privilege Escalation
Trigger: Privileged workflow_run downloads and executes artifacts
from an unprivileged pull_request workflow.
Fix: Extract artifacts to isolated paths. Only read structured data
(PR numbers, coverage scores). Never execute artifact content.
GITHUB_ENV / GITHUB_PATH Injection
Trigger: Attacker-controlled content written to $GITHUB_ENV or$GITHUB_PATH (e.g., from artifacts or untrusted input).
Fix: Never write untrusted data to environment files. Validate all
input before appending to $GITHUB_ENV.
Repo Jacking
Trigger: uses: references an org/user that no longer exists.
Attacker registers the namespace and publishes a malicious action.
Fix: SHA-pin all actions. Audit for references to renamed/deleted orgs.
Typosquatting
Trigger: Misspelled action owner (e.g., actons/checkout instead ofactions/checkout).
Fix: Maintain an org-level allowlist of permitted actions. Double-check
owner names. SHA pinning catches mismatches.
Impostor Commits
Trigger: SHA-pinned action references a commit from a fork network,
not the actual repository.
Fix: Verify SHAs correspond to tagged releases. Use zizmor'simpostor-commit audit rule.
Cache Poisoning
Trigger: Fork PR injects malicious content into GitHub Actions cache.
Privileged downstream workflow restores the poisoned cache.
Fix: Scope caches carefully. Do not restore caches in release
workflows from branches that accept fork PRs.
Workflow Security Checklist
Use this before merging any workflow change:
-
permissions:declared explicitly (workflow or job level) - All third-party actions pinned to full commit SHA
- No
${{ }}interpolation of untrusted input inrun:steps - No
pull_request_target+ checkout of PR head -
workflow_runartifacts treated as untrusted data - Self-hosted runners not exposed to public repos
- OIDC used instead of long-lived cloud credentials where possible
- Environment secrets gated by required reviewers for production
-
persist-credentials: falseonactions/checkoutin sensitive workflows - Dependabot configured for
github-actionsecosystem
Security Scanner Quick Comparison
| Tool | Type | Primary Strength |
|---|---|---|
| zizmor | Static | Broadest rule set (30+ rules): injection, impostor commits, cache poisoning, permissions |
| Scorecard | Static | Project-level security posture (18 checks): Dangerous-Workflow, Token-Permissions, Pinned-Dependencies |
| Poutine | Static | Cross-platform CI/CD (GitHub, GitLab, Azure); "Living Off The Pipeline" detection |
| Actionlint | Static | Workflow YAML validation + untrusted input detection |
| Harden-Runner | Runtime | Network egress monitoring, file integrity, anomaly detection (caught tj-actions attack) |
| Raven | Graph | Multi-step cross-workflow vulnerability detection via Neo4j |
| Snyk Scanner | Static | Regex-based rule engine; includes LD_PRELOAD PoC generator |
For complete rule references, load references/detection-tools.md.