这是我每次换电脑都会回来翻的私人备忘录。 它最早只为了解决 git push 反复询问 passphrase,后来又陆续碰上多台设备、多把密钥,以及 Linux 服务器反过来访问 GitHub。索性把这些真正踩过的坑补在一起:不是从头解释 SSH,而是让我下次换机器时还能照着做。

这篇会讲什么

先让你大致知道这篇博客的结构,再决定要不要往下读:

  • 心智模型:搞懂 SSH 的「方向」,理清密钥对到底归属谁
  • 准备工作:适用场景 + 前置检查
  • 五步操作:准备私钥 → 启 ssh-agent → 加进 agent → 让 Git 走对 ssh → 公钥贴 GitHub
  • 验证全链路 + 平台特殊情况(Windows / Linux 各自的坑)
  • 服务器作为客户端:为什么“我能登录服务器”和“服务器能拉 GitHub”是两条链路
  • 日常使用 + 排错速查表
  • 压缩流程:5 步速查,回头翻只看这里就够了
  • 我最后留下的做法:下次换机器时,到底还需要记住什么

写在最前:先搞懂 SSH 的「方向」

说实话,我自己一开始也以为 SSH 是「服务器之间的通讯协议」,每台机器都生成一对密钥放着就好。后来踩坑多了才意识到——这个模型对了一半,错了另一半

真正的模型:C/S,不对称

