Back to articles
devsecops
11 min readMarch 1, 2026

Securing GitHub Actions: Your Pipeline Is an Attack Surface

SolarWinds proved that compromising a build pipeline is game over. Your GitHub Actions workflows have write access to production — are you treating them like it?

Securing GitHub Actions: Your Pipeline Is an Attack Surface

Securing GitHub Actions: Your Pipeline Is an Attack Surface

Here's a thought experiment: list everything your CI/CD pipeline has access to. If you're like most teams, that list includes your source code (write access), your production deployment credentials, your npm/Docker registry tokens, your database connection strings (for migrations), and possibly your cloud provider's IAM credentials. Now ask yourself: how much scrutiny do you give to the third-party GitHub Actions running in that pipeline?

If the answer is "not much," you're in good company. And you're also running one of the most attractive attack surfaces in modern software.

Why This Matters: Real Supply Chain Attacks

This isn't theoretical. Supply chain attacks via CI/CD have caused some of the most damaging security incidents in recent years:

  • SolarWinds (2020): Attackers compromised the build pipeline and injected malware into software updates that shipped to 18,000+ organizations, including US government agencies. The attack went undetected for months.
  • Codecov (2021): Attackers modified Codecov's Bash Uploader script — a CI tool used by thousands of companies — to exfiltrate environment variables (including secrets) from CI environments. This affected companies like Twitch, Hashicorp, and others.
  • npm package attacks: Packages like
    event-stream
    and
    ua-parser-js
    were compromised to exfiltrate environment variables during install scripts that run in CI.

The common thread: CI/CD pipelines are high-value targets because they have broad access and run code from external sources with minimal verification.

1. Pin Action Versions to Commit SHA

This is the single highest-impact change you can make, and almost nobody does it.

yaml
# ❌ DANGEROUS — tag can be moved to point to malicious code
uses: actions/checkout@v4
uses: actions/setup-node@v4

# ✅ SAFE — pin to exact commit SHA
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0

When you reference

actions/checkout@v4
, you're trusting that the
v4
tag will always point to safe code. But git tags can be moved. If an attacker compromises the action's repository (or a maintainer's account), they can point the
v4
tag to malicious code, and every workflow referencing that tag will start running the attacker's code. This isn't hypothetical — it's happened.

Pinning to a commit SHA means you're referencing an immutable snapshot. Even if the repository is compromised, your pipeline runs the exact code you reviewed.

Gotcha: SHA pinning means you won't automatically get security updates to the action. You need to periodically update the SHAs — tools like Dependabot and Renovate can automate this by opening PRs when new versions are available. It's a trade-off between supply chain security and update convenience, and for CI/CD, I'll take security every time.

2. Use Minimal Permissions

GitHub Actions workflows run with a

GITHUB_TOKEN
that, by default, has broad permissions. You should restrict this to the minimum needed.

yaml
# At the workflow level — start with read-only
permissions: read-all

jobs:
  build:
    permissions:
      contents: read      # Only read code
      packages: write     # Only what this job needs
      # NOT included: actions:write, secrets:write, admin:write

Each job should declare only the permissions it actually needs. A build job that runs tests doesn't need write access to packages. A deploy job doesn't need write access to issues. The

permissions
key is your way of implementing least privilege at the workflow level.

Gotcha: If you set

permissions: read-all
at the workflow level, individual jobs inherit those restricted permissions and can only further restrict, not expand. But if you don't set workflow-level permissions at all, jobs get the default permissions from your repository settings, which are often much broader than needed. Always set explicit permissions.

3. Scan Dependencies in the Pipeline

Your pipeline should catch known vulnerabilities before they reach production. This complements (but doesn't replace) the dependency management practices from our secret management guide.

yaml
- name: Audit dependencies for known vulnerabilities
  run: npm audit --audit-level=high

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    security-checks: 'vuln,secret'
    severity: 'CRITICAL,HIGH'

npm audit
catches known vulnerabilities in your dependency tree. Trivy goes further — it scans for embedded secrets, misconfigured IaC files, and vulnerabilities in Docker images.

The "it depends" part: Should you fail the build on every vulnerability? In an ideal world, yes. In practice, many npm audit findings are in dev dependencies or have no practical exploit path. I typically set

--audit-level=high
in CI (fail on high and critical) and review medium/low findings weekly rather than blocking every PR. If you're handling payment data or health records, be stricter.

4. Never Echo or Log Secrets

GitHub Actions masks secrets in logs (replacing them with

***
), but this masking isn't perfect. Multi-line secrets, base64-encoded secrets, or secrets that get transformed (URL-encoded, for example) can slip through.

yaml
# ❌ LEAKS the secret to logs
- run: echo "Deploying with key $API_KEY"

# ❌ ALSO DANGEROUS — curl output might include the key in error messages
- run: curl -v -H "Authorization: Bearer $API_KEY" https://api.example.com

# ✅ Use secrets in commands without logging them
- run: curl -s -H "Authorization: Bearer $API_KEY" https://api.example.com
  env:
    API_KEY: ${{ secrets.API_KEY }}

Gotcha that I've seen in the wild:

set -x
(or
set -o xtrace
) in shell scripts prints every command before executing it, including the interpolated secret values. GitHub's masking catches some of these, but not always. Never use
set -x
in CI scripts that handle secrets. Debugging a workflow? Add targeted
echo
statements, never enable trace mode globally.

5. Restrict Third-Party Actions

Not all GitHub Actions are created equal. Some are maintained by GitHub, some by verified organizations, and some by random developers. You should limit which actions can run in your organization.

yaml
# In repo Settings → Actions → General → Actions permissions:
# "Allow actions created by GitHub" + "Allow actions by Marketplace verified creators"
# OR maintain an explicit allowlist

For organizations, GitHub allows you to restrict actions at the organization level. Use this. It's far easier to maintain an allowlist of approved actions than to audit every workflow in every repository.

Before adding any third-party action to your allowlist, check:

  • Is it maintained by a known organization or individual?
  • Does it have a significant user base?
  • When was it last updated?
  • Does its code do what it claims? (Yes, actually read the action.yml and the scripts it runs.)

6. Protect Your Workflow Files

If an attacker can modify your workflow files, all the other protections are moot. Use branch protection rules on your main branch:

  • Require PR reviews for changes to
    .github/workflows/
  • Require status checks to pass
  • Don't allow bypassing these rules, even for admins

Consider using GitHub's CODEOWNERS file to require security team review for any changes to workflow files:

# .github/CODEOWNERS
.github/workflows/ @your-org/security-team

The Uncomfortable Truth About CI/CD Security

Most teams treat their CI/CD pipeline as a devops concern, not a security concern. The pipeline has access to everything — code, secrets, production infrastructure — but it's often the least audited part of the stack. A 30-minute review of your GitHub Actions workflows — checking for unpinned actions, excessive permissions, leaked secrets in logs, and unrestricted third-party actions — is one of the highest-ROI security activities you can do.

Do the audit. Pin the actions. Restrict the permissions. It's not exciting work, but the alternative is explaining to your CEO how an npm package maintainer in another country got access to your production database credentials.

Discussion

0 comments

Share your thoughts

No comments yet. Be the first to share your thoughts!