Series Navigation
→ Part 11: DevSecOps — Security in CI/CD Pipelines
Infrastructure as Code Security
IaC files (Terraform, CloudFormation, Pulumi) define your cloud infrastructure. A vulnerability in an IaC file becomes a vulnerability in production.
# ❌ Insecure Terraform — multiple issues
resource "aws_s3_bucket" "data" {
bucket = "company-customer-data"
acl = "public-read" # PUBLIC!
}
resource "aws_db_instance" "postgres" {
identifier = "prod-db"
engine = "postgres"
publicly_accessible = true # exposed to internet
storage_encrypted = false # no encryption at rest
deletion_protection = false # can be deleted accidentally
password = "Password1!" # hardcoded credential
}
resource "aws_security_group" "app" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # SSH from anywhere!
}
}
# ✅ Secure Terraform
resource "aws_s3_bucket" "data" {
bucket = "company-customer-data"
}
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_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.s3_key.arn
}
}
}
resource "aws_db_instance" "postgres" {
identifier = "prod-db"
engine = "postgres"
publicly_accessible = false
storage_encrypted = true
kms_key_id = aws_kms_key.rds_key.arn
deletion_protection = true
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# password from Secrets Manager, never hardcoded
}
IaC Security Scanning
# Checkov — comprehensive IaC scanner
pip install checkov
# Scan all Terraform files
checkov -d ./infrastructure/terraform/ --framework terraform
# Scan CloudFormation
checkov -f template.yaml --framework cloudformation
# Only check specific rules
checkov -d . --check CKV_AWS_18,CKV_AWS_19,CKV_AWS_20
# Generate SARIF output (for GitHub Code Scanning)
checkov -d . --output sarif --output-file checkov-results.sarif
# tfsec — Terraform-specific scanner
brew install tfsec
tfsec ./terraform/ --format json
# terrascan
terrascan scan -t aws -i terraform
# KICS (Keeps Infrastructure as Code Secure) — multi-platform
docker run -v $(pwd):/path checkmarx/kics scan -p /path -o /path/results
# GitHub Actions — IaC security in PR checks
name: IaC Security Scan
on: [pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: infrastructure/
framework: terraform
output_format: sarif
output_file_path: results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif
SAST — Static Application Security Testing
SAST analyses source code without running it, finding vulnerabilities during development.
# Semgrep — rule-based SAST (free, fast, accurate)
pip install semgrep
# Run OWASP Top 10 rules
semgrep --config "p/owasp-top-ten" ./src/
# Run Python security rules
semgrep --config "p/python" ./src/
# Run JavaScript/Node rules
semgrep --config "p/nodejs" ./src/
# Custom rules
cat > sql-injection.yaml << 'EOF'
rules:
- id: sql-injection
pattern: |
db.execute($QUERY.format(...))
message: "Possible SQL injection — use parameterised queries"
languages: [python]
severity: ERROR
EOF
semgrep --config sql-injection.yaml ./src/
# Bandit — Python security scanner
pip install bandit
bandit -r ./src/ -ll # only medium and high severity
bandit -r ./src/ -f json -o bandit-report.json
# ESLint security plugin — JavaScript
npm install eslint-plugin-security
# .eslintrc.json
{
"plugins": ["security"],
"extends": ["plugin:security/recommended"]
}
npx eslint ./src/ --ext .js,.ts
# SpotBugs + FindSecBugs — Java
mvn spotbugs:spotbugs
Writing Custom Semgrep Rules
# custom-rules/no-hardcoded-secrets.yaml
rules:
- id: hardcoded-api-key
patterns:
- pattern: $KEY = "..."
- metavariable-regex:
metavariable: $KEY
regex: '.*(api_key|secret|password|token|credential).*'
- metavariable-regex:
metavariable: "..."
regex: '[A-Za-z0-9]{20,}' # long random string
message: "Potential hardcoded secret in variable $KEY"
severity: ERROR
languages: [python, javascript, java]
- id: sql-string-format
pattern: |
cursor.execute("..." % ...)
message: "SQL injection: use parameterised queries, not string formatting"
severity: ERROR
languages: [python]
- id: unsafe-yaml-load
pattern: yaml.load(...)
fix: yaml.safe_load(...)
message: "yaml.load() is unsafe — use yaml.safe_load()"
severity: WARNING
languages: [python]
DAST — Dynamic Application Security Testing
DAST tests the running application, finding vulnerabilities that only appear at runtime.
# OWASP ZAP — most popular open-source DAST tool
docker pull owasp/zap2docker-stable
# Baseline scan (passive — doesn't attack)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
zap-baseline.py \
-t https://staging.your-app.com \
-r baseline-report.html \
-I # don't fail on warn
# Full scan (active — attacks the app)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
zap-full-scan.py \
-t https://staging.your-app.com \
-r full-scan-report.html \
-J full-scan-report.json
# API scan (uses OpenAPI/Swagger spec)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
zap-api-scan.py \
-t https://api.staging.your-app.com/swagger.json \
-f openapi \
-r api-scan-report.html
# Automated ZAP scan via Python API
from zapv2 import ZAPv2
import time
zap = ZAPv2(proxies={'http': 'http://localhost:8080', 'https': 'http://localhost:8080'})
target = 'https://staging.your-app.com'
# Spider the application
print("Spidering...")
scan_id = zap.spider.scan(target)
while int(zap.spider.status(scan_id)) < 100:
time.sleep(2)
# Active scan
print("Active scanning...")
scan_id = zap.ascan.scan(target)
while int(zap.ascan.status(scan_id)) < 100:
print(f"Progress: {zap.ascan.status(scan_id)}%")
time.sleep(5)
# Get results
alerts = zap.core.alerts(baseurl=target)
high_alerts = [a for a in alerts if a['risk'] in ['High', 'Critical']]
if high_alerts:
print(f"FAILED: {len(high_alerts)} high/critical vulnerabilities found")
for alert in high_alerts:
print(f" [{alert['risk']}] {alert['name']}: {alert['url']}")
exit(1)
print("PASSED: No high/critical vulnerabilities found")
SCA — Software Composition Analysis
# Python — check all dependencies
pip install pip-audit
pip-audit
# Safety
pip install safety
safety check
# Node.js
npm audit
npm audit --json | python3 -c "
import json,sys
data = json.load(sys.stdin)
critical = [v for v in data.get('vulnerabilities',{}).values() if v['severity']=='critical']
print(f'Critical: {len(critical)}')
for v in critical: print(f' {v[\"name\"]}: {v[\"title\"]}')
"
# Java
mvn dependency-check:check # OWASP Dependency Check
# Container images (checks OS packages too)
trivy image your-app:latest --severity HIGH,CRITICAL
# Snyk (multi-language, SaaS)
npm install -g snyk
snyk auth
snyk test --all-projects
snyk monitor # continuous monitoring
snyk container test your-app:latest
# Create a SBOM (Software Bill of Materials) — required for some compliance frameworks
syft your-app:latest -o spdx-json > sbom.json
grype sbom:sbom.json # scan SBOM for vulnerabilities
Secrets Management
Never Hardcode Secrets
# Detect secrets in git history
pip install truffleHog
trufflehog git https://github.com/your-org/your-repo
# Or in local repo
trufflehog git file://./
# detect-secrets — pre-commit hook
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Add to pre-commit:
pre-commit install
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/gitleaks/gitleaks
rev: v8.15.2
hooks:
- id: gitleaks
AWS Secrets Manager
import boto3
import json
from functools import lru_cache
secretsmanager = boto3.client('secretsmanager', region_name='us-east-1')
@lru_cache(maxsize=None)
def get_secret(secret_name: str) -> dict:
"""Get secret — cached in memory to reduce API calls"""
response = secretsmanager.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
# Usage — no secrets in code at all
db_config = get_secret('prod/app/database')
DATABASE_URL = f"postgresql://{db_config['username']}:{db_config['password']}@{db_config['host']}/{db_config['dbname']}"
stripe_config = get_secret('prod/app/stripe')
STRIPE_API_KEY = stripe_config['api_key']
HashiCorp Vault
import hvac
vault_client = hvac.Client(
url='https://vault.your-company.com:8200',
token=os.environ['VAULT_TOKEN'] # short-lived token from Vault auth
)
# Read secret
secret = vault_client.secrets.kv.read_secret_version(
path='app/database',
mount_point='secret'
)
db_password = secret['data']['data']['password']
# Dynamic database credentials — Vault generates short-lived creds
# No static database passwords at all
db_creds = vault_client.read('database/creds/app-role')
# Returns: {'username': 'v-token-XYZ', 'password': 'A1b2C3...', 'ttl': 3600}
# Credentials expire after TTL — no long-lived DB passwords
Interview Questions
Q: What is the difference between SAST, DAST, and IAST?
SAST (Static) analyses source code without running it — fast, runs in CI, finds code-level issues but can't find runtime vulnerabilities. DAST (Dynamic) tests the running application — finds real runtime vulnerabilities, no source code needed, but slower and requires a running environment. IAST (Interactive) instruments the application during testing — combines benefits of both but requires agent installation and active test execution. In practice: SAST early in pipeline, DAST in staging, dependency scanning continuously.
Q: A developer wants to store a database password in a Kubernetes secret. Is this secure enough?
Kubernetes secrets are only base64 encoded by default — not encrypted. Anyone with kubectl get secret permission can read them. To make them secure: enable encryption at rest in etcd (AES-CBC or KMS provider), use external secret operators (External Secrets Operator with AWS Secrets Manager or Vault), restrict RBAC so only the app's service account can read the specific secret, and audit secret access via audit logs. The best approach is using an external secret manager and injecting credentials as files at runtime.
What's Next
In Part 11 we build the DevSecOps pipeline — integrating all these security tools (SAST, DAST, SCA, secret scanning, IaC scanning) into a single CI/CD pipeline that prevents vulnerabilities from reaching production.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.