SSH 走的是 客户端-服务器(C/S)模型,两端角色不对等

  • 客户端 = 发起连接的那一方(比如你的笔记本想 git push
  • 服务器 = 被连接的那一方(比如 GitHub、云主机、跳板机)
[客户端 = 持私钥的发起方]  ──SSH──>  [服务器 = 存公钥的接收方]
    ↑                                    ↑
   私钥永不离开本机                  authorized_keys / GitHub 设置页

一句话口诀

谁发起连接,谁持有私钥;谁被连接,谁存对方的公钥。

由这个模型推导出的三条铁律

  • 密钥对的归属单位是「身份」,不是「机器类型」

    不是「每个服务器」要生成一对,而是「每一台会发起 SSH 连接的机器」要生成一对。笔记本一对、工作站一对、CI 节点一对;同一台机器也可以为不同身份建多对(个人 GitHub + 公司 GitHub)。

  • 私钥默认留在生成它的设备上

    不通过邮件、聊天、密码库或云盘搬运。新机器就生成一对新密钥,再把新公钥挂到目标服务上;这样哪台设备丢了,就只撤销哪一把。即使是同一台电脑重装,我现在也把它当成一个新身份,而不是想办法把旧私钥救回来。

  • 公钥不是秘密,但必须放在「被连接方」

    在 GitHub,是网页里的 Settings → SSH Keys;在云主机,是 ~/.ssh/authorized_keys。本质都是「服务器的白名单」。

还有一个容易绕进去的点:“客户端 / 服务器”描述的是这一次连接里的角色,不是机器的永久身份。

Windows 笔记本 ──SSH──> Linux 云服务器
Linux 云服务器 ──SSH──> GitHub

第一条链路里,笔记本持私钥,云服务器把公钥放进 authorized_keys;第二条链路里,云服务器反而要持有自己的私钥,GitHub 保存对应公钥。能登录服务器,不代表服务器就能访问 GitHub——这是两次认证。

顺便把两个长得很像、实际方向相反的文件记住:

  • id_ed25519我拿什么身份去连接别人
  • authorized_keys哪些人可以连接我

这一节是整篇教程的「为什么」。后面为什么要 chmod 600、为什么要 ssh-agent、为什么有时要指定 Git 使用哪个 ssh.exe,回过头看都在维护这条身份链。


适用场景

这篇文章是写给「这一刻的我」的,你如果命中了下面任一条,也能照抄:

  • 重装 Windows / Linux / 拿到新电脑,想重新建立 SSH 工作环境
  • 想让 GitHub / 服务器 SSH 走 ssh-agent + passphrase 双保险
  • 在服务器上配 ssh-agent 被 systemd 报错搞烦了,想要一个「不依赖 systemd 也能用」的方案

前置检查

  • Windows 10 / 11(系统自带 OpenSSH) 或 Linux(OpenSSH 客户端,绝大多数发行版默认装好)
  • Git 已安装

① 为这台机器生成一把新密钥

以前我把“换机器”理解成恢复同一个身份,所以总想着把旧私钥搬回来。现在这个模型反而更简单:一台会发起 SSH 连接的机器,就是一个独立身份;机器变了,密钥也重新生成。

Windows 和 Linux 都可以直接用:

ssh-keygen -t ed25519 -C "备注这个密钥对的信息"
  • 只有一把密钥时可以用默认的 ~/.ssh/id_ed25519;多设备、多用途时,我会用 id_ed25519_github_<设备名> 这种能看懂的名字

  • 个人设备上的私钥建议设强 passphrase

    没 passphrase 的私钥 = 一个明文凭证。一旦本机被入侵,攻击者就可能直接拿到你的 GitHub 身份。passphrase 给本机磁盘上的私钥再加一道保护。

  • 生成完别忘了把新公钥贴到 GitHub(步骤 ⑤)

旧设备如果已经不用了,就去 GitHub 或目标服务器撤销它的公钥。这里不再讨论私钥备份:我需要保留的是重新建立身份的流程,而不是某一把必须跨机器延续的钥匙。

为了让后面的命令不变得满屏都是占位符,正文仍用 id_ed25519 举例。你如果采用了自定义文件名,所有 IdentityFilessh-add.pub 路径都要一起替换。


② 启动 ssh-agent(一次性配置)

讲讲 ssh-agent 在这里扮演什么角色。 ssh-agent 是一个常驻进程,负责「在内存里解密私钥并代为签名」。 它存在的意义:

  • 你输一次 passphrase,agent 把解密后的密钥握在内存里
  • 之后所有需要这个密钥的命令(git pushssh user@host),都让 agent 在背后帮忙签名
  • 你不用每次都输一遍 passphrase

本质是「用进程隔离,换便利性」——比把密钥裸放着安全,比每次手敲 passphrase 省心。

Windows

管理员 PowerShell:

Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent

之后服务会随系统启动,通常不需要每次手动再跑。 验证:

Get-Service ssh-agent

看到 Status: Running 即可。 Windows 这里和 Linux 最大的差别,是 agent 由系统服务托管,不跟某一个终端窗口一起结束。至于重启后是否还能看到已经加入的密钥,我不再把某个版本下的注册表实现写成铁律;配完后用 ssh-add -l 验证,比记住一条内部实现更可靠。

Linux

先判定场景(这一步决定后面的方案)

桌面 Linux(GNOME / KDE / Ubuntu Desktop / Fedora Workstation)→ 多数情况 ssh-agent 已由桌面环境自动启动并集成 keychain 服务器 / 容器 / 跳板机(云 ECS、VPS、Docker、纯 SSH 登录的 Linux)→ 往往没有桌面 keychain,需要自己确认或配置 判定命令:

echo $SSH_AUTH_SOCK
  • 有输出(一个 socket 路径) = 桌面,直接跳到步骤 ③
  • 空 = 服务器,往下看

桌面 Linux

桌面环境已经在帮你管 agent,通常什么都不用做。 首次 ssh-add 可能由图形界面接管 passphrase;是否能保存、何时自动解锁,要看具体桌面环境和 keychain 配置,不把它当成所有 Linux 桌面的统一行为。

服务器 / 非桌面 Linux —— 一个用户复用一个 agent

这一段是我这次真正需要改正的地方。

我原来把它叫作 per-SSH-session,还写成“登出时随会话自然清理”。但只要 ssh-agent 是作为独立进程启动的,它就不一定跟着那次 SSH 连接结束;再把环境变量写进 agent-env,后续登录实际上是在复用同一个 agent。名字和真实生命周期对不上,安全判断自然也会跟着偏。

我现在采用的模型更老实:每个 Linux 用户复用一个 agent,服务器重启后重建;加入私钥时用 -t 限制解锁窗口。 这样 tmux 和多次 SSH 登录都能接上它,又不会让密钥无限期留在 agent 里。

① 创建初始化脚本
cat > ~/.ssh/agent-init.sh <<'EOF'
#!/usr/bin/env bash

SSH_ENV="$HOME/.ssh/agent-env"

if [ -r "$SSH_ENV" ]; then
    . "$SSH_ENV" >/dev/null
fi

ssh-add -l >/dev/null 2>&1
status=$?

case "$status" in
    0|1)
        # 0:agent 中已有密钥;1:agent 正常,但还没有密钥
        ;;
    2)
        # 无法连接旧 agent,启动一个新的
        umask 077
        ssh-agent -s > "$SSH_ENV"
        . "$SSH_ENV" >/dev/null
        ;;
