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 │
└─────────────────────────────────────────────────────────────────┘
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
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
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
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
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')
"
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"]
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
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
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/"
}
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
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
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"
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"
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)