How to Build a DevSecOps Pipeline from Scratch: A Step-by-Step Guide for 2026
By Kenny Ogunlowo ·
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 Pipelineon: 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.sarifStage 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 |
|---|