DEV Community

Thesius Code
Thesius Code

Posted on

DevSecOps: Integrating Security into Your CI/CD Pipeline

A single leaked API key costs an average of $1.2 million to remediate — and it takes most teams 327 days to even detect it. The uncomfortable truth is that bolting security checks onto the end of your release process doesn't work; by the time you find a vulnerability in production, the blast radius is already enormous.

DevSecOps shifts security left — integrating it into every stage of your CI/CD pipeline so issues are caught early, automatically, and consistently. This guide covers practical implementation: what to scan, when to scan it, which tools to use, and how to wire everything into GitHub Actions without slowing your team down.

The DevSecOps Pipeline

Security checks should happen at every stage, not just at the end:

┌─────────────────────────────────────────────────────────────────┐
│                      DevSecOps Pipeline                         │
│                                                                 │
│  Code ──→ Build ──→ Test ──→ Deploy ──→ Run ──→ Monitor        │
│   │         │        │         │        │         │             │
│   ▼         ▼        ▼         ▼        ▼         ▼             │
│  SAST    Dependency  DAST    Config   Runtime   Incident        │
│  Secrets  Scanning  Container  Audit  Scanning  Response        │
│  Linting  License   Image    Infra   WAF       Alerting        │
│           Check     Scan     Scan    RASP      Forensics       │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Security Scan Types

Scan Type What It Does When Speed
SAST Analyzes source code for vulnerabilities Every commit Fast
SCA Checks dependencies for known CVEs Every build Fast
Secrets Detection Finds leaked credentials in code Pre-commit + CI Fast
Container Scanning Scans Docker images for vulnerabilities Image build Medium
DAST Tests running application for vulnerabilities Staging deploy Slow
IaC Scanning Checks Terraform/CloudFormation for misconfigs Every commit Fast
License Compliance Checks dependency licenses Build Fast

1. Pre-Commit Hooks: First Line of Defense

Catch issues before they even enter version control.

Secret Detection with detect-secrets

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
        exclude: 'tests/.*|\.lock$'

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-added-large-files
        args: ['--maxkb=500']
      - id: check-merge-conflict
      - id: detect-private-key
      - id: no-commit-to-branch
        args: ['--branch', 'main']

  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker
        name: Lint Dockerfiles
Enter fullscreen mode Exit fullscreen mode

Initialize Secret Baseline

# Create baseline (ignores existing false positives)
detect-secrets scan > .secrets.baseline

# Audit baseline to mark false positives
detect-secrets audit .secrets.baseline

# Install pre-commit hooks
pre-commit install
Enter fullscreen mode Exit fullscreen mode

2. SAST: Static Application Security Testing

SAST scans your source code for security vulnerabilities like SQL injection, XSS, and insecure deserialization.

Python SAST with Bandit

# .github/workflows/security.yml
name: Security Pipeline

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

permissions:
  contents: read
  security-events: write
  pull-requests: write

jobs:
  sast:
    name: Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Bandit (Python SAST)
        uses: PyCQA/bandit-action@v1
        with:
          configfile: "bandit.yaml"
          targets: "app/"
          severity: "medium"

      - name: Run Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/python
            p/jwt
            p/sql-injection
Enter fullscreen mode Exit fullscreen mode

Bandit Configuration

# bandit.yaml
skips:
  - B101  # assert used (OK in tests)
  - B601  # paramiko (if you actually use it)

exclude_dirs:
  - tests
  - venv
  - .venv

tests:
  - B102  # exec used
  - B103  # set_bad_file_permissions
  - B104  # hardcoded_bind_all_interfaces
  - B105  # hardcoded_password_string
  - B106  # hardcoded_password_funcarg
  - B107  # hardcoded_password_default
  - B108  # hardcoded_tmp_directory
  - B110  # try_except_pass
  - B201  # flask_debug_true
  - B301  # pickle
  - B302  # marshal
  - B303  # md5 / sha1
  - B304  # des / insecure cipher
  - B305  # cipher_no_integrity
  - B306  # mktemp_q
  - B307  # eval
  - B308  # mark_safe
  - B310  # urllib_urlopen
  - B311  # random
  - B312  # telnetlib
  - B320  # xml_bad_cElementTree
  - B321  # ftplib
  - B323  # unverified_context
  - B324  # hashlib_insecure_functions
  - B501  # request_with_no_cert_validation
  - B502  # ssl_with_bad_version
  - B503  # ssl_with_bad_defaults
  - B504  # ssl_with_no_version
  - B505  # weak_cryptographic_key
  - B506  # yaml_load
  - B507  # ssh_no_host_key_verification
  - B601  # paramiko_calls
  - B602  # subprocess_popen_with_shell_equals_true
  - B603  # subprocess_without_shell_equals_true
  - B604  # any_other_function_with_shell_equals_true
  - B605  # start_process_with_a_shell
  - B606  # start_process_with_no_shell
  - B607  # start_process_with_partial_path
  - B608  # hardcoded_sql_expressions
  - B609  # linux_commands_wildcard_injection
  - B610  # django_extra_used
  - B611  # django_rawsql_used
  - B701  # jinja2_autoescape_false
  - B702  # use_of_mako_templates
  - B703  # django_mark_safe