esac

export SSH_AUTH_SOCK SSH_AGENT_PID
EOF

chmod 700 ~/.ssh/agent-init.sh

这里没有把 agent 硬塞进 systemd,也没有假设某个发行版一定自带可 enable 的 user unit。脚本只做两件事:旧 agent 还能连就复用,连不上就新建。

② 让交互式 Bash 自动接上它

把下面一行放进 ~/.bashrc

[ -f "$HOME/.ssh/agent-init.sh" ] && . "$HOME/.ssh/agent-init.sh"

然后立即应用并检查:

source ~/.bashrc
echo "$SSH_AUTH_SOCK"
ssh-add -l

如果看到 The agent has no identities.,不是失败,而是 agent 已经活着,只是里面还没有密钥。

③ 解锁密钥,但给它一个期限
ssh-add -t 8h ~/.ssh/id_ed25519

-t 8h 表示这把密钥在 agent 中保留 8 小时,之后自动移除。八小时不是安全标准,只是我给“一天开发时间”选的边界;你可以按自己的工作节奏缩短。

ssh-add -l   # 查看
ssh-add -D   # 主动清空
④ 和 tmux 一起用
source ~/.bashrc
ssh-add -t 8h ~/.ssh/id_ed25519
tmux new -s dev

SSH 断开不会结束 tmux 里的任务,后续登录也能通过 agent-env 找回同一个 agent。服务器重启后,tmux 和 agent 都会结束,再重新解锁一次即可。

⑤ 别再为了 agent 覆盖整份 .bash_profile

我第一次配完服务器,重新登录后 prompt 变白、alias 全没,还以为系统坏了。原因不是 agent,而是我新建了 .bash_profile,Bash 发现它之后就没有再走原来的 .profile 路径。

如果你的 ~/.bash_profile 已经存在,要确认它会加载 .bashrc

