StackPractices
intermediate By Mathias Paulenko

CI/CD Pipeline Security Template

A template for securing build and deployment pipelines against credential leaks, tampering, supply chain attacks, and unauthorized deployments.

Overview

CI/CD pipelines are a juicy target: they’ve got access to your source code, your build secrets, and the road into production. A compromised pipeline can slip malware into your builds, exfiltrate data, or deploy changes nobody approved. This template defines controls to protect code integrity, runner security, secrets, and deployment approvals. For the bigger picture, see the CI/CD Security guide; for the secrets half of the problem, Docker secrets management covers the container angle.

When to Use

  • When you’re standing up a new CI/CD platform.
  • Reviewing or improving an existing pipeline.
  • When a supply chain security audit is coming up.
  • After a build system compromise or unauthorized deployment.
  • Integrating DevSecOps controls into engineering workflows.

Prerequisites

  • A version control system with branch protection and audit logging.
  • A CI/CD platform — GitHub Actions, GitLab CI, Azure DevOps, Jenkins, whatever you run.
  • A secret management solution for pipeline credentials.
  • A code review and approval step before changes merge.
  • Ownership from platform engineering, security, and release management.

Solution

Template

1. Source Control Security

ControlRequirementVerification
Branch protectionRequired reviews before merge to mainRepository settings
Signed commitsRequire verified commits for privileged accountsGit configuration
Access controlLeast-privilege access to repositoriesRBAC review
Audit loggingAll pushes, merges, and permission changes loggedPlatform logs
Dependency pinningLockfiles and pinned versions for reproducible buildsRepository files
Secret scanningAutomated detection of secrets in commitsPre-commit hooks + CI

2. Pipeline Configuration

ControlRequirementVerification
Immutable pipeline definitionsPipelines stored as code and reviewedRepository files
No secrets in codeSecrets loaded from vault, CI variables, or OIDCSecret scanning
Input validationPipeline parameters validated and sanitizedCode review
Self-hosted runner isolationProduction runners isolated from dev runnersRunner configuration
Ephemeral runnersFresh runner per build to reduce persistenceRunner settings
Pipeline provenanceSLSA provenance generated for artifactsAttestation tool

3. Secrets Management

Secret TypeStorageRotationScope
Cloud credentialsExternal vault or OIDC90 daysPer environment
Container registry tokensVault or short-lived CI tokens90 daysPer pipeline
Signing keysHardware-backed or KMS180 daysLimited service accounts
API keysVault or secret manager90 daysMinimum required permissions
Database passwordsVault dynamic secrets24 hoursPer pipeline run

4. Build Security

ControlRequirementVerification
Dependency scanningAll dependencies scanned for known CVEsScanner in CI
Static analysisSAST run on every pull requestCI job
Container image scanningBase image and layers scanned before pushRegistry scan
Reproducible buildsSame source produces same artifactBuild verification
Artifact signingAll artifacts signed with build identitySignature verification
SBOM generationBill of materials generated per buildCI output

5. Deployment Security

ControlRequirementVerification
Deployment gatesManual or automated approval before productionPipeline rules
Environment separationProduction credentials not available in devSecret scoping
Rollback planAutomated rollback trigger on failurePipeline definition
Immutable deploymentsArtifacts deployed by reference, not rebuiltDeployment logs
Drift detectionUnauthorized production changes detectedMonitoring tool
Audit trailWho deployed what, when, and whyDeployment logs

6. Incident Response

ScenarioResponseOwner
Secret leakedRotate secret, revoke tokens, audit usageSecurity team
Malicious commitRevert, investigate, revoke credentialsPlatform team
Compromised runnerTerminate runner, rebuild, review logsPlatform team
Unauthorized deploymentRollback, freeze pipeline, auditRelease manager
Tampered artifactBlock deployment, trace provenanceSecurity team

Explanation

Pipeline security is supply chain security applied to the machine that ships your code. Protect the source, the build, and the deploy path and you’ve cut off most of the ways malicious code reaches production. The template maps every control to how you verify it, so it doubles as an audit artifact.

