Your development pipeline is a privileged system. It has access to source code, deployment credentials, cloud provider keys, and production infrastructure. If an attacker compromises your CI/CD pipeline, they don’t need to find a vulnerability in your application — they can inject one directly, sign it with your certificate, and deploy it to your production environment through your own automation. The SolarWinds attack wasn’t a software vulnerability. It was a pipeline compromise. And most pipelines are far less secured than SolarWinds’ was.
This guide covers the specific, actionable steps to secure your development workflow — from branch protection to deployment. Every item is something you can implement this week. Most are free.
DO / DON’T
DO:
- Enable branch protection on main/production branches
- Require at least one code review approval before merge
- Add SAST scanning to every pull request
- Run dependency audits on every build
- Use a secrets manager instead of environment variables
- Sign your commits with GPG or SSH keys
- Scan for secrets in pre-commit hooks and CI
DON’T:
- Allow direct pushes to main — ever
- Skip security checks for “emergency” deploys without a documented exception process
- Store secrets in source code, environment variables, or CI configuration files
- Give CI/CD service accounts more permissions than they need
- Ignore SAST findings because “we’ll fix them later”
- Use the same credentials across development, staging, and production
- Leave deprecated CI/CD workflows or old pipeline configurations active
Branch Protection
Branch protection is the first line of defense. Without it, any developer (or any compromised developer account) can push directly to main and trigger a production deployment.
What to Enable
- Require pull request reviews. At least one approval before merge. For security-critical repositories, require two reviewers — one of whom should have security awareness. GitHub, GitLab, and Bitbucket all support this natively.
- Require status checks. SAST, SCA, linting, and tests must pass before the merge button is available. If the security scan fails, the merge is blocked. This is the gate that actually enforces security — not a policy document, but a technical control.
- Require signed commits. Commit signing proves the commit came from the claimed author. GPG signing (
git config commit.gpgsign true) or SSH signing (Git 2.34+) both work. Without signing, an attacker who compromises a developer’s credentials can push commits as that developer with no cryptographic verification. - Disable force pushes. Force pushes to protected branches can overwrite history, remove security fixes, or inject malicious commits that replace legitimate ones. Block them unconditionally.
- Require linear history. Enforce squash or rebase merges to maintain a clean, auditable commit history. Every merge to main should be a clear, reviewable unit of change.
GitHub’s branch protection documentation covers the configuration. GitLab uses “protected branches” with similar options. Enable everything. Loosen later only with documented justification.
Code Review with Security Focus
Code reviews catch what automated tools miss: business logic flaws, authorization bypasses, insecure design patterns, and assumptions about trust boundaries.
Security Review Checklist
Every reviewer should ask:
- Authentication: Does this code properly verify identity? Are there endpoints without authentication?
- Authorization: Does this code check that the authenticated identity is authorized for this specific action on this specific resource? Watch for BOLA/IDOR patterns.
- Input validation: Is all input validated on the server side? Watch for string concatenation in queries, unsanitized user input in HTML output, and command injection in system calls.
- Error handling: Do error messages reveal internal details (stack traces, database errors, internal paths)? Does the code fail safely — defaulting to deny rather than allow?
- Cryptography: Are modern algorithms used? Are secrets hardcoded? Are random values cryptographically secure?
- Logging: Are security-relevant events (auth failures, authorization failures, admin actions) logged? Are sensitive values (passwords, tokens, PII) excluded from logs?
Automate what you can. Enforce the rest through reviewer training and review checklists. The OWASP Code Review Guide provides detailed guidance by vulnerability class.
SAST in Your Pipeline
Static Application Security Testing scans source code for vulnerability patterns. Add it to every pull request so findings appear before code merges.
Quick Setup
Semgrep (free, open-source, fast):
# GitHub Actions example
- name: Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: "p/default p/owasp-top-ten p/security-audit"
CodeQL (free for public repos, GitHub-native):
# GitHub Actions — enable in repository Settings > Code security
- name: CodeQL Analysis
uses: github/codeql-action/analyze@v3
Configure findings to appear as PR annotations — inline comments on the specific lines with the issue. Developers fix what they see in their workflow. They ignore what lives in a separate dashboard.
Start by blocking on critical and high findings. Warn on medium. Revisit the thresholds monthly as false positives are tuned out.
Dependency Scanning
Your application is mostly other people’s code. Scan it.
Automated Dependency Updates
- Dependabot (GitHub-native) — Generates PRs for vulnerable dependencies automatically. Enable it in your repository settings under “Code security and analysis.”
- Renovate — More configurable. Supports grouping updates, scheduling, and auto-merge for minor/patch updates with passing tests.
Manual Audits
Run these regularly and in CI:
# Node.js
npm audit
# Python
pip audit
# Ruby
bundle audit
# Go
govulncheck ./...
# Rust
cargo audit
Block deployments when critical vulnerabilities with known exploits are found. Check against the CISA KEV catalog — if your dependency is on that list, it’s being actively exploited right now.
Secrets Management
Secrets in code are the most preventable and most common pipeline security failure.
Prevention
Pre-commit hooks catch secrets before they enter the repository:
# Install gitleaks as a pre-commit hook
brew install gitleaks # or download from GitHub releases
gitleaks protect --staged
CI scanning catches what hooks miss (developers can skip hooks):
# GitHub Actions
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
Remediation
If a secret reaches the repository, rotating it is more important than removing it from history. Git history is distributed — the secret is already on every developer’s machine, in every fork, and potentially in cached copies. Rotate the secret immediately, then clean the history with git filter-repo or BFG Repo-Cleaner.
Proper Secret Storage
Use a vault. Not environment variables. Not config files. A vault.
- HashiCorp Vault — Self-hosted or managed. Dynamic secrets, automatic rotation, detailed audit logging.
- AWS Secrets Manager / GCP Secret Manager / Azure Key Vault — Cloud-native options with IAM-based access control.
- GitHub Actions Secrets — For CI/CD secrets. Encrypted at rest, masked in logs, scoped to specific repositories or environments.
Every secret should have an owner, an expiration, and a rotation schedule. NIST SP 800-57 provides key management lifecycle guidance.
CI/CD Infrastructure Hardening
Your CI/CD system is a production system. Treat it like one.
- Least privilege for service accounts. The CI/CD service account should have only the permissions needed for deployment — not admin access to the entire cloud environment.
- Separate credentials per environment. Development, staging, and production should use different credentials. A compromised staging token shouldn’t unlock production.
- Ephemeral build environments. Use fresh build agents for each run. Persistent agents accumulate cached credentials, stale configurations, and potential compromise.
- Audit CI/CD changes. Log every pipeline modification, every secret access, and every deployment. Alert on unexpected changes — a new pipeline step, a modified deployment target, or an unusual deployment time.
- Pin your CI/CD actions. Reference actions by SHA hash, not by tag. Tags can be moved; SHA hashes cannot.
uses: actions/checkout@v4can silently change.uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11cannot.
If It Already Happened
If secrets have been leaked in your repository, rotate them immediately — before cleaning the git history. The secret is compromised the moment it’s pushed, regardless of how quickly you remove it.
If your pipeline has been compromised, treat it as a full security incident. Audit every deployment made by the compromised pipeline. Check for unauthorized code changes, unexpected dependency additions, and modified build scripts. Review CI/CD logs for unusual activity. Rotate all secrets that the pipeline had access to. Rebuild your pipeline infrastructure from known-good configurations.
If malicious code was deployed to production through the pipeline, initiate your incident response process. Identify the affected systems and data. Notify stakeholders per your incident response plan. CISA accepts voluntary incident reports and can provide assistance.
Your pipeline deploys your code, with your credentials, to your production environment. Every control in this guide protects that chain of trust. Start with branch protection and a SAST scanner in CI. Add dependency scanning and secrets detection. Harden the CI/CD infrastructure. Each step closes an attack vector that the usual suspects are actively exploiting. The pipeline is the path to production — lock it down.