[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"

重点不是复制一份完整模板,而是别让一段 agent 配置接管你原来的 shell 初始化文件

Linux 方案对比

方案适用场景优点缺点
桌面 keychainGNOME/KDE 桌面零配置、图形化解锁、开机持久仅桌面环境
**用户级复用 + 有效期(推荐)****个人开发服务器、tmux 环境**跨登录复用、无 systemd 依赖、可限制解锁时间需要维护一小段初始化脚本
**我的选择规则**:桌面先用现成 keychain;个人开发服务器用用户级 agent,再用 `ssh-add -t` 限时;自动部署则不要照搬个人账号私钥,优先用单仓库 Deploy Key 或权限更小的凭据。

③ 把私钥加进 agent

下面这组是 Windows 和桌面 Linux 的常规命令。无桌面服务器如果采用了上面的限时方案,就继续使用 ssh-add -t 8h,不要在这里又把期限去掉。

Windows

ssh-add $env:USERPROFILE\.ssh\id_ed25519

Linux

ssh-add ~/.ssh/id_ed25519
  • 输入这把密钥生成时设置的 passphrase
  • 验证(两平台一致):
ssh-add -l

能看到指纹即成功。 Linux 桌面通常用 GNOME Keyring / KDE Wallet 集成,首次 ssh-add 会弹图形界面问 passphrase,可勾选「保存到 keychain」—— 之后开机自动解锁,无需每次 ssh-add


④ 确认 Git 走的是哪一个 OpenSSH

Windows

先看 Git 有没有被单独指定过 SSH:

where.exe ssh
git config --global --get core.sshCommand

如果 PowerShell 的 ssh -T 正常、git push 却仍然反复问 passphrase,再执行:

git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"

Git for Windows 自带的 SSH 和 Windows 系统 OpenSSH 可以同时存在;如果密钥加在 Windows agent 里,Git 却调用了另一套 ssh.exe,两边就接不上。我第一次没意识到调用链分叉,被反复问 passphrase 问到怀疑人生。

但这条是排错后的修复,不是每台 Windows 机器都必须先改一次的仪式。能正常工作就不动全局配置。

Linux

不需要这一步。Linux 上 git 直接调用系统 /usr/bin/ssh,天然连接系统 ssh-agent。


⑤ 把公钥贴到 GitHub

这是「把公钥发给服务器」的具体操作——回想开头那条铁律:公钥放在被连接方。GitHub 在这里就是「被连接方」。 复制公钥到剪贴板: Windows

Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub | clip

Linux(X11)

xclip -sel clip < ~/.ssh/id_ed25519.pub

Linux(Wayland)

wl-copy < ~/.ssh/id_ed25519.pub

Linux 兜底(直接显示再手动复制):

cat ~/.ssh/id_ed25519.pub

去 GitHub → Settings → SSH and GPG keys → New SSH key,粘贴保存。


验证全链路

两平台一致

ssh -T git@github.com

看到 Hi &lt;你的GitHub用户名&gt;! You've successfully authenticated... = 成功 任意 git 仓库里:

git fetch

不问 passphrase = 读取仓库的链路已经打通。需要确认写权限时,再在确实有提交要推送的情况下执行 git push,不必为了测试制造一个空提交。


平台特殊情况

Windows:Git Bash 和 PowerShell 可能走了两套 ssh

如果 PowerShell 里验证成功,Git Bash 里却重新问 passphrase,先别怀疑密钥坏了。更常见的原因是两边调用了不同的 ssh.exe,各自连接自己的 agent 环境。

想明确用 Windows 系统 OpenSSH 测试,可以写完整路径:

/c/Windows/System32/OpenSSH/ssh.exe -T git@github.com

再结合前面的 where.exe sshcore.sshCommand 判断,不要把“某个终端里失败”直接等同于“Git 一定失败”。

Linux:自动加载密钥

想让 ssh-agent 在首次用到密钥时自动加载(少打一次 ssh-add),编辑 ~/.ssh/config

Host github.com
    User git
    AddKeysToAgent yes
    IdentitiesOnly yes
    IdentityFile ~/.ssh/id_ed25519

之后第一次 git push 时输入一次 passphrase,在当前 agent 持有这把密钥期间不再重复输入。

我现在不再为一把 GitHub 密钥写 Host *。限制到 github.com 后,SSH 不会拿着同一把身份去尝试所有主机,多把密钥时也更容易排错。如果你用了自定义文件名,把 IdentityFile 一并改掉。


日常使用

Windows

通常只在首次配置时 ssh-add 一次,之后先让 agent 服务接管。 但不同 OpenSSH / Git 组合下,重启后的行为不要靠猜,直接检查:

Get-Service ssh-agent
ssh-add -l

以下情况需要重新 ssh-add

  • 首次配置、agent 里还没任何密钥(也可以靠 AddKeysToAgent yes 在第一次 ssh host 时自动喂入)
  • 主动 ssh-add -D 清空过 agent
  • 重启后 ssh-add -l 已经看不到密钥
ssh-add $env:USERPROFILE\.ssh\id_ed25519

安全含义:密钥留在 agent 的时间里,能以你的用户身份访问 agent 的进程就可能请求它签名。便利性和本机失守后的影响范围是一组真实取舍,不是“设置了 passphrase 就永远安全”。

Linux

  • 桌面环境(GNOME / KDE)+ keychain:通常由桌面接管,实际持久化行为以当前 keychain 配置为准
  • 服务器(按用户级复用配置完成): 在 ssh-add -t 8h 的有效期内,多次 SSH 登录和 tmux 可以复用;到期、主动清空或重启后重新解锁
  • 未配 AddKeysToAgent yes:需要时手动 ssh-add -t 8h ~/.ssh/id_ed25519

排错速查

现象平台检查 / 修复
`ssh-add` 报 `Error connecting to agent`Windows`Get-Service ssh-agent` 是否 Running;不在则回到步骤 ②
`ssh-add` 报 `Could not open a connection to your authentication agent`Linux`echo "$SSH_AUTH_SOCK"` 看是否为空;按本文服务器方案执行 `source ~/.bashrc`,桌面环境则检查 keychain / agent 是否启动
`Permissions ... are too open` 报错Linux`chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519`
`git push` 一直问 passphraseWindows① `ssh-add -l` 看 agent 是否持有;② 比较 `where.exe ssh` 和 `core.sshCommand`,确认 Git 没走另一套 OpenSSH
`git push` 一直问 passphraseLinux① `ssh-add -l` 看 agent;② `ssh -G git@github.com | grep -i identityfile` 看最终选中的密钥
`ssh -T` 在 PowerShell 通、Git Bash 又询问Windows多半是两套 OpenSSH / agent 环境;用完整路径测试,再决定是否设置 `core.sshCommand`
GitHub 报 `Permission denied (publickey)`Windows / Linux核对最终 `IdentityFile`、agent 指纹与 GitHub 账号里的公钥,不要直接覆盖或重复生成
服务器能登录,但服务器不能拉 GitHubLinux这是两条身份链;服务器作为客户端还需要自己的 GitHub 密钥、Deploy Key 或其他凭据
重启后 `ssh-add -l` 显示 `no identities`Linux无桌面服务器重启后 agent 和已加载密钥都会消失;重新加载 `.bashrc` 并执行 `ssh-add -t 8h`
重启后 `ssh-add -l` 显示 `no identities`Windows先确认服务 Running,再重新 `ssh-add`;不要依赖某个版本的内部存储路径来判断
自建 `.bash_profile` 后 SSH 登录用户名/主机名变白、alias 全失效Linux`.bash_profile` 覆盖了 `.profile`,导致 `.bashrc` 没被 source。在 `.bash_profile` 第一行加 `[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"`

压缩流程(换电脑速查)

如果你只是回来翻速查表,看这一节就够了。

Windows

5 步走

  1. 为当前 Windows 设备生成一把新密钥
  2. 管理员 PowerShell:Set-Service ssh-agent -StartupType Automatic + Start-Service ssh-agent
  3. ssh-add $env:USERPROFILE\.ssh\id_ed25519(输入 passphrase)
  4. 把公钥加到 GitHub,再用 ssh -T git@github.com 验证
  5. 只有 ssh -T 正常而 git push 仍询问时,才检查并修正 core.sshCommand

Linux 服务器(用户级 agent + 有效期)

5 步走

  1. 服务器要访问 GitHub,就在服务器上生成自己的密钥;不要顺手复制笔记本私钥
  2. 把公钥加到 GitHub,或按仓库范围配置 Deploy Key
  3. 安装本文的 agent-init.sh,由 .bashrc 加载,不覆盖原有 shell 配置
  4. ssh-add -t 8h ~/.ssh/id_ed25519,按需要调整有效期
  5. ssh -T git@github.comgit fetch 验证;需要写权限时再测 git push

Linux 桌面

3 步走

  1. 为当前 Linux 桌面生成一把新密钥
  2. ssh-add ~/.ssh/id_ed25519(图形界面输 passphrase,勾选保存到 keychain)
  3. ssh -T git@github.com 验证

下次换机器,我会怎么做

写到最后,我真正想给下次的自己留下的,其实没有那么复杂。

新机器到了,就在新机器上生成一把新密钥,把公钥交给 GitHub 或要登录的服务器;旧机器不用了,就把它对应的公钥撤销。私钥不搬家,也不让一把钥匙替所有设备兜底。

平时需要记住的也只有几件事:passphrase 保护落在磁盘上的私钥,ssh-agent 只是替我暂时记住它;Windows 明明能 ssh、Git 却不行时,再去查是不是走了另一套 ssh.exe;Linux 出问题,先看文件权限和 agent 到底还在不在。

我以前总想把 SSH 配成一套“一劳永逸”的东西。现在反而觉得,能随时生成、验证、撤销,比永远维护同一把钥匙更省心。以后再换电脑,我回来照着这几步做就够了。