flowchart diagram: subgraph UNTRUSTED[

GitHub Actions Security Configuration

name: Secure CI Pipeline
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read
  packages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/ci-role
          aws-region: us-east-1
          # No static keys - OIDC only

      - name: Build and sign
        uses: sigstore/cosign-installer@v3
      - run: |
          cosign sign-blob --yes artifact.tar.gz

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Scan dependencies
        uses: github/codeql-action/init@v3
      - run: npm audit --audit-level=high
      - uses: github/codeql-action/analyze@v3

SLSA Provenance Example

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "artifact.tar.gz",
      "digest": { "sha256": "abc123..." }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "builder": { "id": "github-actions" },
    "buildType": "https://github.com/actions/runner",
    "invocation": {
      "configSource": {
        "uri": "git+https://github.com/org/repo",
        "digest": { "sha1": "commit-hash" }
      }
    },
    "materials": [
      { "uri": "git+https://github.com/org/repo", "digest": { "sha1": "commit-hash" } }
    ]
  }
}

Pipeline Security Audit Checklist

ControlVerifiedNotes
No long-lived secrets in CI
OIDC for cloud auth
Actions pinned to SHA
Minimal permissions per job
Branch protection on main
Signed artifacts verified
SBOM generated per build
Dependency scan in pipeline
Runners isolated per env
Audit log retention > 90 days

Real-World Pipeline Attacks

These controls are not hypothetical hygiene: they map to incidents that already happened.

SolarWinds (2020). Attackers got into the build system itself and pushed a backdoored Orion update to thousands of organizations, signed and shipped through the official channel. It’s the canonical case for why build provenance and reproducible builds matter: the malicious artifact passed every consumer-side check because the compromise happened upstream.

Codecov (2021). A modified bash uploader script inside Codecov’s CI tooling exfiltrated environment variables, including secrets, from thousands of customer pipelines for two months. The exact control that would have caught it: pinned, hashed dependencies and alerts on unexpected outbound traffic from runners.

tj-actions/changed-files (2025). A popular GitHub Action was compromised and pushed a malicious release that leaked CI secrets in workflow logs. Teams that pinned actions to commit SHAs were immune by default. Tags moved to the malicious commit; SHAs didn’t.

PHP git server (2021). Attackers pushed backdoored commits directly to PHP’s own repository, impersonating core maintainers. The response is the interesting part: PHP dropped its self-hosted git for GitHub with mandatory 2FA. Sometimes the fix is outsourcing the trust boundary to a platform with better controls.

The pattern across all four: the pipeline is trusted by default, and that trust is exactly what attackers exploit. That’s what this template is for: it makes the trust explicit and verifiable instead of assumed.

Variants

  • GitHub Actions security checklist: Focuses on actions pinning, workflow permissions, and reusable workflows.
  • GitLab CI security template: Includes CI/CD job token scopes, protected runners, and compliance pipelines.
  • Jenkins hardening template: Covers plugin management, agent isolation, and Groovy sandboxing.
  • Container-native pipeline: Emphasizes image signing, registry scanning, and Kubernetes admission.
  • High-compliance pipeline: Adds SLSA Level 3, dual approval, and signed SBOMs for regulated environments.

What works

  • Store pipeline definitions as code and review them like application code.
  • Use short-lived credentials and OIDC instead of long-lived secrets.
  • Scan dependencies before merging and before deploying.
  • Sign artifacts and verify signatures before deployment.
  • Separate build and production environments physically or logically.
  • Require human approval for production deployments.
  • Generate and retain SBOMs for every release.
  • Watch pipeline activity for anything unusual — a build that suddenly takes twice as long or runs at 3 AM is worth a look.

Common Mistakes

  • Storing secrets in environment variables or pipeline files.
  • Using third-party actions without pinning or reviewing them.
  • Allowing any branch to deploy to production.
  • Running production and dev workloads on the same runner.
  • Skipping security scans for hotfix deployments.
  • Not rotating pipeline credentials after a compromise.
  • Trusting artifacts without signature verification.

