1. 先搞清楚凭证到底在解决什么问题1.1 从一次“莫名其妙”的构建失败说起刚接手 Jenkins 那阵子我碰到过一个很典型的场景流水线昨天还跑得好好的今天早上点进去一看控制台一片红卡在git clone那一步报的是fatal: could not read Username for http://git.xxx.com: No such device or address。当时第一反应是网络问题ping 了一下通了第二反应是仓库地址写错了复制粘贴到浏览器能打开折腾了半小时才反应过来——构建任务是从别的项目复制过来的作业里配置的凭证 ID 在新环境里根本不存在Jenkins 拿不到用户名密码就卡在了这里。这件事让我彻底意识到凭证Credentials不是 Jenkins 的一个“可选项”而是流水线能跑起来的地基。只要涉及从代码仓库拉代码就必然涉及“Jenkins 用什么身份去访问 Git 服务器”这个问题。你当然可以把用户名密码硬编码在 Jenkinsfile 里仓库是私有的话也能跑通但这么干的后果就是你的密码会明晃晃地躺在 Git 历史、构建日志、以及任何一个能看到这个作业配置的人眼前。Jenkins 凭证机制的核心作用说白了就三件事统一存储、按需调用、自动脱敏。第一把敏感信息集中存放在$JENKINS_HOME/credentials.xml里并且用 Jenkins 自己的加密密钥做加密不是明文躺着第二流水线里只写一个凭证 ID真正的密钥在运行时才注入进去代码里看不到任何敏感字符第三Jenkins 会尽量把绑定到环境变量的敏感值在控制台输出中替换成****降低日志泄露的风险。这三件事合起来才是“凭证”这两个字真正的分量。这篇笔记面向的是已经装好 Jenkins、也装好了 Git但卡在“怎么把 Git 连接配通”这一步的朋友。不管你是用 SSH 密钥还是账号密码是 Windows 上跑还是 Linux 上跑是自由风格项目还是声明式 Pipeline下面这套流程基本都能套用。1.2 凭证在 Jenkins 内部是怎么存和怎么取的很多人以为凭证是存在某个数据库里的其实不是。Jenkins 的凭证默认落在$JENKINS_HOME/credentials.xml文件长这样com.cloudbees.plugins.credentials.SystemCredentialsProvider domainCredentialsMap classhudson.util.CopyOnWriteMap$Hash entry com.cloudbees.plugins.credentials.domains.Domain specifications/ /com.cloudbees.plugins.credentials.domains.Domain java.util.concurrent.CopyOnWriteArrayList com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey scopeGLOBAL/scope idgit-ssh-prod/id description生产仓库 SSH 密钥/description usernamegit/username privateKey secretBytes.../secretBytes /privateKey /com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey /java.util.concurrent.CopyOnWriteArrayList /entry /domainCredentialsMap /com.cloudbees.plugins.credentials.SystemCredentialsProvider注意secretBytes这一段它是被加密过的。加密用的密钥在$JENKINS_HOME/secret.key和$JENKINS_HOME/secrets/目录下。这就解释了一个很常见的问题为什么把整个 Jenkins 目录拷到另一台机器上凭证全部失效了。因为密钥文件没一起拷过去或者拷贝过程中变了原来的密文自然解不开。理解这一点对后面的故障排查非常关键。当你看到java.io.IOException: Unable to decrypt或者凭证列表里显示一坨看不懂的东西基本就是密钥文件出问题了而不是凭证本身写错了。1.3 四类凭证的适用场景对照Jenkins 默认提供的凭证类型不多但覆盖了绝大部分 Git 场景。我整理了一张选型表你在加凭证之前先对号入座能省掉很多来回试错的时间。凭证类型典型字段适合的 Git 场景我的推荐度Username with password用户名、密码HTTP(S) 方式访问 GitLab/Gitea/Gitee且服务端未开启令牌强制一般SSH Username with private key用户名通常填 git、私钥、口令githost:group/repo.git形式的拉取最稳强烈推荐Secret text一段字符串用访问令牌Token替代密码走 HTTP(S)推荐Secret file一个文件需要传.pem、.key文件本身给脚本使用特殊场景判断逻辑其实很简单看你的仓库地址是http://开头还是git开头。git开头的走 SSH 密钥http://或https://开头的走用户名密码或者令牌。这两条路的选择不是随便定的而是由 Git 客户端的协议决定的用错了必然连不上。2. 动手之前先把 Git 侧的事情理顺2.1 确认 Jenkins 主机和 Git 服务器是通的这一步听起来像废话但实际排查中至少三成的“凭证无效”最后都定位到了网络层。Jenkins 跑在容器里、跑在内网隔离区、跑在另一台云主机上这些情况都会导致它能打开管理页面但连不上代码仓库。先做最基本的连通性测试。SSH 方式的话用 Jenkins 运行账户去测不是用你自己的账号# Linux先切到 jenkins 用户 sudo su -s /bin/bash jenkins ssh -T -p 22 gitgit.example.com # 成功的话一般会返回类似 welcome 的提示并明确告诉你认证通过HTTP(S) 方式的话curl -I -u yourname:yourpass http://git.example.com/group/repo.git/info/refs?servicegit-upload-pack # 返回 200 说明账号密码没问题返回 401 说明认证不过注意Windows 上以服务方式安装的 Jenkins默认运行账户是Local System它的家目录是C:\Windows\System32\config\systemprofile跟你当前登录用户的家目录完全不是一回事。你在自己账号下生成密钥、配好 known_hosts对 Jenkins 服务来说等于不存在。这是 Windows 环境最容易踩的第一个坑。2.2 生成 SSH 密钥对并完成服务端授权如果你决定走 SSH 密钥路线密钥的生成姿势有讲究。我一般这么干ssh-keygen -t ed25519 -C jenkinsci-host -f /var/lib/jenkins/.ssh/id_ed25519_ci -N 几个参数解释一下都是有意为之的-t ed25519比 RSA 更短更快现代 Git 服务端基本都支持。如果你的服务端比较老只认 RSA那就换-t rsa -b 4096。-C注释写上用途和主机名将来在服务端看授权列表的时候能一眼认出是谁的密钥别写个xxxxxx这种没信息量的。-f指定文件名。千万不要用默认的id_rsa覆盖很多环境下 Jenkins 主机还跑着别的东西覆盖默认密钥会连带把别的服务搞挂。单独起个名字比如id_ed25519_ci。-N 空口令。这里要解释一下不是说不该设口令而是 Jenkins 凭证里如果填了带口令的私钥得把口令也一起填进去多一层管理成本。规范做法是私钥本身设口令、然后 Jenkins 凭证里把 Passphrase 也填上嫌麻烦的话空口令也行但前提是这台主机的访问权限要收紧到只有运维能登。生成完公钥内容复制出来cat /var/lib/jenkins/.ssh/id_ed25519_ci.pub粘到 Git 服务端的部署密钥Deploy Key或者用户 SSH Keys 里。这两者选哪个看你的场景只读单个仓库用 Deploy Key权限最小要读多个仓库用机器用户比如叫ci-bot的 SSH Keys一把钥匙开多扇门。我倾向后者因为跨项目复用一个凭证管理成本低得多。2.3 Jenkins 运行账户与家目录对照表前面提过 Windows 的坑这里给一张跨平台对照表把它彻底说清楚。你可以照着表去确认“我的密钥到底该放哪”。运行环境Jenkins 运行账户实际家目录密钥/known_hosts 的落点Linux 包安装jenkins/var/lib/jenkins/var/lib/jenkins/.ssh/Linux 手动 war 启动当前登录用户取决于用户该用户家目录的 .sshDocker 容器通常 root 或 jenkins/var/jenkins_home/var/jenkins_home/.ssh/Windows 服务默认Local SystemC:\Windows\System32\config\systemprofile该目录下的 .sshWindows 服务改了登录账户指定账户该账户的家目录C:\Users账户.ssh确认方法很简单在 Jenkins 里新建一个自由风格任务构建步骤写一条命令whoami echo $HOMEWindows 上就写whoami和echo %USERPROFILE%。跑一次看输出比自己猜靠谱一百倍。我第一次遇到 Windows 那个坑的时候就是靠这招五分钟定位的。3. 添加 SSH 类型凭证字段逐个拆开讲3.1 从管理界面点到添加页路径是固定的Manage Jenkins → Credentials → System → Global credentials (unrestricted) → Add Credentials。这里有两个层级的坑需要提醒。第一System下面的Global credentials是全局域任何作业都能用如果你建了自定义 Domain凭证就只能在对应域里可见作业里写 ID 会报“找不到凭证”。第二Store 选默认的Jenkins就行除非你装了 HashiCorp Vault 之类的插件那另说。选类型的时候挑SSH Username with private key然后字段是这些ScopeGlobal意味着所有节点、所有作业可用System意味着只有 Jenkins 自身比如拉取 Jenkins 配置仓库能用普通作业看不到。没特殊需求就选 Global。ID这是关键字段流水线里就是靠它引用的。建议用可读性强的命名别用系统自动生成的那串 UUID。我的命名习惯是git-ssh-用途-环境比如git-ssh-deploy-prod、git-ssh-readonly-dev。UUID 那种a1b2c3d4-...过两个月你自己都认不出来。Description写清楚这把钥匙对应哪个 Git 服务、哪个机器用户、什么权限。比如“GitLab 生产组 ci-bot 只读密钥2024-06 轮换”。Username这里填的是 SSH 连接时的用户名不是 Jenkins 用户名也不是你的 Git 账号名。GitLab/Gitea 走 SSH 时统一是gitBitbucket 也是git极少数自建服务可能用别的去 Git 服务的 SSH 克隆地址里看前面那一段是什么就填什么。Private Key选Enter directly把私钥文件全部内容粘进去包括-----BEGIN OPENSSH PRIVATE KEY-----和结尾那一行。或者选From the Jenkins master ~/.ssh让 Jenkins 自己去读文件。我一般推荐 Enter directly因为凭证跟着 Jenkins 配置走备份迁移的时候不会漏文件。注意粘贴私钥的时候极度容易出问题。一是别把.pub公钥粘进去公钥粘进去必然认证失败而且报错信息不会直接告诉你“你粘错了”。二是别丢换行或加多余空格有些编辑器会自动给文件末尾加空行一般没事但中间被自动格式化过就废了。粘完检查一下行数ed25519 私钥通常 3 到 4 行。3.2 known_hosts 这个坎三种处理姿势凭证加完了很可能第一次构建还是失败报Host key verification failed。原因是 SSH 客户端第一次连陌生主机会要求确认主机指纹而 Jenkins 是无交互的确认不了就断了。处理方式有三种按推荐度从高到低排第一种ssh-keyscan 预置指纹最推荐。用 Jenkins 运行账户执行把目标 Git 服务器的公钥写进 known_hostssudo su -s /bin/bash jenkins mkdir -p ~/.ssh chmod 700 ~/.ssh ssh-keyscan -p 22 git.example.com ~/.ssh/known_hosts 2/dev/null chmod 644 ~/.ssh/known_hosts这么做的逻辑是指纹是通过一个相对可信的通道预先固定的之后每次连接都会比对。安全性和便利性平衡得最好。第二种用 Jenkins Git 插件自带的 Host Key Verification 配置。新版本的 Git 插件在全局安全配置里提供了主机密钥校验策略可以选“接受首次连接的主机密钥”或者“手动维护已知主机列表”。这个方案的好处是跨节点统一坏处是它只对 Git 插件发起的连接生效你在 Pipeline 里手写sh git clone是走不到这个逻辑的。第三种-o StrictHostKeyCheckingno不推荐但要知道它的存在。可以在 Jenkinsfile 里这么写sh GIT_SSH_COMMANDssh -o StrictHostKeyCheckingno -o UserKnownHostsFile/dev/null git clone gitgit.example.com:group/repo.git这东西能用但它意味着你放弃了对主机身份的校验中间人攻击是拦不住的。只在临时排查时用不要写进正式的流水线。我见过太多团队把这一行抄进模板一用就是三年。3.3 验证凭证是否真的通了加完凭证别急着往流水线里塞先单独验一次。最直接的办法是建一个一次性的自由风格任务源码管理选 GitURL 填gitgit.example.com:group/repo.gitCredentials 选你刚建的那个然后直接点构建。还有一个更轻的办法在 Jenkins 的脚本控制台Manage Jenkins → Script Console里跑一段 Groovy确认凭证存在且可读import jenkins.model.Jenkins import com.cloudbees.plugins.credentials.CredentialsProvider def creds CredentialsProvider.lookupCredentials( com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey.class, Jenkins.instance, null, null ) creds.each { c - println ID: ${c.id} | User: ${c.username} | Desc: ${c.description} }提示只在测试环境用脚本控制台做验证生产环境的脚本控制台权限要收好因为它本质上相当于 Jenkins 的 root shell。4. 账号密码 / 令牌类型凭证的配置要点4.1 什么时候绕不开 HTTP(S) 方式有些团队就是不用 SSH原因多种多样防火墙只开了 443、Git 服务侧没开放 SSH、或者干脆是为了审计方便想走统一的 HTTP 网关。这个时候就得上用户名密码或者令牌。选类型的时候Username with password和Secret text两者差别在于前者在 Pipeline 里能同时拿到用户名和密码两个变量适合拼 URL 的场景后者只有一个字符串适合直接当令牌用。如果 Git 服务端已经禁用了密码认证、强制要求访问令牌那老老实实用Secret text把令牌整串填进去。4.2 用访问令牌替代密码的具体做法以常见的自建 Git 服务为例先去个人设置里生成一个访问令牌权限勾选read_repository就够了用于拉代码不需要write_repository。生成出来的令牌通常只显示一次复制走。然后回到 Jenkins 加凭证类型选Username with password用户名填你的账号密码字段填令牌串不是登录密码。很多服务端在禁用密码认证后你填登录密码会直接返回 401而且日志里只告诉你认证失败不告诉你是密码还是令牌的问题白白浪费排查时间。如果就是要用Secret text类型那在 Pipeline 里拼接的方式会不太一样withCredentials([string(credentialsId: git-token-prod, variable: GIT_TOKEN)]) { sh git clone http://oauth2:${GIT_TOKEN}git.example.com/group/repo.git }注意这里的用户名占位符不同服务端要求不一样GitLab 常用oauth2Gitea 用你的用户名也行具体看你服务端的文档。4.3 ID 命名的统一规范凭证多了以后命名混乱是灾难。我给自己定的规矩是四段式系统-协议-用途-环境。举几个实际的例子git-ssh-deploy-prodgit-http-readonly-devgit-token-release-stagingnexus-http-publish-prod好处很直接在流水线里看到credentialsId: git-ssh-deploy-prod不用点进凭证页面就知道这是干啥的、挂在哪个环境。换环境部署的时候把后缀从prod改成staging就行不用去翻密码本。5. 在流水线里把凭证真正用起来5.1 自由风格项目的配置位置自由风格任务最简单源码管理 → Git → Credentials下拉框选一个就行URL 填 SSH 或 HTTP 地址Jenkins 会自动匹配对应类型的凭证。如果是 SSH 地址但下拉框里只有密码类型凭证它会连不上反过来也一样。这里有个容易忽略的点下拉框里显示的是 Description不是 ID。所以 Description 写得清楚非常重要尤其是凭证数量上双之后。另外就是前面提到的那次翻车——复制任务的时候凭证 ID 在新机器上不存在下拉框会显示成红色报错但构建按钮还是能点点了就失败。5.2 声明式 Pipeline 的两种写法声明式 Pipeline 里拉代码主要有两种方式。第一种用checkout scm让 Jenkins 自己去读作业配置里的源码管理设置pipeline { agent any stages { stage(Checkout) { steps { checkout scm } } } }这种写法适合代码仓库固定不变的情况简单省事。第二种显式指定仓库和凭证适合一个流水线要拉多个仓库的场景pipeline { agent any stages { stage(拉取主仓库) { steps { checkout([ $class: GitSCM, branches: [[name: */main]], userRemoteConfigs: [[ url: gitgit.example.com:group/main-repo.git, credentialsId: git-ssh-deploy-prod ]] ]) } } stage(拉取配置仓库) { steps { checkout([ $class: GitSCM, branches: [[name: */master]], userRemoteConfigs: [[ url: http://git.example.com/group/config-repo.git, credentialsId: git-http-readonly-prod ]] ]) } } } }第二种写法里$class: GitSCM这一坨看起来吓人但套路固定抄一次改改 URL 和凭证 ID 就能反复用。关键是每个userRemoteConfigs里都要单独指定credentialsId不能指望它复用第一个的。我踩过这个坑第二个仓库没写凭证 ID构建日志里报“没有权限”查了半天才发现两处配置不一样。5.3 脚本式 Pipeline 与 withCredentials 的细节脚本式 Pipeline 里最常见的两种模式sshagent和withCredentials。// 模式一sshagent把私钥临时加载进 ssh-agent node { sshagent(credentials: [git-ssh-deploy-prod]) { sh git clone gitgit.example.com:group/repo.git } } // 模式二withCredentials把用户名密码注入环境变量 node { withCredentials([usernamePassword( credentialsId: git-http-readonly-prod, usernameVariable: GIT_USER, passwordVariable: GIT_PASS )]) { sh git clone http://${GIT_USER}:${GIT_PASS}git.example.com/group/repo.git } }两者的区别值得说清楚。sshagent是把私钥交给 ssh-agent 进程托管Git 在需要的时候通过 agent 完成认证私钥本身不会出现在环境变量或命令行里安全边界更干净。withCredentials是把明文值注入环境变量虽然 Jenkins 会尝试在日志里做脱敏替换但如果你的脚本把变量重定向到文件再cat出来脱敏就绕过去了这一点必须心里有数。注意用withCredentials拼 URL 的时候变量一定要用单引号包裹的 shell 脚本或者 Groovy 字符串插值混用会导致变量没展开。sh git clone http://${GIT_USER}...这种双引号写法里${GIT_USER}是 Groovy 先插值的值在 Jenkins 侧展开容易和 shell 变量搞混用单引号写sh git clone http://${GIT_USER}...才是让 shell 去展开环境变量行为更符合预期。更稳妥的做法是避免把密码塞进 URL改用 Git 的凭证助手withCredentials([usernamePassword( credentialsId: git-http-readonly-prod, usernameVariable: GIT_USER, passwordVariable: GIT_PASS )]) { sh git config --global credential.helper !f() { echo username${GIT_USER}; echo password${GIT_PASS}; }; f git clone http://git.example.com/group/repo.git }这样密码不会出现在进程命令行参数里ps看不到比直接拼 URL 干净得多。5.4 那些和 Git 相关、值得记住的环境变量Jenkins 的 Git 插件在拉完代码后会往构建环境里注入一批变量写脚本的时候很有用变量名含义典型用途GIT_COMMIT当前构建对应的提交哈希打镜像 Tag、写进构建信息GIT_BRANCH分支名形如origin/main判断分支走不同流程GIT_URL仓库地址日志记录、通知消息里带链接GIT_PREVIOUS_COMMIT上一次成功构建的提交计算本次变更范围GIT_CHANGESET变更集信息部分插件提供生成变更日志判断分支的时候有个小坑GIT_BRANCH带origin/前缀直接跟main比是不相等的。我一般这么处理def branch env.GIT_BRANCH.replaceFirst(^origin/, ) if (branch main) { // 走发布流程 }还有一点这些变量只有在 Git 插件真正执行过 checkout 之后才存在。如果你的流水线第一步是别的操作就去读env.GIT_COMMIT拿到的是空字符串然后后面拼出来的镜像 Tag 就是一个空串问题会延后到部署阶段才暴露排查起来更费劲。6. 报错排查实录与速查表6.1 六个高频报错的现象、原因和处置下面这张表里的每一条都是我或者身边同事真实遇到过的不是从文档里抄的。报错信息节选大概率原因处置方式Permission denied (publickey)公钥没加到 Git 服务端或 Jenkins 用了别的私钥确认服务端授权列表里有对应公钥确认凭证里的私钥和ssh -i测试用的是同一把Host key verification failedJenkins 运行账户的 known_hosts 里没有目标主机指纹用ssh-keyscan预置指纹见 3.2 节fatal: could not read Username for http://...HTTP 地址但没配凭证或凭证 ID 在当前环境不存在检查作业配置里的凭证下拉框是否报红确认 ID 拼写Authentication failed for http://...密码错误或服务端要求令牌但填了登录密码用curl -u单独验证一次改填访问令牌Unable to decrypt/ 凭证列表显示异常Jenkins 主密钥文件损坏或被替换别急着重建凭证先确认secret.key和secrets/目录完整性No such device or address非交互环境下 Git 想弹交互提示说明凭证根本没注入成功回头查 withCredentials 的包裹范围注意第 5 条这是我见过最隐蔽的一类问题。表面上看起来是凭证内容有问题实际上是密钥体系坏了。这种情况下如果你直接去删凭证重建会连累所有依赖这些凭证的作业。正确顺序是先确认secret.key在不在、secrets/目录下的文件是否完整再决定要不要重建。6.2 我自己习惯的排查顺序每次遇到拉代码失败我基本是按这个顺序走的平均五分钟能定位第一步看构建日志里的第一行 git 命令。Jenkins 的 Git 插件实际上执行的是类似git -c diff.mnemonicprefixfalse -c core.quotepathfalse --no-optional-locks fetch ...这样的命令看清楚它实际用的是哪个地址、哪个协议别被你在界面上填的东西迷惑有时候作业配置和实际执行不一致。第二步在 Jenkins 主机上用同一个运行账户手动敲一遍等价命令。这是最有效的办法因为它把 Jenkins 的干扰因素全排掉了剩下的就是纯 SSH/HTTP 的问题。手动能通Jenkins 不通那就是凭证注入环节的问题手动也不通那就是密钥或网络的问题。第三步看凭证的作用域和域。前面说过Domain 和 Scope 配错了会导致作业根本看不到这个凭证表现是下拉框里压根没这一项或者流水线报 “Could not find credentials”。第四步才去看密钥内容本身。公钥私钥是否配对公钥有没有过期或被移除服务端账号有没有被禁用。这一步放最后因为概率最低但很多人一上来就怀疑这个来回折腾。6.3 几条踩过坑才记住的经验第一条凭证轮换要提前想好。密钥是有有效期的团队里人离职了要换服务端策略变更了要换。我给自己定的规矩是凭证 ID 一旦确定就永远不改轮换的时候只更新内容不更新 ID。这样所有流水线都不用动改一处生效全部。如果反过来每次轮换都换个新 ID那你得去全量搜索所有 Jenkinsfile 和作业配置漏一个就炸一个。第二条节点上的凭证是各自独立的。Jenkins 支持多节点凭证虽然配置在 master 上但实际使用是在 slave 上完成的。SSH 密钥会被传输过去但 known_hosts 不会——slave 上得单独配。这个坑在多节点环境下几乎是必踩的表现就是 master 上跑得好好的任务一到某个 agent 上就报主机指纹错误。第三条日志脱敏不是万能的不要依赖它。Jenkins 的脱敏机制是基于字符串匹配的它会尝试把绑定的敏感值替换成星号。但如果你在脚本里对密码做了编码、拼接、或者写进文件再读出来脱敏很容易失效。真正的安全原则是敏感值尽可能不要出现在标准输出里而不是赌脱敏一定生效。第四条Windows 上优先改用非服务方式启动 Jenkins 来做调试。服务方式的账户隔离问题会带来大量难以理解的认证失败。调试阶段用java -jar jenkins.war直接跑跑通了再改成服务方式并且把服务登录账户显式改成生成密钥的那个账户比在 Local System 那套家目录里摸索要省事得多。第五条插件镜像和学习路径。升级站点在国内访问有时会比较慢可以换成国内高校或云厂商提供的镜像源来加速插件下载这跟凭证本身没关系但配置凭证前如果先装好了 Git 插件、Credentials Binding 插件、SSH Agent 插件这几个必备项后面的步骤会顺畅很多。Git 客户端本身也要装并且确保git --version在 Jenkins 主机的命令行里能执行——Jenkins 自己不带 Git它只是个调用者。这些经验听起来琐碎但都是那种“不知道就得卡半天知道了三分钟搞定”的东西。凭证配置这件事本身没有多高深的技术含量真正的门槛在于左右依赖的环境细节太多任何一环错位都会表现为同一个模糊的认证失败。把上面这套流程走一遍把每步的验证点都过一遍后面的流水线开发才会真正顺畅起来。我自己现在带新人的时候都会让他们先独立把这条链路配通一遍生成密钥、加授权、建凭证、跑通一次 checkout、再故意制造一次失败去读日志。走完这一轮后面遇到再花哨的 Jenkins 报错基本都能顺着这条线摸到根上。