一、为什么要专门为“凭证”写一整篇笔记
在 Jenkins 的日常使用里,我们常把“能跑起来”当成终点:连接代码仓库、拉取镜像、推送产物,只要任务变绿就万事大吉。可当安全审计下来,问题清单往往密密麻麻:
-
数据库密码明文躺在脚本里;
-
拉取 Git 的私钥被当成普通文件存进工作空间;
-
构建日志把 API Token 当成参数打印;
-
某人离职后,所有人不得不全局改密码。
这些隐患的根源并非“不知道要保密”,而是“不知道 Jenkins 提供了怎样的保密机制”。凭证(Credential)不仅是“把密码藏起来”那么简单,它涉及加密存储、作用域隔离、任务注入、日志过滤、生命周期管理、轮换策略六个环节。任何一个环节缺失,都会让“保密”变成形式。下文用 3000 字把“凭证”拆成八段:概念、存储、类型、作用域、注入、旋转、排错、案例,全程不提任何云厂商,也不贴命令截图,帮助你把“密钥”从“肉眼可见”变成“可运维、可审计、可回滚”的基础设施。
二、概念:到底什么叫“凭证”
在 Jenkins 的语境里,凭证 = 任何需要被保密的字节串。它可以是一段密码、一条私钥、一份证书、一个令牌,甚至是一个自定义文件。凭证的核心属性有三项:标识(ID)、描述(Description)、内容(Secret)。标识用于在任务里引用,描述用于人类可读,内容则存放在加密存储里,构建结束后自动从环境变量、日志、工作空间里擦除。理解“标识即引用”是第一步:只要 ID 不变,密码换了 100 次,所有任务都无需修改。这种“解耦”让轮换成为可能,也让“泄露”范围降到最小。
三、存储:加密、文件系统与备份
-
加密机制
Jenkins 采用一次一密的“主密钥→对象密钥”两层模型:启动时在内存生成随机主密钥,写进$JENKINS_HOME/secret.key;每个凭证内容再用独立的对象密钥加密,存进$JENKINS_HOME/credentials.xml。即便硬盘被拷走,没有 secret.key 也无法解密。 -
文件系统权限
把$JENKINS_HOME设成 750,属主为运行用户,杜绝“其他用户可读”。很多泄露事件并非“被黑客拖库”,而是“同事备份时把整块硬盘挂载到 Windows 上,NTFS 权限全开”。 -
备份与恢复
备份时必须同时拿secret.key + credentials.xml,缺一不可。恢复时先关 Jenkins,再拷贝文件,再启动,否则会出现“能解密但无法加载”的诡异错误。建议把 secret.key 单独存放到加密 U 盘,备份磁带只放 credentials.xml,实现“密钥与数据分离”。
四、类型:密码、私钥、证书、文件与 Secret 文本
-
用户名+密码
最常用,适合 Basic Auth、数据库连接、SMTP。注意:不要把用户名写在描述里,描述仅供人类阅读,真正引用时只用 ID。 -
SSH 私钥
拉取 Git 或登录远程主机的首选。私钥内容被加密存储,构建时临时写入工作空间,构建结束立即删除。即便如此,仍建议在服务端部署“强制命令”或“受限 Shell”,防止私钥被拿去交互式登录。 -
证书(PKCS#12 / PEM)
适合双向 TLS、Docker Registry 加密登录。证书里常带中间链,上传前先用openssl裁剪,只留必要信息,减少泄露面。 -
Secret 文件
把一整段配置文件(例如.env)当成Secret上传。任务里通过变量拿到路径,构建结束自动删除。适合“格式复杂、无法拆成字段”的场景。 -
字符串(Secret Text)
纯文本令牌,例如 API Token、Webhook Secret。长度超过 256 字节时建议拆成文件类型,否则界面加载会变慢。
五、作用域:全局、文件夹、项目、域四级隔离
-
全局作用域
任何任务都能引用。适合“公司级”共享凭证,例如 SMTP 密码。数量越少越好,过多会变成“全局变量地狱”。 -
文件夹作用域
只在特定文件夹(ItemGroup)内可见。适合“部门级”隔离,例如财务部自己的数据库密码。 -
项目作用域
Multibranch Pipeline 每个分支都能生成独立凭证,避免“开发分支拿到生产密码”。 -
域(Domain)作用域
限定凭证只能被特定“插件功能”使用,例如“只有 Git 插件能读取 SSH 私钥”。域隔离在底层通过CredentialsProvider接口实现,即使任务拿到 ID,也无法在非域环境使用。
六、注入:变量、绑定、掩码与日志过滤
-
环境变量注入
构建前,Jenkins 把凭证内容写进临时环境变量,构建结束后立即 unset。变量名默认是CREDENTIALS_ID大写形式,可在任务里直接引用。 -
绑定机制(Bindings)
SSH 私钥类型提供sshagent绑定,构建时自动启动 ssh-agent,把私钥加载到内存,构建结束 kill 代理;证书类型提供keystore绑定,临时生成keystore.pem文件,构建完删除。绑定让“文件路径”对任务透明,减少硬编码。 -
掩码(Masking)
凭证内容一旦注入,就会被加入“掩码列表”,构建日志若出现相同字符串,会被****替换。掩码是逐字节匹配,若密码里含空格或特殊符号,要确保掩码列表也包含相同格式,否则会出现“一半星号一半明文”的尴尬。 -
日志过滤插件
有些插件(例如 Maven)会把命令行参数打印到日志,掩码无法覆盖。此时可安装“Log Filter”插件,用正则表达式二次过滤;或者把敏感参数放进文件,用@符读取,避免出现在命令行。
七、旋转与轮换:让“过期”自动化
-
定期轮换策略
每 90 天强制更换数据库密码;Jenkins 通过“Credentials Provider”插件支持“到期提醒”:在凭证描述里写Expires: 2025-08-31,插件会在到期前两周在首页弹警告。 -
脚本化轮换
结合外部保险库(例如 HashiCorp Vault、Ansible Tower),在保险库里生成新密码,调用 Jenkins API 更新凭证内容,再触发任务重跑。整个流程无需人工登录 Jenkins。 -
灰度轮换
先更新“预生产”文件夹的凭证,验证通过后再批量更新“生产”文件夹;利用文件夹作用域实现灰度,避免“一把梭”导致全站故障。 -
回滚机制
轮换失败时,通过 API 把credentials.xml回滚到上一版本;Jenkins 每次保存凭证都会生成带时间戳的备份文件,回滚后需重启或调用reload-configuration端点才能生效。
八、排错:从“无法解密”到“注入失败”的漏斗
-
无法解密
现象:任务里引用凭证,构建日志报“cannot decrypt”;原因:secret.key 丢失或权限不对;解决:确认文件存在且属主为 jenkins,权限 600。 -
注入为空
现象:环境变量里没有出现预期变量;原因:任务类型不支持绑定(例如 Freestyle 没勾选“Use secret text(s)”);解决:在任务配置里添加绑定,确保 ID 与变量名对应。 -
掩码失效
现象:日志里出现明文密码;原因:密码里含换行,掩码列表只填了单行;解决:把换行符也加入掩码,或者改用文件注入,避免日志打印。 -
权限拒绝
现象:ssh 连接报“Permission denied”;原因:私钥注入成功,但远程主机改了公钥;解决:在远程主机预留“旧公钥 7 天缓冲期”,等 Jenkins 轮换后再移除旧公钥。
九、真实案例:一条密钥的完整生命周期
-
需求
财务系统每月 1 号凌晨拉取银行对账单,需要 SFTP 登录。银行每季度强制更换 SFTP 密钥。 -
设计
-
文件夹作用域:只有“财务”任务能访问
-
SSH 私钥类型:使用 sshagent 绑定
-
轮换策略:季度前两周在保险库生成新密钥→API 更新 Jenkins→重跑任务→验证成功→删除旧密钥
-
-
执行
第一次轮换时,发现银行新密钥格式为 OpenSSH 8.8,而 Jenkins 节点仍在 7.4,出现“算法不匹配”错误;通过升级节点 OpenSSH 解决。此步骤纳入“轮换 checklist”,下次轮换前先做 SSH 版本扫描。 -
复盘
轮换失败时,回滚 Jenkins 凭证只需 30 秒;但回滚银行端公钥需要走工单,耗时 2 天。于是把“公钥上传”拆成独立步骤,先上传新公钥→验证→再删除旧公钥,实现“可灰度可回滚”。
十、安全加固:把“保密”做成例行公事
-
最小权限
每个凭证只给“必要任务”使用;通过文件夹+域双重作用域限制,避免“一个密钥走天下”。 -
最小暴露
描述信息里不要写“这是生产数据库”,用“财务-报表-只读”代替,降低社会工程攻击面。 -
定期审计
每季度跑脚本拉取credentials.xml,解析出 ID、类型、描述、过期时间,生成 CSV 给审计部门;无过期标记的凭证强制补录。 -
密钥与数据分离
secret.key放加密 U 盘,credentials.xml放备份磁带;恢复时必须两人同时在场,防止“单点内部作恶”。