那年我们团队接手了一个金融风控系统的重构项目,当时流水线里到处都是硬编码的云凭证。我打开Jenkins的凭据管理页面,看到里面躺着二十多个AWS Access Key和Docker Hub密码,有些甚至三年没换过。这种管理方式让我整晚睡不着觉——只要任何一个开发者的笔记本被攻破,或者某个离职员工的权限没回收,整个生产环境就暴露在风险中。
我们决定彻底改造这个架构。传统的静态凭证方案最大的问题是生命周期管理困难。比如我们之前用的AWS密钥,按照合规要求每90天必须轮换,但每次轮换都要手动更新十几条流水线配置,运维同学怨声载道。更糟糕的是,这些密钥在流水线日志里偶尔会明文出现,有次我排查构建失败时,在日志里直接看到了数据库密码。
转向OIDC(OpenID Connect)联邦身份验证是个转折点。这个方案的核心逻辑是让CI/CD系统直接通过云厂商的身份提供商获取临时凭证,而不是预先配置长期密钥。我们在GitLab CI/CD v17.5上做了第一个试点,因为GitLab对OIDC的支持已经相当成熟。
具体落地时,我先在AWS IAM里配置了一个身份提供商,指向我们的GitLab实例。然后定义了一个IAM角色,信任策略里限定只有特定项目的特定分支才能代入这个角色。这样流水线运行时,GitLab会自动向AWS请求临时凭证,有效期只有15分钟,用完即废。
这里有一个关键细节:很多团队只配置了仓库级别的信任关系,这还不够。我们进一步限制了branch和tag的匹配规则,防止有人通过fork仓库恶意触发生产部署。下面是我们实际使用的信任策略配置片段:
在流水线代码里,获取凭证的方式变得极其简洁。以前我们要从Vault拉取密钥,现在直接让工具链自动处理。这是我们在GitLab CI里配置AWS OIDC的实例:
第一次跑通这个流程时,我盯着日志里AssumedRoleUser的输出看了很久。那种不再依赖长期密钥的安全感,比任何扫描报告都让人踏实。不过OIDC也不是银弹,我们在实施中遇到过身份映射混乱的问题。有次预发环境部署失败,排查发现是GitLab Runner的版本太旧(14.x),不支持最新的OIDC声明格式,升级到GitLab Runner 16.8才解决。
现在回过头看,零密钥架构最明显的改变是应急响应速度。上个月有个AWS密钥疑似泄露,如果是以前,我们要花两小时轮换所有相关凭证,现在只需要吊销那个IAM角色,流水线自动获取新凭证,全程不影响业务。
2021年12月那个周五下午,我正在和团队讨论双十二大促的保障方案,安全部门的紧急工单直接弹到了我屏幕上:Log4j漏洞(CVE-2021-44228)在我们的商品推荐服务里被检测到。当时距离大促只有10天,那个服务每天处理着超过2000万次请求,一旦被利用,攻击者可以直接在服务器上执行代码。
我们第一反应是升级依赖,但问题远比想象中复杂。那个推荐服务依赖了37个内部库和上百个第三方包,光是梳理哪些组件间接引用了Log4j就花了我们整整一个通宵。我记得特别清楚,当时用mvn dependency:tree查出来的依赖路径有六层深,有个叫elasticsearch-rest-client的包悄悄传递了漏洞版本,而我们之前根本没注意到这个链路。
这次事件暴露了我们供应链管理的致命缺陷:我们对自己的软件成分一无所知。应急响应结束后,我推动团队必须落地SBOM(软件物料清单)。这不是为了应付合规,而是下次再遇到类似情况时,我们能在一分钟内知道哪些服务受影响。
我们选择了CycloneDX格式,因为它对Java生态支持最好,而且能和现有的Maven/Gradle构建链集成。在Jenkins 2.479的流水线里,我们加入了SBOM生成步骤。下面是我们实际使用的Maven插件配置:
生成的SBOM文件我们直接存档到制品库,同时推送到一个中央SBOM管理平台。这里有个实战经验:SBOM不是生成完就结束了,必须建立查询机制。我们写了一个简单的Python脚本,当新的漏洞预警出来时,自动扫描所有历史SBOM:
去年另一个供应链事件(Spring Cloud Gateway漏洞)爆发时,这个机制真的救了我们。安全团队早上9点发出预警,我们9点15分就定位到了三个受影响的服务,其中两个甚至已经不在活跃维护列表里,要不是SBOM,根本不会有人想到去检查它们。
SBOM落地过程中最大的阻力来自开发效率。有段时间流水线平均增加了40秒的SBOM生成时间,开发同学抱怨构建变慢。我们后来优化了策略:只在合并到主分支时生成完整SBOM,开发分支只做增量检查。同时把SBOM生成移到构建后期,避免因为SBOM问题导致主构建失败。
现在我们的SBOM已经细化到每个容器镜像层。在Argo CD v2.12的部署流程里,我们通过Kyverno策略强制要求所有镜像必须附带SBOM,否则拒绝部署。这种供应链透明度的提升,让我们在面对SLSA L3级别认证时,比预期顺利得多。
去年我们给一个微服务架构的订单系统接入安全扫描时,遇到了典型的效率矛盾。流水线原本平均8分钟就能完成构建部署,加入SAST和DAST后,直接飙升到23分钟。开发团队开始绕过安全扫描,直接在流水线配置里注释掉扫描步骤,这比没有安全扫描更可怕。
我决定对扫描流程做一次彻底的优化。先说SAST(静态应用安全测试),我们用的是SonarQube配合Checkmarx。最初配置是扫描所有代码,包括第三方库和测试代码,这显然不合理。我做了个实验:分别扫描完整代码库和仅扫描业务代码,结果差异惊人。
测试对象是订单服务的代码库,总共12万行Java代码。全量扫描耗时14分32秒,内存占用峰值达到3.2GB。而当我们排除掉target/目录、测试代码和com.thirdparty包后,扫描时间降到4分18秒,内存占用只有1.1GB。下面是我们优化后的SonarQube扫描配置:
DAST(动态应用安全测试)的优化空间更大。最初我们用OWASP ZAP做完整扫描,对一个有50个API端点的服务,扫描耗时19分钟。分析发现,ZAP默认会爬取所有页面并测试所有参数,但我们的微服务其实只有JSON接口,没有传统网页。
我调整了DAST策略:只扫描关键API路径,并且预定义好接口规范。我们写了一个OpenAPI到ZAP导入的脚本,让扫描器直接针对已知端点测试,而不是盲目爬取。优化后的扫描时间降到了6分42秒,漏洞检出率却提高了15%,因为扫描器能更专注地测试每个参数。
这里有个重要的取舍:我们最终决定把DAST从每次提交都执行,改为每天定时运行一次。因为DAST发现的问题通常是配置类或运行时问题,不会随着每次代码提交而改变。而SAST保持每次提交都运行,但只扫描变更文件。
为了验证优化效果,我记录了优化前后一个月的数据。优化前,流水线平均耗时21.3分钟,开发同学每天因为等待构建浪费约47分钟。优化后,平均耗时降到9.8分钟,安全扫描的通过率从68%提升到92%(因为减少了误报)。最关键的是,再没有开发同学主动跳过安全扫描步骤。
不过这种优化不是一劳永逸的。上个月我们引入了一个新支付SDK,它的依赖树特别复杂,导致SCA(软件成分分析)扫描时间突然增加了3分钟。我不得不再次调整策略,把SCA扫描移到夜间批量执行,白天只做增量SCA检查。这种持续平衡效率与安全的博弈,大概就是流水线维护的常态。
去年我负责一个金融级支付系统的改造,客户明确要求流水线必须满足等保三级的隔离要求。当时我们在 GitLab CI (v17.5) 和 GitHub Actions (2024.10) 之间做了长达两周的 PoC 测试。我得出的结论是:如果你追求极致的 Runner 隔离和自托管控制权,GitLab 的架构更底层;如果你追求 SaaS 集成的便捷性和 OIDC 身份联邦的现代化,GitHub Actions 则更胜一筹。
GitLab CI 的 Runner 采用的是一种轮询拉取(Pull)模式。Runner 主动去 GitLab 实例拉取 Job,这意味着 Runner 可以部署在完全隔离的内网环境中,只要它能出网访问 GitLab 即可。我在这个支付项目中,为了彻底隔离生产凭证,部署了 GitLab Runner 17.5 版本,并配置了 --docker-runtime runc --privileged=false。
原因在于,GitLab Runner 的 Executor 机制非常成熟。我使用的是 docker executor,并强制开启了 --docker-isolation 选项。这确保了每个 Job 的容器文件系统是独立的,且通过 Linux Namespace 隔离。如果不这么做,同一个 Runner 上的两个并发 Job 可能会通过共享的 /tmp 目录泄露中间产物。
相比之下,GitHub Actions 的 Runner 在 SaaS 版下是推送(Push)模式,由 GitHub 平台分配 Runner。虽然 GitHub 官方承诺使用临时 VM(每次 Job 结束后销毁),但在我看来,对于敏感数据,你无法验证其底层的清理是否彻底。解决方案是我们在 GitHub Actions (2024.10) 中大量使用了 Self-hosted Runner,并配合 EPHEMERAL 模式。
在凭证管理上,两者的思路截然不同。GitLab CI 依赖于 Project/Group Variables,支持 Masked 和 Expanded 选项。我在实际配置中发现,GitLab 的 Masked 变量如果包含特殊字符(如 /)会直接报错,这迫使我不得不使用 Base64 编码后再传入,增加了流水线的复杂度。
GitHub Actions 在 2024 年的更新中,对 OIDC(OpenID Connect) 的支持已经非常完善。我在一个需要推送镜像到 AWS ECR 的场景中,彻底抛弃了长期 Access Key,转而使用 OIDC。
为什么这么做? 原因在于长期密钥一旦泄露,攻击窗口期极长。使用 OIDC,GitHub Actions 会向 AWS STS 请求一个临时的、仅对当前 Job 有效的 Token。
实际遇到的问题: 有一次我们在 GitLab 流水线中构建 Docker 镜像,发现构建速度极慢,且偶尔出现 no space left on device。排查下来发现,GitLab Runner 默认的 docker executor 会在 /var/lib/docker 下存储缓存,而我们的 Runner 磁盘只有 50G。解决方案是我在 config.toml 中配置了 docker-volumes = ["/cache"] 并挂载了外部存储,同时禁用了 DOCKER_BUILDKIT 的某些调试特性,将构建耗时从 15 分钟优化到了 4 分钟。
总结来看,GitLab CI 适合需要完全掌控 Runner 生命周期和深度定制隔离策略的场景;GitHub Actions 则凭借 OIDC 和云原生集成,在无需维护基础设施的前提下提供了极高的安全性。
在 2024 年,我们面临的一个核心挑战是软件供应链安全。客户要求我们达到 SLSA L3 级别,这意味着流水线必须是“防篡改”的,且所有制品必须有不可伪造的来源证明。我在这个项目中采用了 OPA (Open Policy Agent) 作为策略即代码(PaC)引擎,结合 Sigstore 进行签名,实现了这一目标。
传统的流水线安全往往依赖于人工审批,但在高频发布场景下,人肉审核形同虚设。我在项目中引入 OPA 的原因在于,我需要将安全规则转化为可自动执行的代码,且这些规则必须作为代码库的一部分进行版本管理。
例如,我制定了一条规则:禁止在构建阶段使用 latest 标签的基础镜像,且必须包含安全扫描报告。如果不这么做,开发人员可能会无意中引入含有严重漏洞的镜像。
我编写了一个 opa_policy.rego 文件,并在流水线中调用 conftest 来执行它:
在流水线中,我会在构建前运行这个检查:
SLSA L3 要求构建过程必须是“可审计”且“防篡改”的。我采用了 Sigstore Cosign 对容器镜像进行签名。Sigstore 的优势在于它使用了基于 PKI 的短期密钥,且公钥由透明日志(Rekor)记录,解决了密钥管理的难题。
实际场景: 我们在构建完镜像后,立即使用 Cosign 进行签名。随后,在部署到 Kubernetes 集群时,使用 Kyverno 策略引擎验证签名。如果镜像未签名或签名不匹配,Kyverno 会直接拒绝 Pod 的创建。
遇到的问题与排查: 在初期集成时,我们发现流水线偶尔会报 cosign: error: signing blob: no matching signatures。排查了整整一个下午,最后发现是因为我们的自托管 Runner 时间不同步,导致 OIDC Token 的时间戳验证失败。解决方案是在 Runner 基础镜像中强制安装 ntpdate 并在 Job 开始时同步时间,同时调整了 OIDC Token 的容忍时钟偏差。
为了达成 SLSA L3,仅有签名是不够的,还需要证明“这个制品是在哪个流水线、由谁、基于什么代码构建的”。我使用了 slsa-github-generator 的思路,在 GitLab CI 中手动生成了 SLSA Provenance(溯源证明)。
通过这种方式,我们构建了一条从代码提交到镜像部署的完整信任链。任何部署环节,只要检测到 Provenance 缺失或来源非受信任的 Runner,就会被 OPA 或 Kyverno 阻断。这种机制在应对类似 Log4j 这类供应链漏洞时非常有效,我可以迅速通过 Rekor 日志查询哪些镜像受到了影响,并强制要求所有新镜像必须包含修复后的 Provenance。
去年双十一前,我带团队给一个日订单量百万级的电商做流水线改造。当时他们用的是 GitHub Actions,Runner 直接跑在几台裸金属服务器上,所有项目的 AWS 密钥都硬编码在仓库的 Secrets 里。我接手第一周就发现,有个前端仓库的 Secrets 权限设置错了,居然能读到生产环境的 RDS 密码,这要是被恶意 PR 扫到,后果不敢想。
我决定推 OIDC 联合身份,把 Runner 和云厂商直接打通,去掉硬编码密钥。方案定的是 GitHub OIDC + AWS IAM Role,按仓库名和分支动态签发临时凭证。结果刚上线就出了怪事:大促压测时,支付服务的构建任务频繁报 403,说 STS 凭证获取失败。我盯着日志排查了一下午,发现是他们的构建脚本里有个递归调用,瞬间触发了上千次 OIDC 请求,直接撞上了 AWS 的 STS 限流。
最后我给 Runner 加了一层本地缓存,用内存存临时凭证,同一个 Job 内复用,只在过期前 5 分钟才去刷新。改完再压,限流直接消失,构建耗时反而比之前用静态密钥还快了 8 秒,因为省去了每次去 Vault 拉取密钥的网络开销。
关于 SBOM 和 Sigstore 签名,我得说句实在话。如果你的产品是纯内网部署,或者客户根本不关心软件供应链合规,别急着上 Sigstore 那套。我之前在一个小项目里强推,结果开发天天抱怨构建慢,最后发现生成的签名没人验,纯属白费劲。这东西最适合那种要给外部客户交付二进制、或者走 SLSA 合规审计的场景,真不是所有流水线都得标配。
最后给个建议:别一上来就照着大厂的方案抄。先看看你们流水线里最痛的那个点,是密钥泄露风险大,还是依赖库漏洞多?先解决那个最要命的,比堆一堆用不上的安全工具强得多。