Troubleshooting

  • A secret shows up in a build log: rotate it immediately, and assume it’s compromised the moment it prints. Then find the code path that logged it (usually an unmasked echo or an error dump) and add the value to the log masking list.
  • A dependency scan fails on a transitive CVE you can’t fix: check whether the vulnerable code path is actually reachable. If it isn’t, document the accepted risk with a review date and pin an override; if it’s reachable, bump the parent dependency or vendor a patch.
  • Provenance verification fails at deploy time: don’t deploy. A mismatched attestation means the artifact isn’t what CI built. Diff the digest against the CI artifact store and find out where the substitution happened before you ship anything.
  • An approval gate gets bypassed: check who has admin rights on the environment protection rules. Gates fail open more often from misconfigured permissions than from attackers.
  • A third-party action publishes a malicious release: if you pin to commit SHAs this can’t reach you. Tags move; SHAs don’t. If you’re on a tag, pin the known-good SHA immediately and audit what ran in the window.

See Also

Companion code: CI/CD pipeline security resources: the controls template, hardened workflow, SLSA provenance example, and audit checklist from this page.

Frequently Asked Questions

What is the biggest risk in CI/CD?

Credential theft from a runner or a pipeline file — once an attacker has your deploy keys they don't need to touch your application at all; they just ship their own code as if it were yours.

How do we balance security with fast deployments?

Automate the checks and keep them fast, then reserve human approval for production only. Shift-left scanning gives developers feedback in minutes, which is what makes the security gate something they will actually keep.

What is SLSA provenance?

SLSA is the framework everyone means when they talk supply chain security. Provenance records how an artifact was built: source repo, build command, dependencies. A tampered or rebuilt artifact doesn't match its own paper trail.

How do we secure secrets in CI/CD pipelines?

Stick them in a dedicated secrets manager: HashiCorp Vault, AWS Secrets Manager, or GitHub Actions secrets. Never hardcode them in pipeline files or env vars, and prefer OIDC for cloud auth over static keys. Rotate quarterly and after any suspected compromise. On self-hosted runners, make sure secrets get scrubbed from logs and the runner itself is ephemeral.

What is the difference between SAST, DAST, and SCA?

SAST analyzes source code for vulnerabilities without running it, DAST pokes a running application from the outside, and SCA scans your dependencies for known CVEs. You want all three in the pipeline: SAST on every PR, SCA on every merge, DAST against staging.

Should we use self-hosted or cloud-managed runners?

Cloud-managed runners (GitHub-hosted, GitLab SaaS) are ephemeral and isolated by default, which shrinks the attack surface for free. Self-hosted runners earn their keep for private network access or specialized hardware, but they need hardening: ephemeral instances, no shared state between jobs, and network segmentation between build and prod. And never run production deploys on the same runner as untrusted PR builds.

How do we implement dual approval for production deployments?

Have two different team members approve manually before anything hits production. Environment protection rules in GitHub Actions or protected environments in GitLab both do this. The approvers shouldn't be the person who triggered the run. Log every approval with timestamp, user, and reason for the audit trail.

What is an SBOM and why do we need it?

An SBOM (Software Bill of Materials) is a machine-readable inventory of all components in a software artifact, including transitive dependencies, versions, and licenses — so you can scan for vulnerabilities, check licenses, and actually see what's in your supply chain. Generate one per build with syft, trivy, or GitHub's dependency graph, keep it next to the artifact, and retain it for as long as that software is deployed.

What is cosign and how does it work?

Cosign, from the Sigstore project, signs and verifies container images and blobs. Its keyless mode uses the OIDC tokens your CI provider already issues, so there's no signing key to manage or leak. Signatures land in the Rekor transparency log, which makes them publicly verifiable. Sign artifacts after build and verify before deploy.

How do we handle secrets for self-hosted runners?

Run ephemeral runners that die after each job and inject secrets at runtime from your secrets manager — never leave them on the runner disk. Mask secret values out of pipeline logs, rotate runner credentials regularly, and audit who touched the runner. For sensitive environments, give each one its own runner pool behind network isolation.