ssh-agent 是一个在内存里保管你已解锁私钥、并用它们对认证挑战进行签名的后台程序,这样你每个会话只需输入一次密钥的密码短语,而不是每次连接都输一遍。 ssh-add 则是把密钥加载进去的命令。
你做了正确的事,给自己的 SSH 密钥设了密码短语——然后午饭前你已经第五次输它了。解决办法几十年前就有:SSH 代理。把密钥解锁一次,之后的每次连接都会无声地完成认证。问题在于,这个“一次”只有在三件事同时成立时才管用:代理确实在运行、你的 shell 能找到它、密钥已经加载——这是三件独立的事,各有各的出错方式。本文是实操指南:在各个操作系统上启动代理、用 ssh-add 驱动它、把整件事做成自动的,以及解读几乎囊括所有代理麻烦的那两个报错。
一段话讲清代理是什么
ssh-agent 是一个小小的后台进程,在内存里保管你已解密的私钥——绝不落盘。当服务器向你发起挑战时,ssh 把挑战交给代理,代理用内存中的密钥给它签名,返回去的只有签名——密钥本身从不离开代理,从不经过网络,在磁盘上也始终保持加密。加载密钥时提示一次密码短语,之后的每次连接都由内存来应答。整个思维模型就这么简单:代理是一个签名服务,而不是谁都能读取的密钥库——它守着秘密,只对外交出签名。(正是这一条特性也埋下了安全隐患,文末会专门讲它。)
在 Windows 上?先看 Windows 指南
如果你是从 error connecting to agent: No such file or directory 找过来的,这条报错是 Windows 专属的:它来自 Git Bash / MSYS2 的 ssh,它的修法也一样——OpenSSH Authentication Agent 服务、Git Bash 自带的代理,以及错误 1058。这些全都在 Windows 上的 ssh-agent 里。下面讲的是跨平台的 ssh-agent 与 ssh-add 工作流,它所解读的报错是 OpenSSH 客户端自己的:Could not open a connection to your authentication agent 和 The agent has no identities。
按操作系统启动代理
Linux:eval "$(ssh-agent -s)"
在桌面登录的环境下,你几乎肯定已经有一个代理在跑了——要么是一个 systemd 用户服务,要么是 GNOME Keyring 在扮演这个角色——并且 SSH_AUTH_SOCK 已经为你整个会话导出好了。用 ssh-add -l 测一下。如果没有任何回应(在纯 SSH 登录、容器里或极简服务器上很常见),就手动启动一个:
eval "$(ssh-agent -s)"
eval 很关键。ssh-agent -s 只是打印出 SSH_AUTH_SOCK 和 SSH_AGENT_PID 这两个变量;是 eval 把它们真正设进你当前的 shell,ssh 才找得到那个套接字。光跑一个裸 ssh-agent、然后纳闷为什么什么都没变,是“启动”出一个任何工具都看不见的代理的最常见方式。
macOS:launchd 替你运行
launchd 从你登录起就替你运行着一个代理——没什么要启动的。macOS 独有的好处是下面讲的钥匙串集成。
在 Windows 上启动 ssh-agent
现代 Windows 自带一个 OpenSSH Authentication Agent 服务,但它被设为手动启动,默认并不运行。在以管理员身份打开的 PowerShell 里启用一次:
Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
此后,ssh-add 在任何终端里都能用——PowerShell、cmd,或 Git Bash 下的 Windows OpenSSH 客户端——而且代理能扛过重启。有一个坑:Git for Windows 自带它自己的 ssh 和 ssh-agent,在 Git Bash 里你 PATH 上的 ssh 可能就是那一个,连的是另一个独立的代理。如果 ssh-add -l 在 Git Bash 和 PowerShell 之间结果对不上,就把 Git 指向系统的客户端,让两边共用同一个代理:
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
完整的分步教程——服务、Git Bash,以及在登录时加载密钥——都在 Windows 上的 ssh-agent里。
用 ssh-add 驱动它
加载密钥:ssh-add ~/.ssh/id_ed25519
ssh-add # load the default keys (id_ed25519, id_rsa, …)
ssh-add ~/.ssh/id_ed25519 # load a specific key (prompts for the passphrase once)
不给路径时,ssh-add 会从 ~/.ssh 加载那些默认命名的密钥;给它一个路径则加载指定的密钥。无论哪种方式,它都只问一次密码短语,然后把解密后的密钥留在内存里。
查看已加载的内容:ssh-add -l
ssh-add -l # list fingerprints of loaded keys
ssh-add -L # print the full public keys
ssh-add -d ~/.ssh/id_ed25519 # remove one key
ssh-add -D # remove all keys
ssh-add -l 是任何认证问题的第一个诊断步骤:如果你以为会在的那把密钥不在这个列表里,那无论别的看上去多么正常,代理都没法用它来签名。
设置 ssh-agent 超时:ssh-add -t
有两个参数值得养成习惯:
-t设置有效期。ssh-add -t 3600 ~/.ssh/id_ed25519把密钥加载一小时;过后代理就忘掉它,下次再用会重新提示。一把在内存里解锁、随机器连续运行三周的密钥,在你早已不再想着它之后,依然是一把能被使用的密钥。- **
-c要求确认。**每次签名请求在代理应答之前都会弹出一个本地提示。略微烦人,但对高价值密钥来说恰到好处。
让它自动化:AddKeysToAgent
只要在 ~/.ssh/config(英文)里加一行,手动 ssh-add 这套仪式就消失了:
Host *
AddKeysToAgent yes
现在,第一次需要密钥的 ssh 会提示一次密码短语,并顺带把密钥加载进代理。不用再记一条单独的命令——那个提示就是不再反复出现了。你也可以给它设个有效期(AddKeysToAgent 1h),不用加参数就得到 -t 的效果。
**macOS:借助钥匙串扛过重启。**默认情况下代理重启后会忘掉密钥。macOS 可以把密码短语存进你的登录钥匙串,这样密钥重新加载时就不用再输:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
并在 ~/.ssh/config 里:
Host *
AddKeysToAgent yes
UseKeychain yes
此后,密码短语的问题会在登录时由钥匙串来回答,你再也不会看到它了——而磁盘上的密钥文件依然保持加密。
人人都会撞上的两个报错
Could not open a connection to your authentication agent(无法连接到你的认证代理)——ssh-add 找不到代理。要么一个都没在跑,要么有一个在跑、但这个 shell 没有设置 SSH_AUTH_SOCK。解决办法就是上面按操作系统的启动方式:Linux 上的 eval "$(ssh-agent -s)" 那一行(先确认你的桌面会话是不是已经提供了一个——每个终端都启动第二个代理,只会把已加载的密钥散落到各个进程里),Windows 上的启动服务。在 cron 任务和 CI 里你也会碰到它,因为那里从来没有一个登录会话导出过这个变量。
The agent has no identities.(代理里没有任何身份)——相反的问题:代理没毛病,但什么都没加载。ssh-add 一下你的密钥。如果它本来加载过、后来消失了,那是某个 -t 有效期到期了,或者代理重启了——而这恰恰就是 AddKeysToAgent yes 和 macOS 钥匙串要解决的。
还有第三种情况:代理看上去完美无缺——ssh-add -l 显示了密钥——但服务器仍然说 Permission denied (publickey)。那就不再是代理的问题了;照着 publickey 排查清单走一遍——用户错了、提供的密钥错了、authorized_keys 权限不对。
一个解锁的代理就是一份活着的凭据
代理从不泄露密钥——但那个套接字是一个签名预言机:它会为任何能连上它的人签名,而且从不过问是谁在请求。在你自己的机器上,这没问题——套接字位于一个只有你能读取、每个用户各自私有的目录里。危险在于混淆代理人(confused deputy)这种情形:当套接字被你意料之外的东西连上时——共享堡垒机上一个被转发过来的代理,或者一个以你的用户身份运行、你却并不信任的进程——它就能以你的身份,向你密钥能到达的每一台服务器完成认证,而自始至终都看不到密钥。密钥从不泄露只是保障的一半;控制谁能使用它才是另一半——这也是为什么在代理卫生上花点小功夫是值得的。
- 给密钥设个有效期(用
-t,或在AddKeysToAgent上设有效期),让一个空闲的代理自己冷却下来。 - 离开座位时锁住代理:
ssh-add -x用一个密码把它锁起来,ssh-add -X解锁;ssh-add -D则干脆把所有东西都清掉。 - 对能连到生产环境的密钥用
-c。 - 不要默认转发代理。
ssh -A——即 ssh agent forwarding(SSH 代理转发)——会把你密钥的实时使用权暴露在远程主机上,那里任何有 root 权限的人,在你会话持续的这段时间里都能以你的身份签名。这一点值得单独读一篇:SSH 代理转发既方便又危险(英文)。
Termalin 如何处理这件事
Termalin 把整套工作流都内建了进来,还把那些扎手的边角打磨掉了。它的密钥代理把你的密钥解锁一次,并代你签名——没有要启用的操作系统服务,没有 eval 那一行,也没有每个 shell 各自丢失的套接字。密码短语缓存默认带一个重新上锁的超时,也就是把 ssh-add -t 那个习惯替你强制执行,而不用你去记。而且因为它说的是标准的 ssh-agent 协议,git 和你现有的命令行工具都能原封不动地通过它来签名——它替换掉的是那套仪式,而不是你的工具链。
Termalin 是一款免费、跨平台的 SSH 客户端,其内置代理把你的密钥解锁一次、并按计时器重新上锁——立即下载,或了解它如何安全地处理你的密钥。