DevSecOps 落地失败最常见的样子,不是工具没买,是卡点设成了"必须清零才能上线"。
第一周:安全团队很兴奋,SCA、SAST、镜像扫描全接进流水线,门禁设成高危必须修完。
第二周:研发发现一个项目要修 200 个依赖漏洞才能发版,直接找领导投诉"安全在卡业务"。
第三周:门禁被改成"仅告警",从此再没人看。
第四周:项目正式宣告"已完成 DevSecOps 建设",实际上什么都没拦住。
这篇讲的是怎么避免走到第四周。核心是三件事:卡点插在哪、门禁分几档、误报怎么治。工具选型只占一小节,因为那是最不重要的部分。
如果你更关心漏洞从发现到修完的流程和度量,可以看 漏洞管理闭环怎么建;这篇讲的是"怎么在代码交上去的那一刻就拦住"。
一、DevSecOps 先算一笔账:你要挡的是哪一类问题
在设卡点之前,先看历史数据:过去一年出过的线上安全问题,分别是什么类型、在哪一环节能拦住。
大部分团队的答案是这三类占大头:
| 问题类型 | 典型例子 | 能在哪拦住 |
|---|---|---|
| 依赖组件漏洞 | Log4j、Fastjson、老旧版本的 Spring | 构建时(SCA) |
| 凭据泄漏 | 代码里硬编码的 AK/SK、数据库口令 | 提交时(密钥扫描) |
| 配置错误 | 镜像用 latest、容器 root 运行、安全组开 0.0.0.0/0 | 构建/部署时(IaC 与镜像扫描) |
这三类的共同点是能用工具明确判定,误报低,所以适合做硬门禁。而 SQL 注入、越权这类业务逻辑漏洞,SAST 误报率高,只适合做提示。
这个区分很关键:门禁只设在你敢让它阻断发布的地方。设一个你自己都不敢强推的规则,最后一定是整条门禁被关掉。
二、五个卡点,插在哪里
流水线的前后顺序是固定:提交 → 构建 → 镜像 → 部署 → 上线后。每段能查的东西不一样,成本也不一样——越靠左越便宜,越靠右越贵。
| 卡点 | 查什么 | 耗时 | 建议动作 |
|---|---|---|---|
| 1. 提交前(pre-commit / pre-receive) | 密钥、证书、私钥、大文件 | 秒级 | 阻断 |
| 2. 构建时 | 依赖组件漏洞(SCA)、IaC 配置 | 1~3 分钟 | 分级阻断 |
| 3. 镜像构建后 | 基础镜像漏洞、是否 root、是否 latest | 1~2 分钟 | 分级阻断 |
| 4. 部署前 | DAST 对预发环境扫一遍、配置基线 | 10~30 分钟 | 仅高危阻断 |
| 5. 上线后 | 运行时检测、RASP、日志告警 | 持续 | 告警 |
2.1 卡点一:提交前挡密钥
这是投入产出比最高的一个。凭据一旦进了 Git 历史,即使后来删掉,历史里还在,必须 rewrite 历史或者轮换密钥。
用 gitleaks 或 trufflehog,在 pre-receive hook 或 CI 的第一秒跑:
# GitLab CI:密钥扫描,任何命中都阻断(这类误报极少,可以硬挡) secret-scan: stage: .pre image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --verbose --redact --exit-code 1 allow_failure: false rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
注意--redact:不要把扫出来的密钥原文打到日志里,那等于二次泄露。
这一档可以设成硬阻断,因为真正的密钥格式很明确(AK 长度、前缀、私钥头),误报很少。配合一份.gitleaksignore(放测试用的假密钥)就能稳定运行。
2.2 卡点二:构建时挡依赖漏洞
SCA 是 DevSecOps 的核心,也是误报最多的地方。策略必须分级:
- 有公开 EXP 且 CVSS ≥ 7:阻断
- CVSS ≥ 7 但需要特定条件:告警 + 要求登记例外
- CVSS < 7:只记录,进月度报告
不要按"存在任何高危就阻断"。一个正常的企业 Java 项目,依赖树里躺着几十个高危是常态——大部分是传递依赖、不在实际调用路径上。
# 依赖扫描:只对"可利用性高"的等级阻断,其余出报告 sca-scan: stage: build image: aquasec/trivy:latest script: - trivy fs --scanners vuln --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed --format table --output sca-report.txt . artifacts: when: always paths: [sca-report.txt] allow_failure: true # 先设 true,稳定两周后再按情况改
--ignore-unfixed这个参数很重要:没有修复版本的漏洞,挡了也没用,只会让研发去研究怎么绕过门禁。这类漏洞应该进"接受风险"清单,靠别的手段缓解(WAF 规则、网络隔离)。
allow_failure: true是我建议的起步姿势:先跑两周只看不挡,把误报摸清楚,再挑最稳的规则改成false。一次只收紧一条规则,出问题能立刻定位。
2.3 卡点三:镜像构建后
容器场景下,这一档比 SCA 更容易见效,因为基础镜像的问题是确定的:
image-scan: stage: package script: # 三件事一起查:漏洞、是否 root、是否用了 latest - trivy image --severity CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - | USER_LINE=$(docker inspect $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \ --format '{{.Config.User}}') if [ -z "$USER_LINE" ] || [ "$USER_LINE" = "root" ] || [ "$USER_LINE" = "0" ]; then echo "镜像以 root 运行,阻断"; exit 1 fi allow_failure: false"镜像以 root 运行"这一条误报几乎为零、修起来也简单(Dockerfile 加一行USER),非常适合做硬门禁。基础镜像漏洞则要分级——用一个打满补丁的基础镜像(如 distroless 或定期重建的 alpine),可以让这一档的噪音降一个数量级。
2.4 卡点四:部署前 DAST
DAST 的问题是慢(扫一遍几十分钟),而且需要可用环境。建议只在两个时机跑:预发环境部署后、上线前的 MR 里,且只对高危阻断。
一个实用技巧:只扫这次变更涉及的接口。全量扫一遍的价值低于持续扫描,而增量扫能把时间压到几分钟,研发才愿意等。
2.5 卡点五:上线后
这一档不是门禁,是兜底。运行时检测(RASP、EDR、WAF 日志)能发现前四档漏掉的东西,尤其是逻辑漏洞和业务欺诈。
关键是把线上发现的真实问题反哺回卡点:如果一类问题反复从线上冒出来,说明前面的卡点没覆盖,要么补规则,要么补卡点。
三、CI/CD 门禁三档策略:从"仅告警"到"真阻断"
这是整套方案能不能落地的核心。我的做法是三档,按项目类型分开:
| 档位 | 适用范围 | 策略 | 什么时候升级 |
|---|---|---|---|
| 观察档 | 所有存量项目第一周 | 只记录,出报告,不阻断 | 跑满两周、误报摸清后 |
| 分级阻断 | 存量项目稳定后、新项目 | 高危+可利用 阻断,其余告警 | 误报率 < 10% 后 |
| 严格阻断 | 核心系统、对外业务 | 高危全阻断,中危需登记例外 | 团队已习惯后 |
存量项目和新项目必须分开,这是最多人忽略的一点。
存量项目一上来就阻断,等于让业务停摆——它的依赖可能三年前的、镜像可能是 alpine:3.8。正确做法是先做一次全量扫描,把结果整理成"欠账清单",给三个月清理期(期间只告警),三个月后再启用阻断。
新项目可以直接上分级阻断,因为从第一行代码开始就没有历史包袱,修起来也便宜。
这个区分要写进制度,并且明确告诉业务方:"老系统给三个月,新系统从今天起"——这句话比任何技术方案都重要,因为它决定了研发是配合你还是绕过你。
四、和研发怎么谈:先把"卡"字去掉
技术方案做得再好,研发不配合就是零。而研发抵触的根因通常不是"不想安全",是你增加了他的工作量,但没有减少他的风险。
谈的时候有三句话要说到位:
第一句:我不在发版当天拦你。承诺"上线窗口内不新增阻断规则",这一句能消掉大部分对抗。发版当天加规则,是安全团队最容易犯的错——它把安全和"延期"直接绑在一起,之后所有沟通都会变成博弈。
第二句:这些问题你改起来比线上修便宜。举具体例子:一个依赖升级在提交时改是十分钟,上线后发现要重新走发布流程是两天。这个对比要说数字,不说概念。
第三句:我给的是可执行的修复建议,不是一份报告。告警里带"升级到 2.17.1 即可,该版本无破坏性变更",研发会愿意改;只写"CVE-2021-44228",他会去搜,搜完发现要改一堆东西,就搁置了。
反过来,有三件事不要做:不要在群里公开点名某个项目漏洞多(会让人想办法隐藏而不是修)、不要拿"等保要求"当唯一理由(研发对合规无感)、不要承诺"以后就不会再扫出问题"(做不到)。
五、误报:推行失败的头号原因
工具误报率高,研发就会开始怀疑所有告警,最后连真的也不看了。治理误报有三个动作,缺一不可。
动作一:每个误报都要有出口。不能只有"修"和"忽略"两个选项。要有第三种:登记例外——记录原因、责任人、复查日期,进例外清单,到期自动重扫。
# 例外文件示例:sca-exceptions.yml(放在仓库里,随代码评审) exceptions: - id: CVE-2023-12345 component: log4j-core:2.14.1 reason: 该模块未被调用,仅作日志格式化使用,已确认无 JNDI 解析路径 owner: 张三 expires: 2027-01-31
放在仓库里、随代码评审,有两个好处:例外是被审过的(不是安全团队一个人偷偷放过),而且有到期日(不会永久留着)。
动作二:定期看"误报率最高的规则"。每月统计一次哪条规则产生的告警最多、其中多少是真问题。占比低于 20% 的规则,要么调参、要么降级为提示。
动作三:让研发能自己判断是不是真问题。告警信息里必须带三样:位置(哪个文件哪一行/哪个依赖)、复现方式、影响说明。只写"CVE-2023-12345 高危",研发的第一反应就是"又来了"。
这三件事里,最容易被跳过的是第三件。它不需要写代码,只需要在工具配置里把模板改一下,但它对研发的观感影响最大——一份能看懂的报告和一份看不懂的报告,处理意愿差好几倍。
六、工具链:四个必需,其余可选
不要一开始就上全套。四个必需的工具,覆盖了 80% 的实际收益:
| 工具类型 | 作用 | 开源可选项 |
|---|---|---|
| 密钥扫描 | 挡凭据进仓库 | gitleaks、trufflehog |
| SCA(依赖) | 挡组件漏洞 | Trivy、Dependency-Check、Syft+Grype |
| 镜像扫描 | 挡基础镜像问题 | Trivy、Clair |
| IaC 扫描 | 挡配置错误 | Checkov、tfsec、kube-linter |
其余的先不上:SAST(误报高,需要专人调规则)、DAST(慢,需要环境)、RASP(要改运行时)。这些等前四个稳定运行三个月、团队有余力再考虑。
工具链有一个隐性成本经常被低估:结果要汇总到一处。四个工具四份报告,没人会看。至少要让它们输出统一格式(SARIF 是通用格式),汇总到同一个地方,才能看到"这个项目现在一共几个待修"。
七、度量:三个数就够
DevSecOps 的度量容易做成"扫了多少次、发现了多少问题",这类数字只证明工具在跑,不证明有效。
真正有意义的是三个:
| 指标 | 定义 | 说明 |
|---|---|---|
| 逃逸率 | 上线后发现的问题数 / 上线前发现的问题数 | 低于 10% 说明卡点有效 |
| 门禁通过率 | 首次提交就通过门禁的比例 | 逐步提升说明研发在改习惯,而不是在绕过 |
| 例外清偿率 | 到期例外已关闭的比例 | 低于 80% 说明例外在变成永久豁免 |
逃逸率是最硬的一个。它直接回答"你的卡点到底拦住了没有"。统计方法是从线上事件和漏扫结果里反查:这个问题本来应该在哪一档被拦住、为什么没拦住。
门禁通过率是过程指标。如果它长期低于 50%,说明门禁设得太严或者提示不清楚;如果它一直是 100%,说明门禁形同虚设。
八、推进节奏:三个季度
| 阶段 | 时间 | 做什么 | 验收 |
|---|---|---|---|
| 一期 | Q1 | 密钥扫描 + SCA 接入,全部观察档 | 覆盖全部活跃仓库;误报清单成型 |
| 二期 | Q2 | 核心项目转分级阻断;镜像扫描接入 | 核心项目逃逸率有基线数据 |
| 三期 | Q3 | 存量项目清理期结束转阻断;例外机制运转 | 例外清偿率 ≥80% |
一期只做"接入",不做"阻断",这一点要顶住压力。很多项目死在一期就想见效,结果门禁被投诉到关掉,后面再想推就难了。
九、几个具体的坑
坑一:只接新项目。新项目只占代码量的一小部分,风险最大的是那些跑了五年的老系统。老项目可以用"观察档 + 欠账清单"的方式先覆盖,不阻断但要看得见。
坑二:把 SAST 设成阻断。SAST 误报率高是行业共识,设成阻断一定会被绕过。它应该只做提示,配合人工审计。
坑三:门禁信息不落到研发日常用的地方。报告放在安全平台没人看。要把结果推到 MR 评论、Jira 工单里,研发在自己的工作流里就能看到。
坑四:不处理"历史欠账"。一上来几千个问题,研发直接放弃。正确做法是只对新增代码生效(增量扫描),历史问题单独列清单排期。这一条能让落地难度下降一个数量级。
坑五:安全团队自己调规则,不问研发。规则调得对不对,研发最有发言权——他们知道哪些告警是误报。让他们参与调规则,接受度会完全不同。
十、写在最后
DevSecOps 的本质不是"在流水线上加几步扫描",是把安全的判断标准变成研发能自己执行的规则。
衡量成功的标志不是工具接了几个,是:研发在提交代码前会自己想"这段会不会被扫出密钥",上线前会自己看一眼依赖报告。到这一步,安全才真正左移了——不是把安全团队移到左边,是把判断能力交给写代码的人。