Hardening GitHub Actions: Blue Team Defense Guide
Overview & Defensive Context#
In this morning's offensive breakdown, we examined how adversaries exploit GitHub Actions pull_request_target workflows to execute Poisoned Pipeline Execution (CICD-SEC-4). By checking out code from an untrusted fork (ref: ${{ github.event.pull_request.head.sha }}) or evaluating unvalidated pull request expressions inside inline shell steps, external contributors can execute arbitrary code inside privileged runner environments, harvest repository secrets, and mint unauthorized cloud OIDC credentials.
Traditional perimeter network controls and static application security testing (SAST) tools miss these vulnerabilities because the security flaw resides in the CI/CD orchestration layer itself. Workflows execute entirely within GitHub-hosted or self-hosted runner infrastructure, where pipeline execution is often mistakenly trusted as internal developer activity.
[!IMPORTANT] Defending GitHub Actions pipelines requires enforcing strict separation between untrusted code execution and privileged token access, applying explicit workflow permission constraints, eliminating inline expression interpolation, and deploying automated static workflow linters such as
actionlintandzizmor.
Architecture Hardening: The Two-Workflow Security Architecture#
To securely process community pull requests that require downstream privileged actions (such as automated label assignment, code quality reporting, or deployment approvals), security teams must implement the Two-Workflow Security Pattern.
flowchart TD
subgraph UntrustedZone [Untrusted Fork Execution Boundary]
ForkPR["Untrusted Fork Pull Request"]
PRWorkflow["Workflow 1: on: pull_request"]
IsolatedRunner["Ephemeral Runner (permissions: read-all, ZERO SECRETS)"]
ArtifactBuild["Build and Generate Raw Test Results Artifact"]
end
subgraph SecurityBoundary [Artifact Transfer Gate]
ArtifactUpload["Upload Test Artifact to GitHub Storage"]
end
subgraph TrustedZone [Trusted Base Repository Context]
WorkflowRunTrigger["Workflow 2: on: workflow_run (Target: Workflow 1)"]
BaseRunner["Privileged Runner (Base Repo Context with Secrets)"]
ArtifactDownload["Download and Validate Schema of Artifact"]
PostResults["Post Label / PR Comment / Security Telemetry"]
end
ForkPR --> PRWorkflow
PRWorkflow --> IsolatedRunner
IsolatedRunner --> ArtifactBuild
ArtifactBuild --> ArtifactUpload
ArtifactUpload --> WorkflowRunTrigger
WorkflowRunTrigger --> BaseRunner
BaseRunner --> ArtifactDownload
ArtifactDownload --> PostResultsIn this architecture:
- Workflow 1 (
pull_request): Executes entirely in an untrusted sandbox withread-allpermissions and zero access to repository secrets. It compiles and tests the fork code, outputting non-executable data (such as coverage reports or JSON summaries). - Workflow 2 (
workflow_run): Triggers only upon the successful completion of Workflow 1, running exclusively in the base repository context. It consumes the isolated data artifact without executing untrusted scripts.
Hardened Policy & Workflow Implementations#
1. Hardened Workflow Configuration#
The workflow below demonstrates secure metadata handling, root-level permission restrictions, and environment variable binding to eliminate expression injection:
name: Secure PR Triage
on:
pull_request_target:
types: [opened, synchronize]
## Explicitly strip all permissions by default
permissions: {}
jobs:
triage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # Minimal scoped permission for PR labeling
steps:
- name: Checkout Base Repository Only
uses: actions/checkout@v4
with:
persist-credentials: false # Do not leave GITHUB_TOKEN on disk
- name: Inspect PR Title Securely
env:
# Bind untrusted context to an environment variable
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
# Use the environment variable; never interpolate directly into shell
echo "Analyzing pull request: ${PR_TITLE}"
if [[ "${PR_TITLE}" =~ ^(feat|fix|docs): ]]; then
echo "Format valid."
fi
[!TIP] Setting
persist-credentials: falseinactions/checkoutprevents subsequent compromised build steps or malicious dependencies from extracting the repositoryGITHUB_TOKENfrom the.git/configcredential store.
Production Detection Queries#
Security teams must audit repository workflow definitions across the organization to catch insecure checkout references and missing permission blocks.
1. Production Sigma Rule: Insecure pull_request_target Checkout#
The following Sigma rule detects commit activity or pull requests introducing insecure checkout references within pull_request_target workflows:
title: Insecure GitHub Actions pull_request_target Fork Checkout
id: 4b2c1d8e-9f3a-4e5c-8b1a-7c6d5e4f3b12
status: production
description: Detects workflow definitions combining pull_request_target with dangerous ref checkouts of pull request head commits.
references:
- https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
- https://owasp.org/www-project-top-10-ci-cd-security-risks/
logsource:
category: file_change
product: git
detection:
selection_path:
file_path|contains: '.github/workflows/'
file_path|endswith: '.yml'
selection_trigger:
file_content|contains: 'pull_request_target'
selection_dangerous_ref:
file_content|contains:
- 'github.event.pull_request.head.sha'
- 'github.event.pull_request.head.ref'
condition: selection_path and selection_trigger and selection_dangerous_ref
falsepositives:
- Rare read-only documentation linting workflows that strictly isolate dependencies
level: critical
tags:
- attack.initial_access
- attack.execution
- attack.t1195.002
Enterprise Mitigation Matrix#
Securing GitHub Actions workflows across enterprise organizations requires combining static analysis, runtime constraints, and repository branch protection policies:
| Defense Control | Technical Mechanism | Operational Blast Radius | Performance Impact | Security Guarantee |
|---|---|---|---|---|
| Two-Workflow Pattern | Decouple build (pull_request) from privileged processing (workflow_run) |
Moderate (requires splitting complex pipelines into two files) | Minimal (< 30s queue latency) | Completely prevents untrusted fork code from accessing repository secrets |
| Environment Variable Binding | Map ${{ github.event.* }}intoenv: blocks instead of inline script interpolation |
Low (straightforward refactoring of shell steps) | Zero | Prevents shell metacharacter injection and arbitrary command execution |
| actionlint & zizmor CI Gates | Automated static analysis linters running in pre-commit and PR review checks | Low (instant feedback on workflow pull requests) | < 5s per commit | Flags insecure triggers, untyped permissions, and injection patterns before merge |
| OIDC Subject Claim Pinning | Enforce job_workflow_ref condition keys in cloud IAM trust policies |
Moderate (requires configuring AWS/GCP IAM role trust conditions) | Zero runtime overhead | Ensures cloud roles can only be assumed by designated base workflow files |
Incident Response & Verification Playbook#
When an alert flags unauthorized command execution or secret exfiltration on a GitHub Actions runner, execute this triage and containment playbook:
Phase 1: Rapid Containment & Token Revocation#
- Cancel Active and Queued Workflow Runs: Immediately terminate the compromised workflow execution via the GitHub CLI:
gh run cancel <RUN_ID>
- Revoke and Rotate Exposed Repository Secrets: If the runner had access to cloud credentials, rotate affected IAM role trust keys or API secrets immediately:
# Audit secrets accessible to the repository
gh secret list --repo owner/repo
- Invalidate Active Cloud Sessions:
If AWS OIDC was assumed, invalidate all active sessions for the assumed role using an inline
aws:TokenIssueTimedenial policy.
Phase 2: Workflow Forensics & Linter Audit#
- Inspect Raw Runner Execution Logs: Download the execution logs to verify which commands were spawned during the run:
gh run view <RUN_ID> --log > run_execution.log
grep -iE "curl|wget|bash|sh|nc|env" run_execution.log
- Scan All Workflows with zizmor: Run static security analysis across the entire repository to identify lingering injection vectors:
zizmor .github/workflows/
[!CAUTION] GitHub-hosted runners are ephemeral and destroyed upon job completion. Security teams must rely on downloaded workflow execution logs and cloud provider control-plane audit trails for forensic artifacts.
Comments
Post a Comment