宿主机 SSH 免密已通,为何 Jenkins 流水线仍报 Permission denied?


【避坑指南】宿主机 SSH 免密已通,为何 Jenkins 流水线仍报 Permission denied?

一、 问题现象

在进行持续集成与部署(CI/CD)时,需要在构建阶段将产物通过 scp 发送至目标服务器,或者通过 ssh 触发远程启动脚本。

  1. 宿主机终端测试: 在运行 Jenkins 的宿主机终端上,切换至当前用户执行免密登录,完全正常且无需输入密码:

    bash 复制代码
    ssh appuser@192.168.1.100 "echo 免密打通成功"
    # 输出:免密打通成功
  2. Jenkins Pipeline 构建报错: 在流水线执行相同的命令时,任务直接卡住并报错失败:

    text 复制代码
    + scp -o StrictHostKeyChecking=no app.tar appuser@192.168.1.100:/data/tgz
    Permission denied, please try again.
    appuser@192.168.1.100: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).
    scp: Connection closed
    ERROR: script returned exit code 255

二、 核心根因剖析

很多开发者会困惑:"明明我在服务器上跑通了,为什么 Jenkins 跑就报权限拒绝?"

核心原因在于:执行命令的"主体身份"与"运行环境"错位。

1. 容器环境隔离(最常见场景)

当 Jenkins 以 Docker 容器 形式运行时,流水线里的 sh 步骤是在 Jenkins 容器内部 执行的:

  • 宿主机环境 :你之前配置并打通的密钥存放在宿主机的 /home/youruser/.ssh/ 下。
  • Jenkins 执行环境 :容器内部运行的是一个独立的系统,使用的身份通常是内置的 jenkins 用户(根路径为 /var/jenkins_home/)。
  • 结论 :容器内部没有宿主机的私钥,发起 SSH 请求时自然找不到可用的认证信息;而 CI/CD 环境属于非交互式终端,无法让人输入密码,因此连接被远端直接切断。

2. 为什么不推荐直接在容器内折腾系统免密?

有人会尝试进入容器内部执行 ssh-keygen 和 ssh-copy-id,但这种做法往往会引来新的问题:

  • 加密算法不兼容 :较旧的 Jenkins 镜像默认生成的 ssh-rsa 密钥类型,在现代高版本 Linux 服务端上会被默认弃用,导致握手直接断开(报错 kex_exchange_identification: Connection closed)1, 2。
  • 无状态脆弱性:一旦容器被重建或升级,容器内手工配置的 SSH 授权和公私钥极易丢失。

三、 标准解决方案:使用 Jenkins Credentials 插件注入私钥

最佳实践思路 :不要依赖底层 Linux 系统的免密机制,直接将现有的有效私钥托付给 Jenkins 凭据中心,在执行流水线时以变量方式安全注入。


第一步:获取宿主机上已打通的私钥文本

在之前测试免密成功的宿主机终端上,打印你的私钥:

bash 复制代码
cat ~/.ssh/id_rsa

完整复制输出的整段内容(包含 -----BEGIN ... PRIVATE KEY----- 和 -----END ... PRIVATE KEY----- 前后两行)。


第二步:在 Jenkins 凭据中录入该私钥

  1. 进入 Jenkins 后台:系统管理 (Manage Jenkins) →\rightarrow → 凭据 (Credentials)。
  2. 点击进入全局域(Global credentials),选择 Add Credentials (添加凭据)。
  3. 按照如下配置填写:
    • 类型 (Kind) :选择 SSH Username with private key
    • ID :填写一个易记的标识,例如 deploy-ssh-key(流水线中需引用)
    • Username :填写目标服务器的登录用户名(例如 appuser)
    • Private Key :选择 Enter directly ,点击 Add,把刚才复制的私钥完整粘贴进去。
  4. 点击 Create (保存)。

第三步:修改 Jenkinsfile 流水线脚本

在执行 scp 和 ssh 的阶段,使用 withCredentials 代码块包裹命令。Jenkins 会在运行时将私钥以临时文件的形式安全地提供给 -i 参数使用:

groovy 复制代码
stage('发布及远程执行') {
    steps {
        script {
            def remoteUser = "appuser"
            def remoteIp   = "192.168.1.100"
            def tarFile    = "${env.WORKSPACE}/app.tar"
            def deployCmd  = "sh /data/shell/run.sh"

            echo ">>>> 正在向目标节点推送文件..."

            // 使用 sshUserPrivateKey 注入凭据
            withCredentials([sshUserPrivateKey(credentialsId: 'deploy-ssh-key', keyFileVariable: 'KEY_FILE')]) {
                sh """
                    # 1. 使用临时私钥文件传输安装包
                    # 注意:StrictHostKeyChecking=no 必须带等号,避免交互挂起
                    scp -o StrictHostKeyChecking=no -i "\${KEY_FILE}" ${tarFile} ${remoteUser}@${remoteIp}:/data/tgz
                    
                    # 2. 使用临时私钥执行远程启停指令
                    ssh -o StrictHostKeyChecking=no -i "\${KEY_FILE}" ${remoteUser}@${remoteIp} "${deployCmd}"
                """
            }
        }
    }
}

四、 方案总结与核心价值

对比维度 方案一:在容器内配置免密 方案二:Jenkins 凭据插件(推荐)
配置复杂度 需进入容器处理密钥、排查目录权限及协议兼容 只需在 UI 粘贴一次私钥,即配即用
容器迁移/重建 容器重建或销毁后,免密配置容易丢失 凭据保存在 Jenkins 内部,容器重建不受影响
安全性 私钥以明文文件形式长期存留在容器路径中 运行时动态生成临时密钥文件,执行完毕自动销毁脱敏
算法兼容性 易踩旧版 OpenSSH 算法协商失败的坑 抹平环境差异,稳定可靠

通过将认证过程转交 Jenkins 凭据体系管理,彻底规避了"宿主机能连、容器不能连"的环境权限盲区,构建任务即可稳定实现无人值守的自动化发布。

相关推荐
落魄大学生之流水线上谋生计8 小时前
从 Jenkins 小白到企业级交付平台
运维·jenkins
java、iOS、Vue1 天前
Maven antrun vs Jenkins 脚本组装 deb的优缺点、适用场景
java·jenkins·maven
事圆则缓3 天前
Android 使用 Jenkins 实现 CI/CD,并用 SonarQube 建立质量门禁
android·ci/cd·jenkins
Zhou1411367 天前
CICD_01_持续集成与Jenkins入门
运维·ci/cd·jenkins
大貔貅喝啤酒7 天前
Gitea+Jenkins+Docker 搭建 Node 项目 CI/CD 自动部署完整教程
ci/cd·docker·jenkins·js·gitea
guo_wen_qiang8 天前
jenkins流水线参数化配置
运维·docker·容器·jenkins·持续部署
何中应9 天前
Jenkins 如何给设置公司 Logo
运维·ci/cd·jenkins
何中应9 天前
Jenkins 如何配置工作节点
运维·ci/cd·jenkins
wjjzhbb10 天前
Jenkins到ArgoCD——GitOps持续交付实践
运维·缓存·jenkins·argocd