Enter fullscreen mode Exit fullscreen mode

3. Dependency Scanning (SCA)

The majority of your codebase is dependencies. If a dependency has a known CVE, your application is vulnerable.

GitHub Actions SCA Pipeline

  dependency-scan:
    name: Dependency Scanning
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Safety (Python)
        run: |
          pip install safety
          safety check --file requirements.txt --output json > safety-report.json

          # Fail on high/critical vulnerabilities
          CRITICAL=$(python3 -c "
          import json
          with open('safety-report.json') as f:
              data = json.load(f)
          vulns = [v for v in data.get('vulnerabilities', []) 
                   if v.get('severity', '') in ('high', 'critical')]
          print(len(vulns))
          ")
          if [ "$CRITICAL" -gt 0 ]; then
            echo "Found $CRITICAL high/critical vulnerabilities"
            exit 1
          fi

      - name: Run Trivy (comprehensive scanner)
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-fs-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-fs-results.sarif'

      - name: License Check
        run: |
          pip install pip-licenses
          pip-licenses --format=json --output-file=licenses.json

          # Check for copyleft licenses in production deps
          python3 -c "
          import json
          BLOCKED = ['GPL-3.0', 'AGPL-3.0', 'GPL-2.0']
          with open('licenses.json') as f:
              deps = json.load(f)
          violations = [d for d in deps if d.get('License') in BLOCKED]
          if violations:
              for v in violations:
                  print(f\"BLOCKED: {v['Name']} ({v['License']})\" )
              exit(1)
          print('All licenses OK')
          "
Enter fullscreen mode Exit fullscreen mode

4. Container Security

Docker images are a common attack vector. Scan them before pushing to a registry.

Dockerfile Hardening

# GOOD: Hardened Dockerfile
FROM python:3.12-slim AS builder

# Don't run as root
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app

# Install dependencies first (cache layer)
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# Copy application
COPY --chown=appuser:appuser app/ app/

# Production image
FROM python:3.12-slim

# Security: non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

# Security: remove unnecessary packages
RUN apt-get update && apt-get upgrade -y && \
    apt-get autoremove -y && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy from builder
COPY --from=builder /root/.local /home/appuser/.local
COPY --from=builder --chown=appuser:appuser /app /app

# Security: read-only filesystem
RUN chmod -R a-w /app

# Security: non-root user
USER appuser

# Security: no new privileges
# (enforced at runtime with --security-opt=no-new-privileges)

ENV PATH=/home/appuser/.local/bin:$PATH
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Container Image Scanning

  container-scan:
    name: Container Security
    runs-on: ubuntu-latest
    needs: [sast, dependency-scan]
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Lint Dockerfile
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning

      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-image-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Scan with Grype
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
          grype myapp:${{ github.sha }} --fail-on high --output json > grype-results.json
Enter fullscreen mode Exit fullscreen mode

5. Infrastructure as Code Security

Terraform and CloudFormation misconfigurations are a top cloud security risk.

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

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: infrastructure/
          framework: terraform
          soft_fail: false
          output_format: sarif
          output_file_path: checkov-results.sarif

      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.3
        with:
          working_directory: infrastructure/
          format: sarif
          soft_fail: false
Enter fullscreen mode Exit fullscreen mode

Common IaC Misconfigurations

# BAD: S3 bucket without encryption
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
}

# GOOD: S3 bucket with encryption + access controls
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
}

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

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

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

resource "aws_s3_bucket_logging" "data" {
  bucket        = aws_s3_bucket.data.id
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "s3-access-logs/"
}
Enter fullscreen mode Exit fullscreen mode

6. DAST: Dynamic Application Security Testing

