跳到内容

元数据卡

  • 前置知识:第7章(SAST/DAST/SCA 概念)、CI/CD 基础知识、容器构建流程
  • 预计时间:50 分钟
  • 完成标志:能描述 DevSecOps 的核心理念,能配置 CI 流水线中的 SAST/DAST 步骤,能解释 Policy as Code 和容器签名的基本原理

你的进度

你在边境要塞已经构建了一套完整的防御体系:加密通信保护信使,证书系统识别友军,SDL 流程把安全需求嵌入到软件开发的每个阶段。

但你很快发现了一个新问题——即使 SDL 明确定义了安全活动,实际执行中,开发团队和安全的"协作"仍然是接力模式:开发写完代码,交给安全团队扫描,发现问题再打回重做。安全始终是流水线的"最后一道关卡",而不是流水线的一部分。

需要解决的矛盾很简单:安全团队只有几个人,开发团队有几十个人。等到发布前一周才来扫漏洞,要么推迟发布日期,要么带着漏洞上线。两者不可接受。

你的任务

把安全嵌入到 CI/CD 流水线的每一步,而不是留在最后。你将理解 DevSecOps 的理念——为什么安全要"左移",以及在哪些场景下也要"右移"。你将配置 SAST 扫描、秘密检测、容器签名和策略即代码(Policy as Code),让每次代码提交都自动通过安全门。

本章分层

  • 必读:DevSecOps 核心理念、CI 中集成 SAST/SCA、秘密管理、容器签名
  • 选读:Policy as Code(OPA / Kyverno)
  • 进阶:Supply-chain Levels for Software Artifacts (SLSA) 与构建完整性

破局 · 溯源

问题:下个月要塞要启用新的补给系统。开发团队已经写了一千多行 Python 代码,现在安全团队在发布前审计,发现了一个硬编码的 API 密钥、三个未修复的 CVE 依赖、一个允许未认证访问的端点。

修复需要两周,但发布计划定在下周一。

这就是"安全做在最后"的典型后果。安全发现得太晚——修正成本从"改一行代码"变成了"推迟整个发布计划"。更糟的是,如果每次都这样,团队会形成习惯:绕开安全审计直接上线。

你需要的不是"安全门"而在最后一道门,而是"安全闸"嵌入到每一段路程中——每次代码提交都自动通过安全检查,而不是发布前集中扫一次。

第一块:DevSecOps 的理念

DevSecOps 不是一种工具,而是把安全整合进 DevOps 文化、实践和自动化流水线的方法论。核心思想其实只有一句话:

安全是开发团队的日常职责,不是安全团队的唯一职责。

传统模式下,安全团队承担了所有安全责任,开发团队只负责"写功能代码"。DevSecOps 反过来:开发团队在写代码时就考虑安全性,安全团队负责提供工具、策略和培训。

传统模式(最左到最右):

    开发 ──→ 测试 ──→ 安全扫描 ──→ 发布

                           └── "发现漏洞,打回给开发重做"

DevSecOps 模式:

     左移:安全嵌入每一步

     ├─ Commit → SAST + 依赖检查 + 秘密检测
     ├─ PR/MR → 代码审查 + 容器扫描
     ├─ Test → DAST + Fuzz 测试
     ├─ Build → 镜像签名 + SBOM 生成
     └─ Deploy → 策略验证 + 运行时监控

第二块:NIST SSDF(Secure Software Development Framework)

美国国家标准与技术研究院(NIST)在 2021 年发布了 SP 800-218,即 SSDF 框架。它定义了一套安全软件开发的最佳实践,分为四个核心实践组:

编号实践组核心活动
PO准备组织(Prepare Organization)定义安全角色、培训、配置管理
PS保护软件(Protect Software)组件安全、代码完整性、安全编码
PW生产发布(Produce Well-Secured Software)威胁建模、安全测试、漏洞扫描
RV响应(Respond to Vulnerabilities)漏洞发现和修复、安全公告

SSDF 并不规定具体工具,而是定义"你应该做什么"。DevSecOps 是实现 SSDF 建议的操作手段——在 CI/CD 中,你通过自动化把 PS 和 PW 落地。

SSDF 中的关键活动(PS.2、PW.4、PW.6)正好映射到 DevSecOps 的自动化门禁:

PS.2(保护组件安全) → SCA 依赖检查
PW.4(审查代码安全) → SAST 静态分析
PW.6(测试运行安全) → DAST 动态测试

第三块:SAST 在 CI 中的集成

GitHub Actions 工作流中添加安全扫描步骤。每次开发者的代码提交经过 CI 时,自动启动安全扫描——就像补给车经过哨站时自动被检查,不需要专门派人盯着:

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  # 不阻止构建,但生成报告

      - 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 "发现 $CRITICAL 个严重漏洞,阻断构建"
            exit 1
          fi

关键设计决策:

策略说明适用场景
continue-on-error: true扫描结果不阻止构建遗留代码库,先建立基线
阻断 CRITICAL只有严重漏洞才阻止已经有一定安全基础的团队
阻断 ALL 发现任何发现都阻止绿地基代码库
仅报告,不阻断完全不影响流水线刚开始集成时

