Metadata Card
- Prerequisites: Chapter 7 (SAST/DAST/SCA concepts), basic CI/CD knowledge, container build process
- Estimated time: 50 minutes
- Core difficulty: Advanced
- Completion mark: Can describe the core philosophy of DevSecOps, can configure SAST/DAST steps in a CI pipeline, can explain the basics of Policy as Code and container signing
Your Progress
You've built a complete defense system at the Border Fortress: encrypted communication protects messages, certificate system identifies allies, the SDL process embeds security requirements into every phase of software development.
But you quickly discovered a new problem—even with SDL clearly defining security activities, the "collaboration" between the development team and security in practice is still a relay race: developers write code, hand it to the security team to scan, find issues, and send it back for rework. Security is always the "final checkpoint" in the pipeline, never part of the pipeline itself.
The contradiction is simple: the security team has only a few people, while the development team has dozens. If you wait until a week before release to scan for vulnerabilities, you either delay the release date or go live with vulnerabilities. Neither is acceptable.
Your Task
Embed security into every step of the CI/CD pipeline, instead of leaving it for last. You'll understand the philosophy of DevSecOps—why security should "shift left," and in which scenarios it should also "shift right." You'll configure SAST scanning, secret detection, container signing, and Policy as Code, so that every code commit automatically passes security gates.
Chapter Layers
- Required: DevSecOps core philosophy, SAST/SCA integration in CI, secrets management, container signing
- Optional: Policy as Code (OPA / Kyverno)
- Advanced: Supply-chain Levels for Software Artifacts (SLSA) and build integrity
Breaking Ground · Tracing the Origin
Problem: Next month the fortress needs a new supply system. The development team has already written over a thousand lines of Python code. Now the security team is auditing before release and finds a hardcoded API key, three unfixed CVE dependencies, and an endpoint that allows unauthenticated access.
Fixing these takes two weeks, but the release is scheduled for next Monday.
This is the classic consequence of "doing security at the end." Security finds issues too late—the cost goes from "change one line of code" to "delay the entire release plan." Worse, if this happens every time, the team will develop a habit: bypassing the security review and going live directly.
What you need isn't a "security gate" at the final door, but "security checkpoints" embedded in every leg of the journey—each code commit automatically passes security checks, instead of a centralized scan before release.
First Piece: The Philosophy of DevSecOps
DevSecOps is not a tool, but a methodology that integrates security into the DevOps culture, practices, and automation pipeline. The core idea is actually one sentence:
Security is the daily responsibility of the development team, not the sole responsibility of the security team.
In the traditional model, the security team bears all security responsibility, while the development team is only responsible for "writing feature code." DevSecOps flips this: the development team considers security when writing code, and the security team provides tools, policies, and training.
Traditional Model (left to right):
Development → Testing → Security Scan → Release
│
└── "Found vulnerabilities, send back to dev to redo"
DevSecOps Model:
Shift Left: Security embedded in every step
│
├─ Commit → SAST + Dependency check + Secret detection
├─ PR/MR → Code review + Container scan
├─ Test → DAST + Fuzz testing
├─ Build → Image signing + SBOM generation
└─ Deploy → Policy validation + Runtime monitoringSecond Piece: NIST SSDF (Secure Software Development Framework)
The U.S. National Institute of Standards and Technology (NIST) released SP 800-218, the SSDF framework, in 2021. It defines a set of best practices for secure software development, divided into four core practice groups:
| ID | Practice Group | Core Activities |
|---|---|---|
| PO | Prepare Organization | Define security roles, training, configuration management |
| PS | Protect Software | Component security, code integrity, secure coding |
| PW | Produce Well-Secured Software | Threat modeling, security testing, vulnerability scanning |
| RV | Respond to Vulnerabilities | Vulnerability discovery and remediation, security advisories |
SSDF doesn't prescribe specific tools, but defines "what you should do." DevSecOps is the operational means to implement SSDF recommendations—in CI/CD, you use automation to implement PS and PW.
Key SSDF activities (PS.2, PW.4, PW.6) map directly to DevSecOps automated gates:
PS.2 (Protect component security) → SCA dependency checking
PW.4 (Review code security) → SAST static analysis
PW.6 (Test runtime security) → DAST dynamic testingThird Piece: SAST Integration in CI
Adding security scanning steps to a GitHub Actions workflow. Every time a developer's code commit goes through CI, security scanning starts automatically—like a supply cart being automatically inspected when it passes an outpost, without needing someone to watch it:
# .github/workflows/sast.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Bandit (Python SAST)
run: |
pip install bandit
bandit -r src/ -ll -f json -o bandit-report.json
continue-on-error: true # Don't block build, but generate report
- name: Upload SAST Report
uses: actions/upload-artifact@v4
with:
name: sast-report
path: bandit-report.json
- name: Fail on Critical Findings
run: |
CRITICAL=$(jq '.metrics._totals.CRITICAL' bandit-report.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "Found $CRITICAL critical vulnerabilities, blocking build"
exit 1
fiKey design decisions:
| Strategy | Description | Applicable Scenario |
|---|---|---|
continue-on-error: true | Scan results don't block build | Legacy codebase, first establish baseline |
| Block CRITICAL | Only critical vulnerabilities block | Team with some security foundation |
| Block ALL findings | Any finding blocks | Greenfield codebase |
| Report only, don't block | Completely non-blocking | Initial integration phase |
Fourth Piece: DAST Integration in CI
DAST is more like a patrol team that, given your existing report system, simulates attackers from the outside to probe your defenses.
Integrating OWASP ZAP in GitLab CI:
# .gitlab-ci.yml
dast:
stage: test
image: ghcr.io/zaproxy/zaproxy:stable
script:
- zap.sh -cmd -quick-url https://staging.fortress.local
-quick-out report.html
-quick-progress
artifacts:
paths:
- report.html
when: always
only:
- merge_requestsDAST generally doesn't run on every commit (because it takes too long), but at:
| Trigger | Frequency | Description |
|---|---|---|
| Every PR/MR | Optional | If project is small and test environment is always prepared |
| Daily scheduled | Recommended | Ensure main branch always passes security checks |
| Before release | Mandatory | Final security test before release |
| After infrastructure changes | On-demand | After proxy, firewall, or network config changes |
Fifth Piece: SCA (Dependency Scanning) Security Embedding
Your fortress can't forge every weapon itself. You procure equipment from external armories, but you need to ensure that equipment hasn't been tampered with.
Over 90% of code in modern projects comes from third-party dependencies. A security vulnerability might be hidden in a deep dependency—your direct dependency is secure, but one of its tools has a CVE.
Trivy is currently the most popular container and filesystem scanner:
# docker-compose integration of trivy scanning
security-scan:
image: aquasec/trivy:latest
volumes:
- ./src:/src
- ./trivy-cache:/root/.cache
command: fs --exit-code 1 --severity CRITICAL,HIGH /src--exit-code 1 is the key blocking strategy—if critical or high severity vulnerabilities are found, Trivy exits with a non-zero code, causing the CI pipeline to fail.
# GitHub Actions integration of Trivy scanning Docker images
- name: Build Docker Image
run: docker build -t my-app:${{ github.sha }} .
- name: Scan Image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # Fail on findingsSixth Piece: Secrets Management in CI/CD
The most embarrassing vulnerability isn't SQL injection—it's a developer hardcoding a database password in the code and accidentally pushing it to a public repository.
GitHub's secret scanning is built-in, but you can also configure your own pipeline to detect secret leaks:
# Using truffleHog in CI to detect hardcoded secrets
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Scan entire git history
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
extra_args: --results=verified,unknownMore critically—don't print secrets in CI logs. A common mistake is debugging by outputting environment variables, and the password ends up exposed in the CI log panel:
# Dangerous: exposes in logs
- name: Configure AWS
run: |
echo "Setting up AWS..."
aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
# Safe: avoid log output
- name: Configure AWS
run: |
aws configure set aws_access_key_id "${{ secrets.AWS_ACCESS_KEY_ID }}"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}Principles for using secrets in CI:
| Principle | Description | Practice |
|---|---|---|
| Minimize | Grant only necessary permissions | Use READ-only tokens, not admin |
| Ephemeral | Use temporary credentials per build | OIDC temporary tokens, not long-term API keys |
| Rotatable | Secrets must be replaceable at any time | Vault / AWS Secrets Manager |
| Auditable | Every secret access is recorded | CI logs + audit trail |
| Non-transmissible | Don't leave traces in logs, build artifacts, image layers | --no-cache builds, log redaction |
Using OIDC (OpenID Connect) to avoid long-term credentials:
# Use GitHub OIDC instead of long-term AWS keys
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deploy-role
aws-region: us-east-1
role-session-name: GitHubActions-${{ github.sha }}This way, GitHub Actions gets a temporary token from AWS for each build, valid for only a few hours. Even if leaked, the token expires automatically.
Seventh Piece: Container Image Signing and Verification
Your fortress receives a crate of weapons. How do you confirm that this crate was actually sent by the quartermaster, not forged by the enemy?
The answer is signing and verification—just like checking the official's letter at the fortress gate.
Container image signing uses Sigstore/cosign (detailed in Chapter 10). Here's a brief integration preview—sign after build, verify before deploy:
# Build phase: sign
build-and-sign:
runs-on: ubuntu-latest
permissions:
id-token: write # Needs OIDC token for signing
contents: read
steps:
- uses: actions/checkout@v4
- name: Build Image
run: docker build -t registry.fortress.local/app:latest .
- name: Sign Image with Cosign
run: |
cosign sign --key env://COSIGN_PRIVATE_KEY \
registry.fortress.local/app:latest
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_KEY }}Verify at deploy time:
# Deploy phase: verify signature
deploy:
runs-on: ubuntu-latest
steps:
- name: Verify Image Signature
run: |
cosign verify --key k8s://fortress-ns/cosign-public-key \
registry.fortress.local/app:latest
- name: Deploy
run: kubectl set image deployment/app app=registry.fortress.local/app:latestIf signature verification fails (image tampered or from unknown source), cosign verify exits with a non-zero code, blocking the deployment pipeline.
Eighth Piece: Policy as Code
You have a patrol team, but the patrol rules are written in a thick paper manual. Every time you need to change patrol routes or inspection standards, you have to reprint the manual and retrain everyone. This is the problem with traditional policy management.
Policy as Code turns "rules" into code, stored in version control, taking effect automatically. In cloud-native environments, the two most popular policy engines are OPA (Open Policy Agent) and Kyverno.
Here's an OPA policy restricting container images to only pull from trusted registries:
# policy/registry.rego — OPA Rego language
package kubernetes.admission
import rego.v1
deny contains msg if {
input.request.kind.kind == "Pod"
image := input.request.object.spec.containers[_].image
not startswith(image, "registry.fortress.local/")
msg := sprintf("Image %v is from an unauthorized registry; only fortress registry images allowed", [image])
}This policy is deployed in Kubernetes' admission controller. Every time a Pod is created, OPA checks—if the image isn't from registry.fortress.local/, it's directly denied.
Kyverno is a more Kubernetes-oriented policy engine (policies are written in YAML, no new language to learn):
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-immutable-image-tag
spec:
validationFailureAction: Enforce # Force block
rules:
- name: disallow-latest-tag
match:
any:
- resources:
kinds:
- Pod
validate:
message: Cannot use latest tag; must specify a concrete version
pattern:
spec:
containers:
- image: "!*:latest"This Kyverno policy forces all Pods to use images with specific version tags (e.g., v1.2.3), not :latest. Once deployed to the cluster, any violating operation is denied before creation.
The Sarif format—you might have noticed format: sarif in the SAST scan above. SARIF (Static Analysis Results Interchange Format) is a standardized scan report format enabling interoperability between security tools:
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "Bandit",
"version": "1.7.5"
}
},
"results": [
{
"ruleId": "B105",
"level": "error",
"message": {
"text": "Hardcoded password detected"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/config.py"
},
"region": {
"startLine": 42
}
}
}
]
}
]
}
]
}Ninth Piece: Shifting Left vs Shifting Right
"Shift Left" means finding and fixing security issues early (left side) in the software development lifecycle. SAST is a classic left-shift—checking while writing code.
"Shift Right" means continuously detecting security threats at runtime (right side). DAST is one form of shifting right; runtime monitoring and fuzz testing also count.
They're not an either-or choice; they're complementary:
Left Right
●─────●─────●─────●─────●─────●─────●─────●
Commit PR Build Test Stage Prod
│ │ │ │ │ │
│ │ │ │ │ └─ Runtime monitoring
│ │ │ │ └─────── Canary release + verification
│ │ │ └───────────── DAST / Penetration testing
│ │ └─────────────────── Image signature verification
│ └───────────────────────── SAST + Code review
└──────────────────────────────── Local Lint + Pre-commit| Strategy | Shift Left | Shift Right |
|---|---|---|
| Discovery timing | While writing code | At runtime |
| Coverage | All code paths | Actual execution paths |
| False positive rate | Higher | Lower |
| Fix cost | Low (change code, immediately verify) | High (needs hotfix, rollback) |
| Typical tools | Bandit, SonarQube | OWASP ZAP, Falco |
| Applicable vulnerabilities | SQL injection, hardcoded keys | Runtime privilege escalation, anomalous behavior |
Your fortress doesn't lack the ability to "find vulnerabilities"; it lacks the ability to "find vulnerabilities at the right time"—both finding them as early as possible (shift left) and continuously monitoring (shift right).
Common Pitfalls
- Only shifting left, not right. Thinking that running SAST in CI is enough. SAST can't see runtime configuration errors, permission bypasses, or dependencies' runtime dynamic behavior. Left and right shifts must be used together.
- Understanding DevSecOps as "buying a tool." Tools are just automation means. DevSecOps' core is culture and process—the development team actively takes responsibility for security. Buying SonarQube but nobody looks at the reports is as good as not buying it.
- Blocking builds on every finding. If the team fights against thousands of alerts every day, they'll learn to find ways to bypass scanning. A more effective strategy: block critical vulnerabilities, only report low-risk findings, and establish an alert reduction plan.
- Exposing secrets in CI logs.
echo $MY_SECRET, printing environment variables in--verbosemode, passing tokens in plaintext in Curl commands—these can all leak secrets in CI output. GitHub Actions and GitLab CI both scan logs for security. - Using
latesttags for container images.latestmeans you never know what version is running. Always use specific version tags (v1.2.3or commit SHA) in production and CI.
Pass Challenges
- Warm-up: Based on your existing (or imagined) project, create a GitHub Actions workflow integrating Bandit (Python) or SpotBugs (Java) SAST scanning. Configure: critical findings block the build, high-severity alerts are uploaded as build artifacts.
- Challenge: Add Trivy container image scanning and cosign container signing to a CI pipeline. Images are automatically signed after build, signatures are verified before deployment, and deployment is blocked if signature verification fails.
- Observe: Use
trufflehogto scan an existing git repository (like your own personal project) and see if you can find sensitive information in historical commits. - Troubleshoot: Your team runs into a problem with SAST in CI—each scan takes 15 minutes, slowing down the development commit flow. You discover it's scanning the entire code instead of just the diff's changed portions. How do you optimize? (Hint: scan only changed files + incremental scanning)
Traveler's Notes
- DevSecOps embeds security into every stage of the CI/CD pipeline, not left as a final gate
- NIST SSDF defines four practice groups for secure software development (PO/PS/PW/RV); DevSecOps is its automation implementation
- SAST finds vulnerabilities at the code stage, SCA checks third-party dependencies for known CVEs, DAST simulates attacks at runtime
- Secrets management is core to DevSecOps: use temporary credentials (OIDC), don't output secrets in logs, principle of least privilege
- Container image signing (cosign) ensures the image you deploy is the one you built, not tampered with
- Policy as Code (OPA/Kyverno) turns security rules into code, automatically enforced before deployment
- Shifting left reduces cost; shifting right catches runtime threats; they are complementary, not alternatives
Next Stop Preview
You've integrated security scanning and policy checks in CI/CD, and the deployed images are also signed. But one problem remains: how do you know which third-party components your software includes? What is the security status of these components? Next chapter, we look at software supply chain security—SBOM, SLSA, and the complete trusted build chain.