Securing Your CI/CD Pipeline: A Practical Guide
A CI/CD pipeline is, by design, a system that runs code you didn't write (dependencies, third- party actions) with credentials that can push to production. That combination makes it one of the highest-leverage places to get security fundamentals right - and one of the most commonly ignored.
Why CI/CD Is a Real Target
- Pipelines routinely hold deploy credentials, cloud provider keys, and package-registry publish tokens - compromise one workflow and you may have compromised production.
- Every dependency your build installs, and every third-party action your workflow calls, runs with the pipeline's own permissions - not sandboxed.
- Pipelines are automated and often under-monitored compared to production systems, so a compromise can persist unnoticed for longer.
Secrets Management
- Never hardcode a credential in a workflow file or commit it "temporarily" - git history is forever, even after a later commit removes it.
- Use your CI provider's secret store (GitHub Actions secrets, GitLab CI/CD variables) rather than
.envfiles checked into the repo. - Scope secrets to the narrowest environment that needs them - a secret available to every branch and every PR is a secret available to anyone who can open a PR.
- Rotate credentials on a schedule, and immediately if a workflow that had access to them is ever modified by an untrusted contributor.
Dependency and Supply Chain Risk
- Pin third-party GitHub Actions to a full commit SHA, not a mutable tag like
@v1- a tag can be moved to point at different, potentially malicious code without your workflow file changing at all. - Treat your build's own dependencies the same way - a compromised transitive dependency runs with full build permissions during
npm install, not just at runtime in production. - Be deliberate about which third-party actions you use - prefer actions maintained by the platform itself or a well-known vendor over a random low-star repository.
Least-Privilege Permissions
The single highest-leverage change most repos can make: default every workflow's token to read-only, and grant write access explicitly only to the specific job that needs it.
1# .github/workflows/ci.yml
2name: CI
3
4on: [pull_request]
5
6# Default every workflow to read-only. Grant write access only to the
7# specific job that actually needs it, not the whole workflow.
8permissions:
9 contents: read
10
11jobs:
12 test:
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@v4
16 - run: npm ci
17 - run: npm testThe other classic mistake: workflows triggered by pull_request_target or workflow_runthat check out and execute code from a fork with write-level permissions. A malicious PR from an external contributor can then run arbitrary code with your repo's own credentials. If a workflow needs elevated permissions, make sure it's not also running untrusted, attacker-controlled code in the same job.
A Practical Hardening Checklist
- Workflow permissions default to read-only, elevated only where explicitly needed
- Third-party actions pinned to a commit SHA, not a mutable tag
- Secrets scoped per-environment, not available to every branch and PR by default
- No workflow runs untrusted fork code with write-level permissions
- Dependencies scanned for known vulnerabilities on every PR, not just periodically
- Credentials rotated on a schedule and immediately after any workflow-file change from an untrusted contributor
Automate this in your next PR
Run a free repo scan and see outdated dependencies, CVEs, and code quality issues before they ship.
Run a free repo scan