第三块:DAST 在 CI 中的集成

DAST 更像一个巡逻队,在你已有战报系统的前提下,从外部模拟攻击者来试探防御。

GitLab CI 中集成 OWASP ZAP:

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 一般不在每次提交都运行(因为时间太长),而是在:

触发时机频率说明
每次 PR/MR可选如果项目小且测试环境随时准备好
每日定时推荐确保主分支始终通过安全检查
发布前必须版本发布前最后一道安全测试
基础设施变更后按需代理、防火墙、网络配置修改后

第四块:SCA(依赖扫描)的安全嵌入

你的要塞不可能自己锻造每一把武器。你从外部军械库采购装备,但你得确保这些装备没有被动手脚。

现代项目中 90% 以上的代码来自第三方依赖。一个安全漏洞可能藏在某个深层依赖中——你直接依赖的库是安全的,但它依赖的一个工具包有 CVE。

Trivy 是目前最受欢迎的容器和文件系统扫描器:

yaml
# docker-compose 集成 trivy 扫描
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 是关键的阻断策略——如果发现了严重或高危漏洞,Trivy 会以非零退出码退出,让 CI 流水线失败。

yaml
# GitHub Actions 中集成 Trivy 扫描 Docker 镜像
- 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  # 发现漏洞就失败

第五块:秘密管理(Secrets Management in CI/CD)

最尴尬的漏洞不是 SQL 注入,而是某个开发者在代码里硬编码了数据库密码,然后不小心提交到了公开仓库。

GitHub 的 secret scanning 是内置的,但你自己也可以配置流水线来检测秘密泄露:

yaml
# 用 truffleHog 在 CI 中检测硬编码秘密
secrets-scan:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # 检查整个 git 历史

    - name: Scan for secrets
      uses: trufflesecurity/trufflehog@main
      with:
        extra_args: --results=verified,unknown

更关键的是——不要在 CI 日志里打印秘密。一个常见的错误是调试时输出环境变量,结果密码就暴露在了 CI 的日志面板里:

yaml
# 危险:日志中会暴露
- name: Configure AWS
  run: |
    echo "Setting up AWS..."
    aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}

# 安全:避免日志输出
- 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 }}

在 CI 中使用秘密的原则:

原则说明实践
最小化只提供必要权限只用 READ 权限的 token,不是 admin
临时性每次构建使用临时凭据OIDC 临时 token,不是长期 API key
可轮换秘密必须能随时替换Vault / AWS Secrets Manager
可审计每一次秘密访问都有记录CI 日志 + 审计追踪
不传递不在日志、构建产物、镜像层中留下痕迹--no-cache 构建,日志脱敏

使用 OIDC(OpenID Connect)避免长期凭据:

yaml
# 用 GitHub OIDC 代替长期 AWS 密钥
- 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 }}

这样每次构建时,GitHub Actions 从 AWS 获取一个临时令牌,有效期只有几小时。即使泄露了,令牌过期就自动失效。

第六块:容器镜像签名和验证

你的要塞收到了一箱军火,你怎么确认这箱军火确实是军需官发的,而不是敌人伪造的?

答案是签名和验证——就像你在要塞大门检查官文书信一样。

容器镜像签名用 Sigstore / cosign(将在第10章详细展开)。这里先看一小段集成——在构建后签名,在部署前验证:

yaml
# 构建阶段:签名
build-and-sign:
  runs-on: ubuntu-latest
  permissions:
    id-token: write  # 需要 OIDC token 来签名
    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 }}

部署时验证:

yaml
# 部署阶段:验证签名
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

如果签名验证失败(镜像被篡改或来自未知来源),cosign verify 会以非零退出码退出,部署流水线就此阻断。

第七块:Policy as Code(策略即代码)

你有一支巡逻队,但巡逻规则写在一本厚厚的纸质手册里。每次要更改巡逻路线或检查标准,你得重印手册、重新培训。这就是传统策略管理的问题。

Policy as Code 把"规则"变成代码,存放在版本控制系统中,自动生效。在云原生环境中,两个最流行的策略引擎是 OPA(Open Policy Agent)和 Kyverno。

写一段 OPA 策略,限制容器镜像只能从你信任的仓库拉取:

rego
# policy/registry.rego — OPA Rego 语言
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("镜像 %v 来自未授权仓库,只能使用 fortress 注册表中的镜像", [image])
}

这段策略部署在 Kubernetes 的准入控制器中。每次创建 Pod 时,OPA 都会检查——如果镜像不是来自 registry.fortress.local/,直接拒绝创建。

Kyverno 是一个更面向 Kubernetes 的策略引擎(策略用 YAML 写,不用学新语言):

yaml
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-immutable-image-tag
spec:
  validationFailureAction: Enforce  # 强制阻断
  rules:
  - name: disallow-latest-tag
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: 不能使用 latest 标签,必须指定具体版本
      pattern:
        spec:
          containers:
          - image: "!*:latest"

