自己电脑上的 SSH 密钥:本地生成、passphrase、备份与泄露后的自救
大多数 SSH 密钥文章是从服务器那侧写的:authorized_keys 里放什么、员工离职后怎么回收访问、怎么审计哪把指纹从哪台机器登录过。那是集群层面的问题,本文不写它。本文只写连接属于你自己的那一半——笔记本电脑上的私钥文件、保护它的 passphrase、归档里的备份副本、第二台设备,以及你发现密钥丢了或者泄露了的那一个小时里到底该做什么。
这两半的失效方式不一样,所以要分清。服务器那侧,你撤销的是一把自己留有记录的公钥;客户端这侧,从头到尾只有你见过私钥,没有任何人能替你撤销它。而你的客户端在连接时不会先问你一句,它会直接把密钥递给服务器——所以一个脱离你掌控的私钥文件(笔记本被盗、被你忘掉的同步目录、在错误目录里的一次 git commit)已经是一份落在别人手里的可用凭据。
私钥是你唯一需要自己保管的秘密
普通 OpenSSH 客户端的数据都在 ~/.ssh。私钥必须是 600:只要组或其他用户可读,客户端会直接拒绝使用并报 Permissions 0644 for '...' are too open,然后退而寻找别的密钥。公钥不是秘密,通常是 644:
$ stat -c '%a %n' /tmp/demo_key_rotation /tmp/demo_key_rotation.pub
600 /tmp/demo_key_rotation
644 /tmp/demo_key_rotation.pub权限之后,下一件该知道的事是客户端会主动提供哪些文件。ssh -G 会打印合并了全部默认值和 ~/.ssh/config 之后的最终配置,这是看清「下一次连接会尝试什么」最诚实的办法:
$ ssh -G github.com | grep -E '^(identityfile|identitiesonly)'
identitiesonly no
identityfile ~/.ssh/id_rsa
identityfile ~/.ssh/id_ecdsa
identityfile ~/.ssh/id_ecdsa_sk
identityfile ~/.ssh/id_ed25519
identityfile ~/.ssh/id_ed25519_sk
identityfile ~/.ssh/id_xmss
identityfile ~/.ssh/id_dsaidentitiesonly no 是默认值,也就是说上面每一个默认命名的密钥都可能被递给你连接的每一台主机。这就是为什么一把名叫 id_rsa 或 id_ed25519 的密钥泄露后比看起来更麻烦,也是为什么值得为每台主机写上 IdentityFile 加 IdentitiesOnly yes 这两行:
Host myserver
HostName 192.168.1.100
Port 22
User deploy
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3图形客户端对「保存的密码、私钥内容、passphrase 放在哪」有自己的选择。这个位置应该是可检查的,桌面端一般在应用自己的数据目录下——Windows 和 macOS 上具体是哪些路径,见数据存储路径。
在本地生成密钥:ed25519 优先
算法选择是第一步,而且完全发生在你自己的机器上。RSA 2048 位在当前算力下仍被认为安全,但密钥长度大、生成慢;ed25519 只需 256 位就提供同等甚至更强的安全性,签名和验证性能也更好。OpenSSH 6.5 及以上都支持 ed25519,现代发行版普遍是 7.x 以上,兼容性很少构成约束。
# 在本地生成 ed25519 密钥对,注释里写清归属和用途
ssh-keygen -t ed25519 -C "yuki@laptop-2026" -f ~/.ssh/id_ed25519
# 老旧系统无法使用 ed25519 时,生成 RSA 4096 位
ssh-keygen -t rsa -b 4096 -C "yuki@laptop-legacy" -f ~/.ssh/id_rsa_4096
# 查看公钥指纹——这是你去服务器或控制台核对时要用的值
ssh-keygen -lf ~/.ssh/id_ed25519.pub最后一条命令在刚生成的密钥对上的真实回显:
256 SHA256:EozutPvJRbMlBsZDSQ55p8GH/lusZ8jLZOIzpUJLCq8 yuki@laptop (ED25519)-C 注释是日后你唯一能依赖的标签。当同一把公钥在六个 authorized_keys 里躺了一年之后,能告诉你「这是哪把密钥」的就是这行注释;没有注释,只能对每个文件跑 ssh-keygen -lf 再用肉眼比指纹。所以注释在生成密钥时就写好——归属、机器、年份——不要等以后再补。
passphrase 与 ssh-agent:保护的是文件,不是服务器
给私钥加 passphrase 是成本最低的一层保护,也是被跳过最多的一层。没有 passphrase 的私钥,文件泄露的那一刻就已经可用;有 passphrase 的私钥,攻击者还需要口令这一步——所以 passphrase 绝不能写在密钥旁边,也不能放在同一个同步目录里。
常见的反对理由是麻烦,而 ssh-agent 解决的正是这个。口令只输入一次,解密后的私钥留在 agent 内存里,后续连接直接复用:
# 启动 agent 并加载密钥(会要求输入一次 passphrase)
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# 查看已加载的密钥
ssh-add -l
# 设置有效期:8 小时后需要重新输入 passphrase
ssh-add -t 28800 ~/.ssh/id_ed25519
# 离开工位时把密钥从 agent 里移除
ssh-add -d ~/.ssh/id_ed25519这里有两个客户端侧的细节值得记住。第一,任何能读取 agent 内存或访问它 socket 的东西都能在没有 passphrase 的情况下使用密钥——所以别在别人也能登录的机器上长期加载高权限密钥,优先用 ssh-add -t 设过期时间,离开时显式 ssh-add -d。第二,自动化场景下用 IdentityAgent 把 agent socket 指向专用路径,而不是在脚本里硬编码明文 passphrase,否则一行代码就把有保护的密钥变成了没有保护的密钥。
备份:该归档什么,放在哪里
私钥是 ~/.ssh 里唯一无法重新生成的文件。丢了它,就意味着每一台信任对应公钥的主机都要轮换一次;所以一台没有备份的笔记本丢失,是一次访问管理事故,不只是换台硬件。
值得归档的是三样:私钥、配对的 .pub(避免两者分离)、以及 ~/.ssh/config(里面是让密钥真正可用的主机别名、用户名和 IdentityFile)。known_hosts 可选,它会自己重建。
存放方式:归档文件在离开本机之前先加密,并且至少保留一份不经过任何同步客户端的副本。
# 用口令加密密钥归档(age 会提示输入口令)
tar -czf - ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub ~/.ssh/config | age -p -o ssh-keys-2026.tar.gz.age
# 在新机器上恢复,然后修正权限
age -d -o - ssh-keys-2026.tar.gz.age | tar -xzf - -C ~/
chmod 600 ~/.ssh/id_ed25519 && chmod 644 ~/.ssh/id_ed25519.pub几条底线,作用是避免「备份」本身变成泄露源:私钥不要放进普通的云盘、笔记应用、「服务器」为名的密码字段,也不要提交进任何仓库,公开的私有的都不行。在两台自己的机器之间搬一把密钥,走加密通道是可以接受的;但只靠一个云盘账号口令保护的归档,实际安全性就等于那个最容易被钓鱼的东西。另外,趁不需要它的时候,先在一台无关的机器上试一次恢复流程——从没成功解密过的归档只是一个假设,不是备份。如果你打算用托管通道在多台设备之间同步客户端设置,先看清这个通道如何处理密钥类数据,Termark 这边的模型写在云同步里。
多设备:一设备一密钥,好过一把密钥到处复制
图省事的做法是把 ~/.ssh/id_ed25519 复制到台式机、笔记本和工作虚机上。代价是这三台机器从此是同一份凭据:任何一台泄露就是三台全泄露,撤销时也要三台都动。一设备一密钥只多花一次 ssh-keygen 和每台主机多一行公钥,换来的是「某台设备丢了可以单独切断,不必牵动其它机器」。
如果确实需要复制——临时救急的备用机、事故中途的替换机——就通过加密通道搬,用完把中间存储上的副本删掉,并且把复制到的机器当作一台需要尽快换成自己密钥的设备。密钥周围的东西也是同一个道理:主机列表、分组、连接配置在多设备之间同步是有用且低风险的;私钥不是。
客户端把凭据存在哪里
如果一个图形化 SSH 客户端替你保存密码、私钥或 passphrase,那么真正重要的问题不是它用了什么营销词,而是下面四件事:
- 敏感字段是否明文落盘?
- 本地解密密钥放在哪里?
- 同步到任何地方之前是否已经加密?
- 服务端能不能解密你的数据,能不能替你恢复?
这里的敏感数据至少包括:SSH 密码和私钥内容、私钥口令、跳板机与代理凭据、同步密码、API Key。主机地址、资产名称和分组属于第二层——即使凭据本身安全,它们也会泄露内部网络的结构。
本地加密的关键是密文和密钥不能放在一起。 如果客户端把数据加密之后,又把解密密钥放在同一个普通配置目录里,那么复制整个目录的人依然可以低成本解密。安装版更合理的做法是:本地数据库保存密文,本地数据密钥由操作系统提供的安全存储保管或保护(macOS 是 Keychain,Windows 是系统凭据保护能力)。这挡不住「设备已经被完全控制」的最坏情况,但能消除「复制一个文件就拿到全部凭据」这种失败模式。Termark 这条边界的公开说明见本地加密与数据恢复。
便携版需要单独设计。 Windows 便携版本来就是连数据目录一起搬走的。如果它完全依赖原电脑的系统安全存储,换机后无法解锁;如果为了便携干脆取消加密,风险更大。清楚的边界是让用户设置一个本地加密口令:客户端用口令和随机 salt 派生本地密钥,数据目录里只保存密文、salt 和必要的 KDF 参数,不保存明文口令也不保存明文数据密钥,迁移之后由用户重新输入口令解锁。它多了一步操作,但「谁掌握口令,谁才能解密」是用户能真正推理清楚的规则。
同步数据应该在上传之前就加密。 多设备同步意味着数据要离开本机,所以要继续追问:上传的是密文还是明文?同步密码会不会发给存储方?服务端有没有解密能力?WebDAV、S3、iCloud 这些通道是否使用同样的处理方式?当初次加密发生在客户端时,服务端只承担存储和传输,不需要接触你服务器凭据的明文。直接的代价是:同步密码一旦丢失,通常连厂商也无法恢复。
「我们可以恢复你的全部数据」与端到端加密是冲突的。 普通网站账号能重置密码很正常,但一个产品如果同时声称「服务端无法解密同步数据」和「忘记同步密码后客服可以恢复全部服务器凭据」,它描述的其实是两条解密路径,第二条值得直接问清楚。对服务器凭据来说,「忘记同步密码就无法恢复」确实不方便,但这才是真实存在的安全边界;产品应该在启用同步时说明这一点,并提醒用户自行保存恢复信息。
同步冲突同时是可靠性和安全问题。 台式机和笔记本可能同时修改同一批资产。静默的「后写覆盖先写」会丢掉新增的资产或命令片段、让凭据回退到旧值、用过期数据替换当前的端口转发或跳板配置。检测到冲突时,至少应该让用户在远端版本和本地版本之间自己选。
带 AI 的功能还要多问一层。 带 AI 的 SSH 客户端可能读取终端输出、配置片段或日志。除了本地凭据,还要确认:哪些终端内容会发给模型?最近输出的范围用户能否控制?API Key 保存在哪里?是否支持指向自己的模型接口?会改变远端状态的命令是否会在未经确认的情况下执行?下面是一份简短的客户端检查清单:
- [ ] 密码、私钥、代理凭据不明文落盘
- [ ] 本地数据密钥不与密文放在同一个普通文件里
- [ ] 便携版有明确的迁移与解锁机制
- [ ] 同步数据上传前已在客户端加密
- [ ] 同步密码不发送给存储服务
- [ ] 产品明确说明服务端能否解密
- [ ] 忘记同步密码后的结果提前告知
- [ ] 同步冲突不会被静默覆盖
- [ ] AI 上下文范围可以理解和控制
- [ ] 可能改变远端状态的命令执行前可见
这些边界不能代替设备安全、服务器上的最小权限,也不能代替本文其余部分的轮换与备份习惯。任何本地客户端都无法在一台已经被完全控制的设备上承诺绝对安全。
丢了或者泄露了:第一小时要做什么
两种情况的表面很像,其实不一样。密钥丢失——磁盘坏了、笔记本被抹了、就是找不到了——是可用性问题:凭据的权限还在你手里,只是用不了。密钥泄露——设备被盗、屏幕被人看到、文件被提交、同步目录被人拿到——是失陷:按它已经可以被别人使用来处理,这两者的紧迫程度完全不同。
密钥丢失时,如果做过加密备份,优先从备份恢复;等待期间顺手确认客户端的 agent 里是否还留着这把密钥(ssh-add -l)、第二台设备上是否还有,因为存在一份可用副本会直接改变紧迫程度。如果完全没有副本,那就只能轮换:生成新密钥对,部署到原来信任旧公钥的每一台主机。
密钥泄露时,这一小时大致是这样:
# 1. 看 agent 里还留着什么,并全部清掉
ssh-add -l
ssh-add -D
# 2. 在本地生成替换密钥
ssh-keygen -t ed25519 -C "yuki@laptop-rotation-$(date +%Y%m%d)" -f ~/.ssh/id_ed25519_new
# 3. 把新公钥部署到每一台信任过旧密钥的主机
ssh-copy-id -i ~/.ssh/id_ed25519_new.pub deploy@target-server
# 4. 在撤销任何东西之前先验证新密钥,且不要关闭当前会话
ssh -i ~/.ssh/id_ed25519_new deploy@target-server 'echo "new key works"'
# 5. 确认无误后再从服务器删除旧公钥,并留下记录
ssh deploy@target-server 'grep -n "旧密钥注释" ~/.ssh/authorized_keys'
# 6. 本地换上新密钥、退役旧文件
mv ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.bak.$(date +%Y%m%d)
mv ~/.ssh/id_ed25519_new ~/.ssh/id_ed25519
mv ~/.ssh/id_ed25519_new.pub ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/id_ed25519第 3、4 步必须发生在第 5 步之前,而且整个过程要保持当前会话打开。新密钥还没验证就撤销旧密钥,会把你锁在正在轮换的那台机器外面。同时处理那些不是 shell 服务器的位置:上传到 Git 托管平台的公钥、部署密钥(deploy key)、CI 里的密钥、跳板机配置,这些往往可以在服务器侧轮换还在进行时就从网页控制台替换或禁用,早一点做就能缩短窗口期。某些主机支持第二因素,在你已经不能完全确定这把密钥的情况下,要求第二因素是一层合理的补充——Termark 里键盘交互提示的处理方式见自动 OTP 交互认证。
有一件事客户端侧单独做不到:如果同一把密钥也被别人管理的服务器,或者被有自己 authorized_keys 记录流程的团队信任过,那么必须通知他们。密钥变更是变更管理的一部分,一把泄露的密钥值得给主机负责人写一条通知。
把每一份副本都换掉,包括容易被忘掉的那些
新密钥能登录,不代表轮换结束。旧凭据必须从它存在过的每一个地方消失,而难点几乎全在「想起来它都在哪些地方」。一把两年前生成的密钥,比较现实的清单是:
~/.ssh/id_ed25519,以及同目录下改名或更早的副本- 当前已加载的 agent:
ssh-add -l看一遍,然后ssh-add -D - 加密备份归档——必须替换,不能和新密钥的归档并排放着
- 归档经过的任何同步目录或云盘
- 存放过对应公钥或部署密钥的 Git 托管平台与 CI 系统
- 每台机器
~/.ssh/config里的IdentityFile(如果文件名变了) - 跳板机/堡垒机自己的配置
- 上一台没有彻底抹掉的机器
新密钥到位之后,用指纹而不是文件名来确认身份,并在注释里写上日期,方便下一次轮换找到它:
$ ssh-keygen -lf ~/.ssh/id_ed25519.pub
256 SHA256:EozutPvJRbMlBsZDSQ55p8GH/lusZ8jLZOIzpUJLCq8 yuki@laptop (ED25519)每个季度花几分钟扫一遍自己的机器,就能拦住慢慢积累的偏差:对 ~/.ssh 里每个 .pub 跑一次 ssh-keygen -lf;用 ssh -G <host> 看看有没有意料之外的密钥会被递出去;找找有没有你以为早删掉的 .bak 备份;确认最新的备份归档还能解密。一把只存在于一台机器、一个有 passphrase、一份归档副本,并且注释里写清设备的密钥,才是出事时不会变成意外的那一把。
本地这一摊理顺之后,下一个问题通常是这些密钥都用在了哪些主机上。Termark 把主机、SSH 密钥、命令片段和端口转发收在同一个工作台里,散出去的副本更容易对得上号:www.termark.app/zh。