SafeW如何为CI/CD流水线注入短期签名密钥?

功能定位:为什么要在 CI/CD 里用“短命”密钥
传统做法把签名私钥长期放在仓库变量或 K8s Secret 里,一旦泄露就得全链路轮换。SafeW 的“短期签名密钥注入”把生命周期压缩到“Job 级”:流水线启动时向 SafeW 网关申请一张仅对该 Job 有效的签名证书,Job 结束立即吊销,私钥不落盘、不进缓存、也不写入环境变量文件。后文用“插件”简称这一机制。
插件与 SafeW 原有的“冷端 NFC 一碰签名”相互独立:前者服务自动化流水线,后者服务人工手机端。算法仅支持 ECDSA/secp256r1 与 Ed25519,RSA 不在范围内;有效期最短 5 min、最长 60 min,不可续期,到期后必须重新申请。
版本差异与迁移前提
截至最新版本(SafeW 控制台 6.4.2,网关 API 2026.02.28)才开放插件。若左侧菜单看不到“CI/CD 插件中心”,请让组织 Owner 在“设置-组织-实验功能”里手动开启“第三方流水线集成”开关;该开关默认关闭,且仅 Owner 角色可见。
老版本若使用“永久 API Key + 手动上传 keystore”,需先清理旧 Secret,再接入插件,否则会出现“双签名”导致链上验证失败。
控制台一次性配置:建立信任链
步骤 1:注册流水线身份
进入 SafeW 控制台 → 左上角切换至“组织视图” → CI/CD 插件中心 → 添加流水线。平台要求填写三项:
- 流水线名称:仅做展示,建议与 GitLab/GitHub 仓库同名,方便溯源。
- 可信域名:插件签发 JWT 时会校验 aud 字段,必须包含你的 CI 域名,例如 gitlab.example.com。
- 公钥指纹:把流水线运行器预置的 SSH Ed25519 公钥粘贴进来,用于后续建立双向 TLS。
提交后,控制台返回“插件 ID + 插件 Secret”,Secret 仅展示一次,请立即写入 CI 的受保护变量(GitLab 叫 Protected Variable,GitHub 叫 Encrypted Secret),切勿写进 .yml。
步骤 2:配置吊销策略
在同一页面继续向下滚动,可设置“Job 成功/失败自动吊销”与“最大可签发数量”。经验性观察:若并发 Job 数超过 200,建议把“最大可签发”调到 500,否则网关会返回 429,导致排队时间拉长。
流水线接入:GitLab 示例
以下 .gitlab-ci.yml 片段演示如何为 Android APK 签名。核心思路:在 before_script 阶段调用 SafeW CLI 申请密钥,签名完成后在 after_script 阶段主动吊销。
variables:
SAFEW_PLUGIN_ID: $SAFEW_PLUGIN_ID // 受保护变量
SAFEW_PLUGIN_SECRET: $SAFEW_PLUGIN_SECRET // 受保护变量
.inject_key:
image: safew/cli:6.4.2
before_script:
- safew ci inject --aud gitlab.example.com --validity 10m --out /tmp/sign.key
after_script:
- safew ci revoke --key-file /tmp/sign.key || true
build:
extends: .inject_key
stage: build
script:
- ./gradlew assembleRelease
- apksigner sign --ks /tmp/sign.key --ks-pass pass:empty --out signed.apk app-release-unsigned.apk
artifacts:
paths: [signed.apk]
注意:--ks-pass 必须给空口令,插件返回的密钥已受会话密钥加密,CLI 在内存中解密后喂给 apksigner,全程不落盘。
流水线接入:GitHub Actions 差异点
GitHub 环境缺少原生“after_script”,需把吊销步骤写在独立 job,并用 always() 保证触发:
jobs:
sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Inject key
id: inject
run: |
safew ci inject --aud github.com --validity 10m --out $RUNNER_TEMP/sign.key
echo "key=$RUNNER_TEMP/sign.key" >> $GITHUB_OUTPUT
- name: Sign artifact
run: |
apksigner sign --ks ${{steps.inject.outputs.key}} --ks-pass pass:empty --out signed.apk unsigned.apk
- name: Upload
uses: actions/upload-artifact@v4
with: {name: signed-apk, path: signed.apk}
revoke:
needs: sign
if: always()
runs-on: ubuntu-latest
steps:
- run: safew ci revoke --key-file ${{needs.sign.outputs.key}}
经验性观察:GitHub 并发上限 256,若组织仓库较多,建议把 --validity 调到 15 min,给排队留缓冲。
与自建 Jenkins 的协同
Jenkins 用户需先在“凭据存储”新增类型为“SafeW Plugin Secret”的条目(插件已上架 Jenkins Update Center,搜索“SafeW Short-Lived Signing”)。随后在 Pipeline 中:
stage('Inject Key') {
steps {
safewInject validity: 10, unit: 'MINUTES', audience: 'jenkins.example.com'
}
}
stage('Sign') {
steps {
sh 'apksigner sign --ks $SAFEW_KEY_FILE --ks-pass pass:empty --out signed.apk unsigned.apk'
}
}
stage('Revoke') {
steps {
safewRevoke() // 无论上游是否失败都执行
}
}
注意:Jenkins“回放”功能会重复执行 Pipeline,若手动点击“Replay”,同一 Job ID 会多次申请密钥,触发 SafeW 侧“重放保护”返回 409。解决方法是临时调高“最大可签发”或改用不同 Job 名称前缀。
例外与取舍:什么时候不该用
1. 需要 RSA 2048 且链上合约只认 RSA,插件无法支持,只能回退永久密钥。
2. 流水线运行环境完全离线(冷端),插件需访问 SafeW 网关,显然不满足。
3. 签名过程必须人工二次确认(如金融 U 盾),插件的自动化特性与合规冲突。
4. 单次 Job 运行时间超过 60 min,插件最大有效期仍不够,需把大任务拆段,中间重新申请。
警告
若把插件返回的密钥文件误写入 artifacts 并公开发布,等于把私钥泄露到互联网。SafeW 虽会强制吊销,但已签名文件无法撤回,请务必在 artifacts 上传前把 /tmp/sign.key 加入 exclude 列表。
故障排查速查表
| 现象 | 最可能原因 | 验证方法 | 处置 |
|---|---|---|---|
| CLI 返回 403 | aud 字段与控制台可信域名不一致 | 对比 --aud 参数与控制台 | 修改 .yml 或控制台任一侧域名 |
| 429 Too Many | 并发超限 | 观察控制台“实时签发数” | 临时调高上限或降低并发 |
| apksigner 报“keystore 损坏” | --ks-pass 给了非空口令 | 重新运行并加 -v | 改成 pass:empty |
| 吊销失败 404 | Job 被手动取消,密钥已自动吊销 | 查看网关日志 | 忽略即可,属预期行为 |
验证与观测方法
1. 在流水线末尾加打印:keytool -list -keystore /tmp/sign.key -storepass empty,可见证书有效期仅剩几分钟,证明“短命”生效。
2. 在 SafeW 控制台“审计日志”搜索流水线 ID,可拉出完整链路:签发时间、吊销时间、对应 Job URL,方便合规抽查。
3. 若想量化提速,可在旧流程与新流程各跑 30 次,记录“签名环节”耗时。经验性观察:网络稳定时,新流程平均缩短数十秒,主要省掉下载永久 keystore 的解密环节。
适用/不适用场景清单
- ✅ 移动 App 日构建 200+ 次,需要快速签名并上传测试分发平台。
- ✅ 微服务镜像内嵌 SBOM 签名,每次构建镜像即重新签名,无需长期保管私钥。
- ❌ 嵌入式固件签名后需保存 10 年供监管审计,要求密钥归档,插件自动吊销不符合法规。
- ❌ 链上 NFT 合约只认 RSA 4096,插件算法不支持。
最佳实践 6 条
- 把 --validity 设为 Job 历史最长耗时再留 30% 缓冲,既省配额又防超时。
- 一个仓库只注册一个插件 ID,多分支用 Job 名称区分,方便审计。
- 吊销步骤必须加 always() 或 post-always,防止失败分支把密钥遗留在内存。
- 不要把插件 Secret 用于本地调试,本地请用 SafeW 桌面端“临时签名”功能,避免审计日志混杂。
- 每月检查一次“最大可签发”利用率,高于 80% 就调大,低于 20% 就调小,节省组织级配额。
- 若同时使用 GitLab 与 GitHub,给不同平台创建不同插件 ID,万一某平台泄露可单独吊销,不影响另一方。
FAQ(结构化数据)
插件支持哪些算法?
目前仅支持 ECDSA/secp256r1 与 Ed25519,RSA 全系不在路线图内。
有效期最长能调多少?
控制台上限 60 min,不能再长;若 Job 耗时更长,需拆分阶段重新申请。
吊销失败会不会产生费用?
SafeW 对成功签发计费,吊销无论成败都不额外收费;但重复 404 可能提示你早已自动吊销,可忽略。
可以同时在多个组织复用同一个插件 ID 吗?
插件 ID 与组织绑定,跨组织无法复用;若搬迁仓库,需要在新组织重新注册。
网关 429 后会不会自动重试?
CLI 内置指数退避,最大 3 次;若仍失败需人工调高配额或降低并发。
收尾:下一步行动清单
读完本文,你已了解 SafeW 短期签名密钥注入的边界、成本与收益。建议先用非核心仓库做 1 周灰度,确认配额与并发无冲突后,再逐步将生产流水线切过来;切换前务必把旧永久密钥从变量库删除,防止“双轨”期误用。最后,把“故障排查速查表”贴到团队 Wiki,下次遇到 403/429 就能秒定位。未来版本若开放 RSA 或延长有效期,官方会在控制台 changelog 首条公告,记得打开“组织-消息通知”即可第一时间获得推送。