记一次 TortoiseGit SSH 认证失败的排查与修复

2026-05-05

用 TortoiseGit 拉取自托管 Gitea 仓库时,弹出了 TortoiseGitPlink 的安全提示,确认后却提示 "No supported authentication methods available"。折腾了一圈才把问题全部解决,记录一下排查思路和解决方案。

环境

  • 远端:自托管 Gitea,端口 2222,SSH 协议
  • 客户端:Windows 11,TortoiseGit,OpenSSH
  • 密钥:Ed25519,最初为 PuTTY .ppk 格式

排查路线

1. 主机密钥缓存

首次连接 SSH 服务器时,TortoiseGitPlink 会弹出安全提示框,询问是否信任服务器的主机密钥。点击"是"即可缓存,这是正常的 SSH 安全机制。

2. Pageant 进程干扰

确认主机密钥后仍然失败,报 "Server refused our key"。检查任务管理器发现 Pageant(PuTTY 认证代理)正在后台运行。

TortoiseGitPlink 默认会优先使用 Pageant 中已加载的密钥,而不是远端设置中指定的 .ppk 文件。如果 Pageant 中没有加载正确的密钥,就会认证失败。

在 Pageant 中添加 .ppk 文件后仍然失败,决定放弃 TortoiseGitPlink,改用系统自带的 OpenSSH。

3. 密钥格式转换

TortoiseGit 的 SSH 客户端切换到 OpenSSH 后,需要 OpenSSH PEM 格式的私钥,而不是 .ppk

PuTTYgen 的 GUI 可以手动做转换,但命令行参数在这个版本中不理想。最后用 Python 的 cryptography 库直接解析 .ppk 文件:

import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization

with open("key.ppk") as f:
    lines = f.readlines()

# 提取 Private-Lines 的 base64 数据
private_b64 = ""
in_private = False
for line in lines:
    line = line.strip()
    if line == "Private-Lines: 1":
        in_private = True
        continue
    if in_private and line.startswith("Private-MAC:"):
        break
    if in_private:
        private_b64 += line

raw = base64.b64decode(private_b64)
length = int.from_bytes(raw[:4], "big")
seed = raw[4 : 4 + length]

key = Ed25519PrivateKey.from_private_bytes(seed)
pem = key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.OpenSSH,
    encryption_algorithm=serialization.NoEncryption(),
)

with open("key_openssh", "wb") as f:
    f.write(pem)

4. 文件权限

OpenSSH 对私钥文件的权限非常严格。从 .ppk 转换出来的 PEM 文件默认权限太宽松,OpenSSH 会直接拒绝加载:

Bad permissions. Try removing permissions for user: ...

用 PowerShell 修复:

$acl = Get-Acl -LiteralPath "key"
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    $env:USERNAME, "FullControl", "Allow"
)
$acl.SetAccessRule($rule)
Set-Acl -LiteralPath "key" -AclObject $acl

5. SSH 配置

为了避免每次在 URL 中硬编码端口 2222,把配置写进 ~/.ssh/config

Host gitea.logfun.xyz
    HostName gitea.logfun.xyz
    Port 2222
    User git
    IdentityFile "D:/path/to/key"
    IdentitiesOnly yes

远程 URL 也从 ssh://git@host:2222/... 改为 SCP 格式 git@host:...,端口由配置文件自动处理。

6. Git SSH 变体

切换 OpenSSH 后拉取,遇到:

fatal: ssh variant 'simple' does not support setting port

Git 默认的 SSH 变体是 simple,不支持在 URL 中解析端口。解决方案是全局配置:

git config --global ssh.variant ssh
git config --global core.sshCommand "ssh -F C:/Users/$USER/.ssh/config"

这样就告诉 Git 使用完整的 OpenSSH 功能,包括端口解析和配置文件。

Linux 端配置同一密钥

同一密钥要从 Windows 带到 Linux 用,或者直接在 Linux 上生成,步骤少得多:

  1. 私钥放到 ~/.ssh/,权限必须 600

    cp /path/to/id_ed25519 ~/.ssh/gitea_key
    chmod 600 ~/.ssh/gitea_key
  2. ~/.ssh/config,端口和密钥一起管理:

    Host gitea.logfun.xyz
      HostName gitea.logfun.xyz
      Port 2222
      User git
      IdentityFile ~/.ssh/gitea_key
  3. 验证:

    ssh -T git@gitea.logfun.xyz
    # Hi there, lss6378! You've successfully authenticated...

之后任何 git 操作都走 SSH 协议,无需每次指定密钥或端口。

背后原理

~/.ssh/config 不是玄学,是 OpenSSH 客户端的行为规则。每次执行 sshgit 时,客户端按顺序扫描 config 文件,找到第一个匹配 Host 字段的条目,把下面的参数套进去。所以上面写 Host gitea.logfun.xyz,访问这个主机时 Port、User、IdentityFile 自动生效。

权限 600 是硬性要求。OpenSSH 收到私钥后先检查权限,如果其他用户或组有读权限,直接拒绝加载。这不是 bug,是防止其他进程或用户读到私钥。

公钥认证的过程是:服务器用预先注册的公钥生成一个随机挑战,客户端用私钥签名后返回,服务器验证签名通过即放行。全程私钥不出本地网络。

远程 URL 的 SCP 格式(git@host:repo.git)和 SSH 格式(ssh://git@host:port/repo.git)区别在于:前者靠 config 解析端口和密钥,后者把一切写在 URL 里。用了 config 之后,SCP 格式更简洁。

最终配置

项目
SSH 客户端C:\Windows\System32\OpenSSH\ssh.exe
远程 URLgit@gitea.logfun.xyz:user/repo.git
私钥OpenSSH PEM 格式(非 .ppk
端口管理~/.ssh/config

总结

这次排查涉及了六个独立的环节,任何一个出问题都会导致认证失败:

  1. 主机密钥缓存 — 首次连接必须确认
  2. Pageant 冲突 — 运行 Pageant 时 TortoiseGitPlink 的行为会改变
  3. 密钥格式.ppk 和 OpenSSH PEM 不互通
  4. 文件权限 — OpenSSH 对私钥有严格的 ACL 要求
  5. SSH 配置 — 用 config 文件管理端口和密钥,而不是 URL
  6. Git SSH 变体 — 默认 simple 模式功能受限

对于 Windows 上的 TortoiseGit 用户,如果使用非标准 SSH 端口(2222、443 等),直接切换到系统 OpenSSH 并通过 ~/.ssh/config 管理配置,比纠结 TortoiseGitPlink + Pageant 要省心得多。

https://blog.logfun.xyz/blog/feed.xml