How to Build a DevSecOps Pipeline from Scratch: A Step-by-Step Guide for 2026


title: "How to Build a DevSecOps Pipeline from Scratch: A Step-by-Step Guide for 2026"

slug: "devsecops-pipeline-tutorial"

meta_description: "Step-by-step DevSecOps pipeline tutorial using GitHub Actions, Terraform, Trivy, SonarQube, OWASP ZAP, and Checkov. Real enterprise patterns from Lockheed and Cigna."

author: "Kenny Ogunlowo"

date: 2026-05-08

category: "DevOps"

tags:

  • devsecops pipeline tutorial
  • DevSecOps
  • GitHub Actions
  • Terraform security
  • Trivy
  • SonarQube
  • OWASP ZAP
  • Checkov
  • CI/CD security
  • shift-left security
  • CMMC compliance
  • HIPAA compliance
  • FedRAMP

internal_links:

  • /collections/devops-pipelines
  • /collections/cybersecurity-frameworks
  • /pages/free-courses

word_count: 2100


How to Build a DevSecOps Pipeline from Scratch: A Step-by-Step Guide for 2026

The first time I broke a production pipeline at Lockheed Martin, I learned something that no certification exam teaches: security bolted on at the end of a software delivery process is not security — it is theater. The code had passed every functional test, every code review, and made it to a pre-prod environment before a security audit two weeks later caught a critical vulnerability that could have exposed classified system metadata. Two weeks. The cost of that two-week delay in a defense program context is measured in contract penalties, not sprint points.

That experience reshaped how I think about software delivery. At Cigna Healthcare, where HIPAA violations carry fines north of $50,000 per incident, we rebuilt the entire delivery pipeline around a single principle: security checks must be as fast and automatic as running unit tests. At NantHealth, where oncology data pipelines were processing patient genomic records, the same discipline applied.

This is the guide I wish I had when I started building DevSecOps pipelines. It covers every layer — from static code analysis and container scanning to infrastructure-as-code validation and dynamic application testing — with real tool configuration and GitHub Actions workflow YAML you can adapt and use today.

If you want ready-to-use production templates, the DevOps Pipelines toolkit at Citadel Cloud covers 40+ pipeline patterns including CMMC-compliant and HIPAA-ready configurations.


What DevSecOps Actually Means in Practice

DevSecOps is not a tool. It is not a team. It is not a compliance checkbox. It is the practice of integrating security validation into every stage of the software delivery lifecycle — code commit, build, test, deploy, and runtime — so that security feedback arrives in seconds, not weeks.

The "shift-left" terminology is accurate but often misunderstood. Shifting left means moving security checks closer to the point of code creation. It does not mean dumping the entire security function onto developers. It means:

  • Developers get immediate feedback on security issues while the code is fresh in their heads.
  • Security engineers define the guardrails; the pipeline enforces them automatically.
  • No human security reviewer is blocked waiting to manually approve every build.

The stack I use consistently across enterprise engagements breaks down into five layers:

Layer Tool What It Catches
Static Application Security Testing (SAST) SonarQube Code vulnerabilities, secrets in code, code quality gates
Container Image Scanning Trivy Known CVEs in base images and dependencies
Infrastructure as Code (IaC) Scanning Checkov Misconfigurations in Terraform, CloudFormation, Kubernetes
Dynamic Application Security Testing (DAST) OWASP ZAP Runtime vulnerabilities in running applications

Step 1: Repository Structure and Branch Strategy

Before writing a single line of pipeline code, establish the repository structure. Every security decision downstream depends on this foundation.

At Lockheed, we used a mono-repo structure for each program. At Cigna, we used polyrepos with a shared pipeline template repository. Either works, but the branch protection rules are non-negotiable.

Required branch protection rules on `main`:

  • Require pull request reviews (minimum 2 approvals for changes to pipeline files)
  • Require status checks to pass before merging (every CI job must be a required check)
  • Require signed commits (especially important for FedRAMP and CMMC environments)
  • Disallow force pushes to `main`
  • Require conversation resolution before merging

For secrets management, never store secrets in the repository. In GitHub Actions, use environment secrets with environment-level approval gates. For production deployments, use OIDC (OpenID Connect) to authenticate to cloud providers — this eliminates long-lived credentials entirely.


# .github/workflows/setup-oidc.yml — example OIDC trust for AWS
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1

