Code2Deploy

DevSecOps in Practice: Building a Secure CI/CD Pipeline from Scratch

Code2Deploy5 min read

Most teams that say they’re “doing DevSecOps” actually mean they run one container scan before deploy and call it a day. That’s better than nothing, but it’s not the practice — it’s one stage of it. Real DevSecOps means security checks run at every stage of the pipeline, cheap checks first, and a failing check blocks the merge the same way a failing unit test does.

This is a practical breakdown of what actually goes in a secure pipeline, why each stage exists, and a working example you can adapt.

Shift Left, Concretely

“Shift left” gets thrown around a lot. Concretely, it means: the earlier a class of vulnerability can be caught, the earlier in the pipeline you should catch it, because the cost of fixing it grows the further right it travels.

Stage What breaks if you skip it
Pre-commit / IDE Secrets and obvious bugs reach the remote repo
Pull request (SAST, SCA, secrets scan) Vulnerable code and dependencies reach main
Build (image scanning, SBOM) Vulnerable base images and packages reach a registry
Pre-deploy (policy as code) Misconfigured resources reach a live cluster
Runtime (Falco, audit logging) Active exploitation goes undetected

A mature pipeline has a control at every one of these stages. None of them are optional if you’re actually doing this and not just saying you are.

The Stages, One at a Time

1. Secrets Scanning

Catches API keys, private keys, and credentials before they ever reach a shared branch — including ones buried in commit history, which is the most common way secrets leak even from teams that are “careful.”

  • Gitleaks and TruffleHog are the two standard open-source options; both scan diffs on every push and full history on demand.
  • Run this as a pre-commit hook and in CI. The pre-commit hook stops the leak locally; CI is the backstop for the engineer who skipped their hooks.

2. Static Application Security Testing (SAST)

Analyzes your source code for known-bad patterns — SQL built from string concatenation, hardcoded credentials, unsafe deserialization — without running the code.

  • Semgrep is the pragmatic default: fast, low false-positive rate if you tune the ruleset, and it understands most mainstream languages out of the box.
  • SonarQube adds code-quality metrics alongside security, useful if you want one dashboard for both.
  • Tune the ruleset early. A SAST tool that fires 200 warnings on day one trains engineers to ignore it; start with a curated high-confidence ruleset and expand.

3. Software Composition Analysis (SCA) / Dependency Scanning

Your own code is rarely the biggest attack surface — your dependencies are. SCA tools check your package.json, requirements.txt, go.mod, and similar against known-vulnerability databases (the OSV database, GitHub Advisory Database).

  • Dependabot or Renovate for automated PRs that bump vulnerable dependencies
  • Trivy or Snyk to fail a build outright on a critical CVE in a dependency, rather than just opening a PR to fix it later

4. Container Image Scanning

Once you have a built image, scan the OS packages and application dependencies baked into every layer.

  • Trivy and Grype are both fast enough to run on every build and both check against the same upstream vulnerability data, so pick whichever integrates more easily with your registry
  • Scan before push to a registry, not after — you want the build to fail, not a scheduled job to find out three days later

5. SBOM Generation

A Software Bill of Materials is a machine-readable manifest of every package and library in your image. Increasingly a compliance requirement (especially for anything touching US federal contracts, following Executive Order 14028), and useful on its own merits — when the next critical CVE drops, you can grep your SBOMs instead of re-scanning every image in your registry.

  • Syft generates SBOMs in SPDX or CycloneDX format and pairs naturally with Trivy or Grype for scanning

6. Policy as Code

Static scans catch bad code; policy as code catches bad configuration — a Kubernetes deployment with no resource limits, a container running as root, a security group open to 0.0.0.0/0.

  • OPA (Open Policy Agent) / Gatekeeper for admission control inside a Kubernetes cluster — reject the manifest before it’s ever scheduled
  • Conftest to run the same Rego policies against manifests in CI, before they even reach the cluster
  • Checkov or tfsec specifically for Terraform, catching misconfigured cloud resources before apply

7. Dynamic Testing (DAST)

Everything above analyzes code and config at rest. DAST attacks a running instance of your application the way an external attacker would — fuzzing inputs, probing for injection, checking auth boundaries.

  • OWASP ZAP is the standard open-source option, scriptable enough to run against a staging environment as part of a pipeline stage rather than only manually

A Working Example: GitHub Actions

This is a trimmed but real pipeline shape — secrets scan and SAST gate the PR, image scanning and SBOM generation happen at build time, and policy checks run before anything touches a cluster.

name: secure-ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  secrets-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: semgrep/semgrep-action@v1
        with:
          config: p/ci

  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aquasecurity/trivy-action@0.24.0
        with:
          scan-type: fs
          severity: CRITICAL,HIGH
          exit-code: 1

  build-and-scan-image:
    needs: [secrets-scan, sast, dependency-scan]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t app:${{ github.sha }} .
      - name: Scan image
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: app:${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: 1
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: app:${{ github.sha }}
          format: cyclonedx-json

  policy-check:
    needs: [build-and-scan-image]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check Kubernetes manifests
        uses: instrumenta/conftest-action@master
        with:
          files: k8s/
          policy: policy/

Every job that finds a critical issue exits non-zero, which fails the check and blocks the merge — the same mechanism as a failing test, which is the point. Security stops being a separate gate someone remembers to run and becomes a property of main always being deployable.

Metrics That Actually Matter

Vanity metrics (“we scan 100% of builds”) don’t tell you if the program is working. Better ones:

  • Mean time to remediate, by severity — critical findings should close in days, not quarters
  • Vulnerability escape rate — findings that reach production despite the pipeline, which tells you where a stage is missing or misconfigured
  • False-positive rate per tool — the fastest way to kill a security program is a noisy tool engineers learn to ignore

Where to Start If You Have Nothing

Don’t try to add all seven stages in one sprint. In order of impact-to-effort ratio: secrets scanning first (catches the most damaging class of leak for the least setup), then dependency scanning, then container image scanning. SAST and policy as code take more tuning to get the false-positive rate low enough that engineers trust them — plan for that as a second phase, not day one.

Need help implementing any of this?

Talk to Code2Deploy about DevOps, CloudOps, and LLMOps consulting.

Contact us