这段 Kyverno 策略强制要求所有 Pod 使用的镜像必须有具体版本标签(v1.2.3),不能用 :latest。部署到集群后,任何违反规则的操作都会在创建前被拒绝。

Sarif 格式——前面 SAST 扫描时你可能注意到了 format: sarif。SARIF(Static Analysis Results Interchange Format)是一种标准化的扫描报告格式,让安全工具之间可以互操作:

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": "检测到硬编码密码"
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "src/config.py"
                },
                "region": {
                  "startLine": 42
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

第八块:Shifting Left vs Shifting Right

"左移"(Shift Left)的意思是:在软件开发生命周期的早期(左侧)发现并修复安全问题。SAST 是左移的典型——在写代码时就检查。

"右移"(Shift Right)的意思是:在运行时(右侧)持续检测安全威胁。DAST 是右移的一种,运行时监控和模糊测试也是。

两者不是二选一,而是互补:

左移                               右移
●─────●─────●─────●─────●─────●─────●
Commit  PR   Build  Test  Stage Prod
 │      │     │      │     │     │
 │      │     │      │     │     └─ 运行时监控
 │      │     │      │     └─────── 金丝雀发布 + 验证
 │      │     │      └───────────── DAST / 渗透测试
 │      │     └─────────────────── 镜像签名验证
 │      └───────────────────────── SAST + 代码审查
 └──────────────────────────────── 本地 Lint + Pre-commit
策略左移右移
发现时机写代码时运行时
覆盖范围所有代码路径实际执行路径
误报率较高较低
修复成本低(改代码立刻验证)高(需要热修复、回滚)
典型工具Bandit, SonarQubeOWASP ZAP, Falco
适用漏洞SQL 注入、硬编码密钥运行时权限提升、异常行为

你的要塞不缺"发现漏洞"的能力,缺的是"在正确的时间发现漏洞"——既能尽快发现(左移),也能持续监控(右移)。


常见陷阱

  • 只左移,不右移。 以为只要 CI 里跑了 SAST 就万事大吉。SAST 看不到运行时配置错误、权限绕过、依赖在运行时的动态行为。左移和右移必须组合使用。
  • 把 DevSecOps 理解成"买一个工具"。 工具只是自动化手段。DevSecOps 的核心是文化和流程——开发团队主动为安全负责。买了 SonarQube 但没人查看报告,等于没买。
  • 所有发现都阻断构建。 如果团队每天都在跟大量告警条对抗,他们会学会想办法绕过扫描。更有效的策略:严重漏洞阻断、低风险告警只报告、建立告警瘦身计划。
  • 在 CI 日志里暴露秘密。 echo $MY_SECRET--verbose 模式下打印环境变量、Curl 命令中明文传递 token——这些都可能在 CI 输出中泄露秘密。GitHub Actions、GitLab CI 都对日志进行安全扫描。
  • 容器镜像的 latest 标签。 latest 让你永远不知道跑的是什么版本。在生产和 CI 中始终使用具体版本标签(v1.2.3 或 commit SHA)。

通关挑战

  • 热身:基于你现有的(或者想象的一个)项目,创建一个 GitHub Actions 工作流,集成 Bandit(Python)或 SpotBugs(Java)的 SAST 扫描,配置为:严重漏洞阻断构建,高危告警上传为构建产物。
  • 挑战:在 CI 流水线中增加 Trivy 容器镜像扫描和 cosign 容器签名。镜像构建后自动签名,部署前验证签名,签名不通过不部署。
  • 观察:用 trufflehog 扫描一个现有的 git 仓库(比如你自己的个人项目),看看是否能在历史提交中找到敏感信息。
  • 排障:你的团队在 CI 中跑 SAST 遇到一个问题——扫描每次要 15 分钟,开发提交流程变慢。你发现扫描的是全部的代码,而不是 diff 中变更的部分。如何优化?(提示:只扫描变更文件 + 增量扫描)

旅人笔记

  • DevSecOps 把安全嵌入到 CI/CD 流水线的每个阶段,而不是放在最后作为门禁
  • NIST SSDF 定义了安全软件开发的四组实践(PO/PS/PW/RV),DevSecOps 是它的自动化实现
  • SAST 在代码阶段发现漏洞,SCA 检查第三方依赖的已知 CVE,DAST 在运行时模拟攻击
  • 秘密管理是 DevSecOps 的核心:使用临时凭据(OIDC)、不在日志输出秘密、最小权限原则
  • 容器镜像签名(cosign)保证你部署的镜像是你构建的,未被篡改
  • Policy as Code(OPA/Kyverno)把安全规则变成代码,自动在部署前执行
  • 左移降低成本,右移捕获运行时威胁;两者互补而非替代

下一站预告

你在 CI/CD 中集成了安全扫描和策略检查,流出了中部署的镜像也被签了名。但有一个问题没解决:你如何知道你的软件里包含了哪些第三方组件?这些组件的安全状态如何?下一章,我们看软件供应链安全——SBOM、SLSA 和完整的可信构建链。

Built with VitePress | Software Systems Atlas