Skip to content

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 monitoring

Second 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:

IDPractice GroupCore Activities
POPrepare OrganizationDefine security roles, training, configuration management
PSProtect SoftwareComponent security, code integrity, secure coding
PWProduce Well-Secured SoftwareThreat modeling, security testing, vulnerability scanning
RVRespond to VulnerabilitiesVulnerability 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 testing

Third 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:

yaml
# .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
          fi

Key design decisions:

StrategyDescriptionApplicable Scenario
continue-on-error: trueScan results don't block buildLegacy codebase, first establish baseline
Block CRITICALOnly critical vulnerabilities blockTeam with some security foundation
Block ALL findingsAny finding blocksGreenfield codebase
Report only, don't blockCompletely non-blockingInitial 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:

yaml
# .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_requests

DAST generally doesn't run on every commit (because it takes too long), but at:

TriggerFrequencyDescription
Every PR/MROptionalIf project is small and test environment is always prepared
Daily scheduledRecommendedEnsure main branch always passes security checks
Before releaseMandatoryFinal security test before release
After infrastructure changesOn-demandAfter 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:

yaml
# 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.

yaml
# 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 findings

Sixth 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:

yaml
# 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,unknown

More 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:

yaml
# 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:

PrincipleDescriptionPractice
MinimizeGrant only necessary permissionsUse READ-only tokens, not admin
EphemeralUse temporary credentials per buildOIDC temporary tokens, not long-term API keys
RotatableSecrets must be replaceable at any timeVault / AWS Secrets Manager
AuditableEvery secret access is recordedCI logs + audit trail
Non-transmissibleDon't leave traces in logs, build artifacts, image layers--no-cache builds, log redaction

Using OIDC (OpenID Connect) to avoid long-term credentials:

yaml
# 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:

yaml
# 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:

yaml
# 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:latest

If 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:

rego
# 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):

yaml
# 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:

json
{
  "$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
StrategyShift LeftShift Right
Discovery timingWhile writing codeAt runtime
CoverageAll code pathsActual execution paths
False positive rateHigherLower
Fix costLow (change code, immediately verify)High (needs hotfix, rollback)
Typical toolsBandit, SonarQubeOWASP ZAP, Falco
Applicable vulnerabilitiesSQL injection, hardcoded keysRuntime 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 --verbose mode, 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 latest tags for container images. latest means you never know what version is running. Always use specific version tags (v1.2.3 or 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 trufflehog to 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.

Built with VitePress | Software Systems Atlas