DAST tests the running application for vulnerabilities. Run it against staging environments.

  dast:
    name: Dynamic Security Testing
    runs-on: ubuntu-latest
    needs: [deploy-staging]
    steps:
      - name: OWASP ZAP Scan
        uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: 'https://staging.example.com'
          rules_file_name: '.zap-rules.tsv'
          fail_action: 'true'
          allow_issue_writing: 'true'

      - name: API Security Scan
        run: |
          # Scan OpenAPI spec for security issues
          docker run --rm \
            -v $(pwd):/app \
            42crunch/api-security-audit:latest \
            --openapi /app/openapi.json \
            --min-score 70
Enter fullscreen mode Exit fullscreen mode

7. Secrets Management

Hardcoded secrets are the most common security vulnerability. Here's how to prevent and detect them.

Secret Detection in CI

  secrets-scan:
    name: Secrets Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for scanning

      - name: Gitleaks Scan
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: TruffleHog Scan
        run: |
          docker run --rm \
            -v $(pwd):/repo \
            trufflesecurity/trufflehog:latest \
            git file:///repo \
            --only-verified \
            --fail
Enter fullscreen mode Exit fullscreen mode

Gitleaks Configuration

# .gitleaks.toml
title = "Custom Gitleaks Config"

[allowlist]
paths = [
  '''\.secrets\.baseline''',
  '''tests/fixtures/''',
  '''docs/''',
]

[[rules]]
id = "aws-access-key"
description = "AWS Access Key"
regex = '''AKIA[0-9A-Z]{16}'''
tags = ["aws", "key"]
severity = "critical"

[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(api[_-]?key|apikey)\s*[=:]\s*['\"][0-9a-zA-Z]{32,}['\"]'''
tags = ["api", "key"]
severity = "high"

[[rules]]
id = "private-key"
description = "Private Key"
regex = '''-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----'''
tags = ["key", "private"]
severity = "critical"

[[rules]]
id = "database-url"
description = "Database Connection String"
regex = '''(?i)(postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@'''
tags = ["database", "credential"]
severity = "critical"
Enter fullscreen mode Exit fullscreen mode

8. Complete Pipeline Integration

Here's how all the pieces fit together in a single workflow:

# .github/workflows/devsecops.yml
name: DevSecOps Pipeline

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

jobs:
  # Stage 1: Fast scans (run in parallel)
  secrets:
    uses: ./.github/workflows/secrets-scan.yml
  sast:
    uses: ./.github/workflows/sast.yml
  iac-scan:
    uses: ./.github/workflows/iac-scan.yml

  # Stage 2: Build + scan
  build-and-scan:
    needs: [secrets, sast, iac-scan]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: safety check --file requirements.txt
      - run: docker build -t myapp:${{ github.sha }} .
      - name: Scan image
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          exit-code: '1'
          severity: 'CRITICAL'

  # Stage 3: Deploy to staging
  deploy-staging:
    needs: [build-and-scan]
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "Deploy to staging"

  # Stage 4: DAST against staging
  dast:
    needs: [deploy-staging]
    runs-on: ubuntu-latest
    steps:
      - uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: 'https://staging.example.com'

  # Stage 5: Deploy to production
  deploy-prod:
    needs: [dast]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - run: echo "Deploy to production"
Enter fullscreen mode Exit fullscreen mode

Security Metrics to Track

Metric Target Why
Mean time to remediate critical CVEs < 48 hours Limits exposure window
% of builds with security scans 100% No bypasses
Known critical vulnerabilities in prod 0 Zero tolerance for criticals
Secret leak incidents 0 per quarter Prevention effectiveness
Dependency update frequency Weekly Reduce known CVE exposure
Container image age in prod < 30 days Fresh base images

Summary

DevSecOps is about automating what security teams already know should happen:

Stage Tool Block Deploy?
Pre-commit detect-secrets, hadolint Yes (local)
CI - SAST Bandit, Semgrep Yes (critical only)
CI - SCA Trivy, Safety Yes (critical/high)
CI - Secrets Gitleaks, TruffleHog Yes (always)
CI - Container Trivy, Grype Yes (critical)
CI - IaC Checkov, tfsec Yes (high+)
Staging - DAST OWASP ZAP Warn (don't block)

Start with secrets detection and dependency scanning — they're fast, high-impact, and low friction. Then add SAST, container scanning, and DAST incrementally.


For more data engineering and DevOps toolkits, templates, and production-ready resources, visit DataStack Pro.

Top comments (0)