PAYATHON 2026

同一 SSH 私钥本地可用,GitHub Actions 的 scp-action 为何失败?

数据阿明

材料中没有实际错误日志,暂时无法确定唯一原因。本地连接成功只能证明本地环境可以登录;还需要分别确认 GitHub runner 是否取得了相同的私钥、能否访问 VPS,以及登录后能否写入部署目录。

先找出 Deploy docker-compose to VPS 的首个错误,再处理后续部署步骤。前一步失败后,后续步骤默认会被跳过,这通常不代表 ssh-action 也有独立故障。

1. 先确认失败发生在哪个阶段

打开失败的 Actions run,展开 Deploy docker-compose to VPS,查看报错前后的日志。以下是常见的错误线索,不能据此认定本次出现了这些错误:

日志线索优先检查
私钥解析失败、no key found、invalid formatSecret 是否为空、私钥是否保留真实换行、内容是否完整
unable to authenticate、Permission denied (publickey)用户名、公钥授权、实际使用的私钥、服务端认证策略
i/o timeout、connection refused、域名解析错误主机、端口、SSH 服务和防火墙规则
找不到源文件、远端解包失败、目录写入被拒绝工作区文件、目标目录权限和远端 tar

确认连接耗时超过限制后,再考虑调整超时。增加超时时间无法解决认证失败或目录无写入权限的问题。

2. 检查当前 job 是否真正取得 Secrets

确认这些名称与 workflow 完全一致:

  • VPS_HOST
  • VPS_USER
  • VPS_DEPLOY_USER_KEY
  • VPS_SSH_PORT
  • VPS_DEPLOY_PATH

如果使用 Environment secrets,job 需要引用相应的 environment;如果使用 Organization secrets,需要确认当前仓库有访问权限。引用未设置的 Secret 会得到空字符串。相关规则见 GitHub Secrets 文档。

可以在上传步骤之前临时加入以下检查。它会检查必填值、源文件以及私钥能否被 OpenSSH 解析,不会输出私钥内容:

- name: Check deployment inputs
  if: github.event_name != 'pull_request'
  shell: bash
  env:
    VPS_HOST: ${{ secrets.VPS_HOST }}
    VPS_USER: ${{ secrets.VPS_USER }}
    VPS_SSH_PORT: ${{ secrets.VPS_SSH_PORT }}
    VPS_DEPLOY_PATH: ${{ secrets.VPS_DEPLOY_PATH }}
    VPS_DEPLOY_USER_KEY: ${{ secrets.VPS_DEPLOY_USER_KEY }}
  run: |
    set -euo pipefail

    for name in VPS_HOST VPS_USER VPS_SSH_PORT VPS_DEPLOY_PATH VPS_DEPLOY_USER_KEY; do
      if [[ -z "${!name}" ]]; then
        echo "Missing deployment input: $name"
        exit 1
      fi
    done

    test -f docker-compose.yml

    key_file=$(mktemp)
    trap 'rm -f "$key_file"' EXIT
    chmod 600 "$key_file"
    printf '%s\n' "$VPS_DEPLOY_USER_KEY" > "$key_file"
    ssh-keygen -y -P '' -f "$key_file" > /dev/null

    echo "Deployment inputs and private key parsing passed"

这里的 -P '' 适用于题目中“私钥没有 passphrase”的前提。检查通过只说明输入非空、文件存在且私钥可被 OpenSSH 解析,不能证明 VPS 接受该密钥,也不能证明 scp-action 一定能完成传输。

3. 核实私钥内容和本地实际使用的身份

key: 接收完整私钥文本,不是 .pub 公钥、私钥文件路径或 Base64 编码结果。保存 Secret 时要保留多行内容,不要把真实换行替换成字面量 \n,也不要在外层添加引号。未加密的私钥无需设置 passphrase。参数含义可参阅 scp-action 配置说明。

本地 SSH 可能读取 ~/.ssh/config、使用 SSH agent,或通过代理连接。可以在本地运行以下命令,确认是否使用了指定私钥。先将环境变量设为实际连接值,并将 ./deploy_key 替换为私钥路径:

ssh -vvv \
  -F /dev/null \
  -o IdentitiesOnly=yes \
  -o IdentityAgent=none \
  -o BatchMode=yes \
  -i ./deploy_key \
  -p "$VPS_SSH_PORT" \
  "$VPS_USER@$VPS_HOST" true

检查日志中的目标地址、端口和接受的公钥。首次连接仍需正常核验服务器主机密钥。如果这条命令失败,而原来的本地命令成功,应检查原命令依赖的 SSH 配置或其他身份。

在 VPS 上,确认对应公钥位于 VPS_USER 所属账户的 ~/.ssh/authorized_keys 中,并结合服务端 SSH 日志检查拒绝原因。还需要确认私钥对应的账户与登录时填写的用户名是否匹配。

4. 根据日志区分网络问题和传输问题

如果连接超时,检查 Hostinger 控制台中实际启用的网络规则以及 VPS 内部防火墙。若配置了仅允许本地公网 IP 访问 SSH,GitHub 托管 runner 的连接可能不在允许范围内。材料没有提供这些规则,因此需要检查确认。

如果日志显示认证已经成功,应转向文件传输:

  • docker-compose.yml 是否位于 checkout 后的工作区根目录。
  • VPS_DEPLOY_PATH 是否指向预期目录,建议使用明确的绝对路径。
  • 部署账户是否能创建目标目录、写入文件,以及覆盖已有文件。
  • VPS 是否提供可执行的 tar,远端临时目录是否可写。

scp-action 包含打包、上传和远端解包过程;普通 SSH 登录成功,并不能证明这些操作都有权限。其 target 参数应为目录,相关配置见 scp-action 文件传输参数。

5. 检查实际 YAML 的缩进

提供的代码片段没有保留 YAML 层级。如果仓库里的文件也是如此,需要修正;如果只是粘贴时丢失缩进,则不能据此认定这是失败原因。前面的步骤已经执行,也不能单凭展示格式推断真实 workflow 无法解析。

上传步骤在 jobs.build.steps 下应具有以下层级:

- name: Deploy docker-compose to VPS
  if: github.event_name != 'pull_request'
  uses: appleboy/scp-action@master
  with:
    host: ${{ secrets.VPS_HOST }}
    username: ${{ secrets.VPS_USER }}
    key: ${{ secrets.VPS_DEPLOY_USER_KEY }}
    port: ${{ secrets.VPS_SSH_PORT }}
    source: "docker-compose.yml"
    target: "${{ secrets.VPS_DEPLOY_PATH }}/"

这里保留原有 Action 引用,便于对照配置。后续可以将 @master 固定为经过验证的版本或完整 commit SHA,但更改引用本身不能证明当前问题已经修复。

6. 上传成功后再验证远端部署命令

先用原来的 ssh-action 连接参数执行最小命令,例如 whoami。确认远程命令可以运行后,再恢复 Docker 部署脚本。script 是远端执行的命令内容,参见 ssh-action 参数说明。

随后分别检查部署目录、docker compose 是否可用、部署账户是否有 Docker 操作权限,以及 GHCR 凭据是否有拉取目标镜像的权限。这些检查针对上传后的步骤,不能解释尚未执行 Docker 命令时的 SCP 失败。

要定位当前问题,还需要上传步骤的具体报错及其前后日志。分享日志时,去掉私钥、token 等敏感内容,保留错误文本和失败阶段。

备注:内容仅供参考。