这是我每次换电脑都会回来翻的私人备忘录。 它最早只为了解决 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 举例。你如果采用了自定义文件名,所有 IdentityFile、ssh-add 和 .pub 路径都要一起替换。
② 启动 ssh-agent(一次性配置)
讲讲 ssh-agent 在这里扮演什么角色。
ssh-agent 是一个常驻进程,负责「在内存里解密私钥并代为签名」。
它存在的意义:
- 你输一次 passphrase,agent 把解密后的密钥握在内存里
- 之后所有需要这个密钥的命令(
git push、ssh 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 方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 桌面 keychain | GNOME/KDE 桌面 | 零配置、图形化解锁、开机持久 | 仅桌面环境 |
| **用户级复用 + 有效期(推荐)** | **个人开发服务器、tmux 环境** | 跨登录复用、无 systemd 依赖、可限制解锁时间 | 需要维护一小段初始化脚本 |
③ 把私钥加进 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 <你的GitHub用户名>! 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 ssh 和 core.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` 一直问 passphrase | Windows | ① `ssh-add -l` 看 agent 是否持有;② 比较 `where.exe ssh` 和 `core.sshCommand`,确认 Git 没走另一套 OpenSSH |
| `git push` 一直问 passphrase | Linux | ① `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 账号里的公钥,不要直接覆盖或重复生成 |
| 服务器能登录,但服务器不能拉 GitHub | Linux | 这是两条身份链;服务器作为客户端还需要自己的 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 步走:
- 为当前 Windows 设备生成一把新密钥
- 管理员 PowerShell:
Set-Service ssh-agent -StartupType Automatic+Start-Service ssh-agent ssh-add $env:USERPROFILE\.ssh\id_ed25519(输入 passphrase)- 把公钥加到 GitHub,再用
ssh -T git@github.com验证 - 只有
ssh -T正常而git push仍询问时,才检查并修正core.sshCommand
Linux 服务器(用户级 agent + 有效期)
5 步走:
- 服务器要访问 GitHub,就在服务器上生成自己的密钥;不要顺手复制笔记本私钥
- 把公钥加到 GitHub,或按仓库范围配置 Deploy Key
- 安装本文的
agent-init.sh,由.bashrc加载,不覆盖原有 shell 配置 ssh-add -t 8h ~/.ssh/id_ed25519,按需要调整有效期ssh -T git@github.com→git fetch验证;需要写权限时再测git push
Linux 桌面
3 步走:
- 为当前 Linux 桌面生成一把新密钥
ssh-add ~/.ssh/id_ed25519(图形界面输 passphrase,勾选保存到 keychain)ssh -T git@github.com验证
下次换机器,我会怎么做
写到最后,我真正想给下次的自己留下的,其实没有那么复杂。
新机器到了,就在新机器上生成一把新密钥,把公钥交给 GitHub 或要登录的服务器;旧机器不用了,就把它对应的公钥撤销。私钥不搬家,也不让一把钥匙替所有设备兜底。
平时需要记住的也只有几件事:passphrase 保护落在磁盘上的私钥,ssh-agent 只是替我暂时记住它;Windows 明明能 ssh、Git 却不行时,再去查是不是走了另一套 ssh.exe;Linux 出问题,先看文件权限和 agent 到底还在不在。
我以前总想把 SSH 配成一套“一劳永逸”的东西。现在反而觉得,能随时生成、验证、撤销,比永远维护同一把钥匙更省心。以后再换电脑,我回来照着这几步做就够了。