This eliminates `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from your secrets store entirely. Every authentication event is logged as an assumed-role event in CloudTrail, with the GitHub Actions job URL embedded in the session context. At Cigna, this was the single change that most dramatically improved our audit trail quality.


Step 2: Static Analysis with SonarQube

SonarQube is the SAST tool I reach for first. It handles code quality and security simultaneously, which matters in enterprise environments where you are selling both properties to engineering leadership.

The critical configuration decision is the quality gate. The default SonarQube quality gate is too permissive for production workloads. I always create a custom gate with at minimum:

  • Zero new Critical or Blocker security issues
  • Zero new vulnerabilities with CVSS score ≥ 7.0
  • Code coverage on new code ≥ 80%
  • Duplicated lines on new code < 3%

Here is the GitHub Actions job that runs SonarQube analysis and fails the pipeline if the quality gate fails:


  sonarqube-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for SonarQube branch analysis

      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v3
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
        with:
          args: >
            -Dsonar.projectKey=my-app
            -Dsonar.qualitygate.wait=true
            -Dsonar.qualitygate.timeout=300

The `sonar.qualitygate.wait=true` flag is what people miss. Without it, the action submits the scan and immediately returns success. With it, the action polls the SonarQube server until the quality gate computation completes, then fails the build if the gate fails. This is a blocking check, which is what you want.

At NantHealth, we extended this with custom SonarQube rules for PHI (Protected Health Information) — specifically, custom rules that flagged any string concatenation involving variables named `patient_id`, `ssn`, `dob`, or matching common PHI field naming patterns. This caught logging statements that would have written PHI to CloudWatch in plain text.


Step 3: Container Scanning with Trivy

Trivy is the fastest production-grade container scanner I have used. It scans container images for OS package vulnerabilities (CVEs), application dependency vulnerabilities, secrets accidentally baked into images, and misconfigurations. A full scan of a production Node.js image completes in under 30 seconds.

The key configuration decision: what severity level fails the build? I use `CRITICAL` and `HIGH` for most workloads. For CMMC Level 2 and FedRAMP Moderate environments at Lockheed, we blocked on `MEDIUM` as well, which requires a more disciplined base image update cadence.


  container-scan:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Build Docker image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: my-app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: 1
          ignore-unfixed: true
          vuln-type: os,library
          scanners: vuln,secret,misconfig

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

The `ignore-unfixed: true` flag deserves explanation. When a CVE has been identified but no patch is yet available, failing the build on an unfixable vulnerability accomplishes nothing except blocking deployment. Set this to `true` and focus enforcement energy on vulnerabilities that have available fixes.

Uploading SARIF results to the GitHub Security tab creates a persistent vulnerability tracking record linked to specific commits. This is exactly the kind of audit trail that FedRAMP assessors and HIPAA auditors look for.

One practice I established at Cigna: pin base images to specific digest hashes, not just tags. `FROM python:3.12.3-slim-bookworm@sha256:abc123...` is immutable. `FROM python:3.12-slim` is not — the tag can be updated to point to a different, potentially vulnerable image without your knowledge.


Step 4: Infrastructure Scanning with Checkov

Terraform is the IaC tool of choice across all three organizations I mentioned. Checkov is the tool that validates Terraform configurations against security best practices before any infrastructure is provisioned.

At Lockheed, a misconfigured S3 bucket (public read access on a bucket containing program artifacts) would have been a career-ending event. Checkov catches that in the pipeline before `terraform apply` ever runs.

Here is the Checkov job configuration:


  iac-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov IaC scanner
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: ./terraform
          framework: terraform
          output_format: sarif
          output_file_path: checkov-results.sarif
          soft_fail: false
          skip_check: CKV_AWS_144  # Example: skip cross-region replication for dev
          check: CKV_AWS_*,CKV2_AWS_*

      - name: Upload Checkov results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: checkov-results.sarif

A common Checkov finding in Terraform configurations is unencrypted storage. Here is what a compliant Terraform S3 bucket with the checks that Checkov enforces looks like:


resource "aws_s3_bucket" "artifacts" {
  bucket = "my-app-artifacts-${var.environment}"

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.artifacts.arn
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_public_access_block" "artifacts" {
  bucket                  = aws_s3_bucket.artifacts.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

This configuration passes the critical Checkov checks: `CKV_AWS_19` (encryption enabled), `CKV_AWS_21` (versioning enabled), `CKV_AWS_53`, `CKV_AWS_54`, `CKV_AWS_55`, `CKV_AWS_56` (public access blocked). At Lockheed, we had Checkov enforcement prevent over 200 potential misconfigurations across 18 months of active Terraform development. Most were in development environments — but development environments have a way of becoming production environments when timelines compress.

For more Terraform security patterns and hardened IaC templates, the Cybersecurity Frameworks collection at Citadel Cloud includes Terraform modules pre-validated against CMMC Level 2 and NIST 800-53 controls.


Step 5: Dynamic Analysis with OWASP ZAP

Static and IaC scanning catches issues before the application runs. DAST catches issues in the running application — authentication bypasses, injection vulnerabilities, misconfigurations that only manifest at runtime. OWASP ZAP is the tool for this layer.

The practical challenge with DAST in CI/CD is that you need a running instance of the application to scan. This means running ZAP in a separate job after the application is deployed to a staging environment. The workflow looks like this:


  dast-scan:
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v4

      - name: OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: ${{ secrets.STAGING_URL }}
          rules_file_name: .zap/rules.tsv
          cmd_options: >
            -a
            -j
            -T 60
            -z "-config api.addrs.addr.name=.* 
                -config api.addrs.addr.regex=true"
          fail_action: true
          artifact_name: zap-report

      - name: Upload ZAP report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-security-report
          path: report_html.html

The `.zap/rules.tsv` file lets you configure which findings block the pipeline versus which are accepted risks. At Cigna, we maintained a controlled suppression list — findings that had been reviewed and accepted as low risk for our specific deployment context — with each suppression requiring a documented security exception approved by the HIPAA Security Officer.

A baseline scan is the starting point. For more thorough coverage, run ZAP's full active scan (`zaproxy/action-full-scan`) on weekly scheduled runs rather than every PR — active scanning takes 10-40 minutes depending on application surface area, which is acceptable for a nightly or weekly schedule but too slow for per-commit feedback.


Step 6: Composing the Full Pipeline

The full pipeline composes these stages with proper job dependencies and parallelization:


name: DevSecOps Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # Stage 1: Code quality and security (runs in parallel)
  sast:
    name: Static Analysis (SonarQube)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v3
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
        with:
          args: -Dsonar.qualitygate.wait=true

  iac-scan:
    name: IaC Security (Checkov)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: ./terraform
          framework: terraform
          soft_fail: false

  # Stage 2: Build and scan container image
  build-and-scan:
    name: Build + Container Scan (Trivy)
    runs-on: ubuntu-latest
    needs: [sast, iac-scan]
    permissions:
      contents: read
      packages: write
      security-events: write
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

      - name: Trivy image scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: 1
          ignore-unfixed: true

      - name: Upload Trivy SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

  # Stage 3: Deploy to staging and run DAST
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build-and-scan
    environment: staging
    if: github.ref == 'refs/heads/develop' || github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1
      - name: Deploy via Terraform
        run: |
          cd terraform/environments/staging
          terraform init
          terraform apply -auto-approve \
            -var="image_tag=${{ github.sha }}"

  dast:
    name: Dynamic Analysis (OWASP ZAP)
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v4
      - uses: zaproxy/action-baseline@v0.12.0
        with:
          target: ${{ secrets.STAGING_URL }}
          rules_file_name: .zap/rules.tsv
          fail_action: true

  # Stage 4: Production deployment (requires passing all security gates)
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: [build-and-scan, dast]
    environment: production
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_PROD_ROLE_ARN }}
          aws-region: us-east-1
      - name: Deploy via Terraform
        run: |
          cd terraform/environments/production
          terraform init
          terraform apply -auto-approve \
            -var="image_tag=${{ github.sha }}"

This structure enforces a clear security gate chain: SAST and IaC scanning must pass before a container is built; the container must pass Trivy scanning before staging deployment; staging DAST must pass before production deployment. No shortcutting the chain.


Step 7: Compliance-Specific Configurations

HIPAA Workloads (Cigna Pattern)

For HIPAA, the audit trail requirements are the primary driver of additional pipeline configuration. Every pipeline run must produce artifacts that demonstrate:

  • Which code version was scanned
  • Which tool versions performed the scan
  • What the scan results were
  • Who approved the deployment
  • What time the deployment occurred

GitHub Actions' environment protection rules with required reviewers satisfy the human approval requirement. The SARIF uploads to the Security tab satisfy the scan result retention requirement. Add artifact retention policies to ensure scan results are kept for at least 6 years (HIPAA retention requirement for security records):


      - name: Upload security artifacts
        uses: actions/upload-artifact@v4
        with:
          name: security-scan-results-${{ github.sha }}
          path: |
            trivy-results.sarif
            checkov-results.sarif
            zap-report.html
          retention-days: 2190  # 6 years

CMMC Level 2 / FedRAMP Moderate (Lockheed Pattern)

CMMC and FedRAMP add requirements around the pipeline infrastructure itself — not just what the pipeline scans, but how the pipeline runs. Key additional controls:

  • Use self-hosted GitHub Actions runners on government-controlled infrastructure (not GitHub-hosted runners) to satisfy data sovereignty requirements
  • Require signed commits with GPG keys tied to PIV/CAC cards
  • Run Semgrep with the OWASP rule set in addition to SonarQube, to satisfy the requirement for multiple independent SAST tools
  • Archive all pipeline logs to a FIPS 140-2 validated storage endpoint

  compliance-scan:
    runs-on: self-hosted  # Government-compliant runner
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep OWASP scan
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/cwe-top-25
            p/secrets
          auditOn: push

Building the Skills to Own This Pipeline

The tooling in this guide reflects patterns I developed across a decade of building pipelines in regulated industries. The combination of SonarQube, Trivy, Checkov, and OWASP ZAP covers the four critical security layers that compliance frameworks — FedRAMP, CMMC, HIPAA, SOC 2 — consistently require.

For engineers building these skills from scratch, the DevOps Pipelines collection at Citadel Cloud includes pre-built GitHub Actions workflow templates, hardened Terraform modules, and Checkov policy sets for common compliance frameworks. The free courses at Citadel Cloud include a full DevSecOps module covering each tool in this stack with hands-on labs.


FAQ

Do I need all four tools (SonarQube, Trivy, Checkov, OWASP ZAP) or can I start with just one?

Start with Trivy and Checkov if you are building from zero — they cover the highest-impact risk categories with the least configuration overhead. Trivy will immediately surface known CVEs in your container images, and Checkov will catch the most common Terraform misconfigurations that lead to breaches. SonarQube requires more setup (a hosted SonarQube instance or SonarCloud account) and OWASP ZAP requires a running staging environment, so they are natural second-phase additions. The goal is a complete pipeline, not a perfect pipeline on day one.

How do I handle the false positive problem in SAST and DAST tools?

False positives are real and manageable. In SonarQube, use the "Won't Fix" marking with a documented rationale — this creates an audit trail showing the finding was reviewed and accepted, not ignored. In OWASP ZAP, maintain a `.zap/rules.tsv` file where suppressed findings are listed with alert IDs. In Checkov, use the `skip_check` parameter with a comment explaining the business justification. The key discipline is: every suppression must be documented with a reason and reviewed periodically. At Cigna, we reviewed all suppressions quarterly during our HIPAA risk assessment cycle.

How long does a full DevSecOps pipeline run take in practice?

The parallel jobs in the pipeline above (SAST and IaC scanning running simultaneously while the previous stage completes) typically complete in 8-15 minutes for a medium-sized application. Container build and Trivy scan adds another 3-5 minutes. Staging deployment varies by infrastructure complexity — 5-15 minutes for a containerized ECS service. OWASP ZAP baseline scan on a staging application takes 3-8 minutes. Total wall-clock time for the full pipeline: 20-40 minutes. That is acceptable for a staging-to-production promotion. For PR validation (which excludes staging deployment and DAST), the wall-clock time is 12-20 minutes.

What is the difference between a DevSecOps pipeline and just adding a security scan to my CI/CD?

A single security scan bolted onto an existing CI/CD pipeline is better than nothing, but it is not DevSecOps. The key differences are: (1) multiple complementary scanning layers covering code, container, IaC, and runtime rather than a single tool; (2) security gates that actually block deployment on failure rather than generating reports nobody reads; (3) security findings tracked in the same tooling as code quality findings, visible to the entire engineering team rather than siloed in a security team dashboard; (4) compliance-ready artifact retention so security evidence is available for audits without manual collection efforts. The pipeline in this guide implements all four properties.

Can this pipeline work with cloud providers other than AWS?

Yes. The OIDC authentication pattern works with GCP (Workload Identity Federation), Azure (Federated Identity Credentials), and AWS — the specific `aws-actions/configure-aws-credentials` action swaps for the GCP or Azure equivalent. Terraform and Checkov are cloud-agnostic; Checkov supports GCP, Azure, Kubernetes, Dockerfile, and CloudFormation in addition to AWS Terraform. Trivy is container-registry-agnostic. SonarQube and OWASP ZAP operate at the code and HTTP layer and have no cloud dependency. For multi-cloud environments — which is the default in most enterprise deployments today — the pipeline architecture requires minimal modification beyond the deployment steps.


*Kenny Ogunlowo is a Senior Multi-Cloud DevSecOps Architect and AI Engineer. He has built production DevSecOps pipelines at Lockheed Martin (defense, Secret Clearance), Cigna Healthcare (HIPAA), and NantHealth. He holds certifications in AWS, Azure, GCP, FedRAMP, CMMC, and HIPAA security engineering.*

Pipeline Orchestration GitHub Actions Workflow automation, secret management, environment promotion

You might also like