先说结论:终极形态只有三个要素 —— 一把密钥 + 一个 Agent + 一份共享配置。
多设备 SSH 的痛,几乎全部来自三件重复劳动:
- 每台新设备都要重新
ssh-keygen生成一份密钥 - 每台服务器都要手动往
authorized_keys里拷公钥 - 每次连接都要重新输一遍密码短语
三件事都得做一遍,设备一多就立刻变成一种折磨。而这三件事其实都可以只做一次。
核心思路是——
IMPORTANT
别在每台设备上生成密钥。 让「身份」和「设备」解耦:一把密钥 + 一个 Agent 在所有设备上共享, 私钥本身永远不落在任何一台笔记本上。 换新设备时,只要装好 Agent 和配置,登录能力就自动继承了。
这篇文章就围绕 ~/.ssh/ 这个目录展开:里面有哪些文件、每个文件负责什么、怎么组织才能让所有设备共用同一套。
一、~/.ssh/ 里都有什么
先搞清楚这个目录的结构,后面所有配置都落在这些文件里:
~/.ssh/
├── id_ed25519 # 私钥 —— 绝对不外传
├── id_ed25519.pub # 公钥 —— 交给服务器
├── config # 客户端配置:主机别名、端口、跳板机
├── config.d/ # 拆分出来的配置片段
├── known_hosts # 已连接过的主机指纹
├── authorized_keys # 服务端:我允许谁登录我
└── cm/ # ControlMaster 的 socket 目录
有个容易混淆的点值得先分清:
NOTE
authorized_keys 在服务端,id_ed25519.pub 是客户端的公钥。 「把公钥拷到服务器的 authorized_keys」——这是整个 SSH 公钥认证的唯一动作。 剩下的所有便利性配置,都是在优化这一次动作之后的东西。
这几个文件承担了全部职责:
| 文件 | 在哪一侧 | 作用 | 能否泄露 |
|---|---|---|---|
id_ed25519 | 客户端 | 私钥,签名用 | 绝对不行 |
id_ed25519.pub | 客户端 | 公钥,放进服务端的 authorized_keys | 无所谓 |
authorized_keys | 服务端 | 允许哪些公钥登录这台机器 | 无所谓 |
config | 客户端 | 连接参数、主机别名、跳板机 | 无所谓 |
known_hosts | 客户端 | 已信任过的主机指纹,防止中间人攻击 | 无所谓 |
IMPORTANT
目录权限是第一道防线。 SSH 客户端会拒绝使用权限过于开放的私钥:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519.pub服务端的 authorized_keys 同理,通常需要 600(644 在部分配置下也能工作, 但 600 更保险),且属主必须是登录用户本身,不能是 root。
二、公钥私钥:生成与分发
生成一对 ed25519
ssh-keygen -t ed25519 -a 100 -C "you@example.com"-t ed25519—— 目前推荐的默认算法,比 RSA 更短更快,安全强度足够-a 100—— KDF 迭代次数,密码短语的加密强度-C—— 备注,方便日后在authorized_keys里辨认是哪台设备的
生成后会有两个文件:
ls -l ~/.ssh/id_ed25519*-rw------- 1 you you 464 Aug 26 10:00 id_ed25519
-rw-r--r-- 1 you you 103 Aug 26 10:00 id_ed25519.pub
私钥 600、公钥 644,这是标准状态。
TIP
私钥的密码短语一定要设。 它是「密钥文件被拷走」时的最后一道锁。 不设的话,私钥文件本身就是通行凭证 —— 任何拿到它的人直接就能登录。
把公钥送上去
最省事的方式是 ssh-copy-id,它会自动追加到服务端的 authorized_keys:
ssh-copy-id -i ~/.ssh/id_ed25519.pub you@example.com它做的事很简单:读本地公钥 → 通过密码登录一次 → 把公钥追加到远端 ~/.ssh/authorized_keys → 修正权限。
没有 ssh-copy-id 时手动做也一样:
cat ~/.ssh/id_ed25519.pub | ssh you@example.com 'cat >> ~/.ssh/authorized_keys'WARNING
手动追加之后记得检查权限,否则 SSH 会因为权限过松而拒绝使用这个密钥:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys一把密钥,多台设备共用
现在回到开头那三件重复劳动。第二件(拷公钥)之所以要反复做,是因为第一件(生成密钥)在反复做。
只要所有设备共用同一对密钥,那公钥只需要送到每台服务器一次,之后换设备、装新机、临时借用别人的电脑,都不用再碰 authorized_keys。
具体做法是:把这几个文件放进密码管理器或者私有 Git 仓库,换设备时取出来用。
TIP
用私有 Git 仓库同步时,注意只同步 id_ed25519、config、config.d/, 不要同步 known_hosts(每台机器的主机指纹不同,而且它是安全相关的本机状态)。 仓库务必设为 private,且确认历史里没有意外提交过私钥。
三、ssh-agent:让密码短语只输一次
私钥有了密码短语,每次连接都输入是灾难。
ssh-agent 的作用是把私钥解锁一次,之后代劳所有签名 —— 它持有已解锁的私钥,你连接时它替你签名,密码短语不会再出现。
Agent 本身只是一个本地进程,和公钥分发、服务器配置都无关。关键在于怎么让它跑起来、怎么让它知道你的密钥。
最省事:直接用 ssh-add
ssh-agent会输出类似:
Agent pid 59566
但别手动 eval "$(ssh-agent -s)" —— 这会把 Agent 的环境变量写进当前 shell, 一旦这个 shell 退出,Agent 还活着但你已经连不上了。
更干净的做法是让 ssh 自己管,在 ~/.ssh/config 里加一行:
AddKeysToAgent yes
之后每次用某把密钥连接,它会自动询问是否加入 Agent;回答一次「是」,之后就再也不用输密码短语了。这个选项需要 OpenSSH 7.2+。
更彻底:把 Agent 交给系统托管
手动 ssh-agent 的老问题在于重启后钥匙全没了,得重新 ssh-add 一次。
Linux 上有三种让它开机自动就绪的方式。
方式 A:GNOME Keyring(Ubuntu 桌面自带)
Ubuntu 桌面自带 gnome-keyring,其中包含一个 SSH Agent 组件。先确认 socket 路径:
ls -l "$XDG_RUNTIME_DIR"/keyring/ssh存在的话,在 ~/.ssh/config 里写死:
IdentityAgent ${XDG_RUNTIME_DIR}/keyring/ssh
然后把密钥交给它托管:
ssh-add ~/.ssh/id_ed25519输入一次密码短语后,凭据会存进登录钥匙环(login keyring),随开机自动解锁。
NOTE
不同 Ubuntu 版本随附的组件略有差异,先用上面的 ls 确认路径存在再改配置。 路径不存在就换下面两种方式。
顺带一提,GNOME Keyring 的这个 Agent 是登录时自动解锁的 —— 这正是它比裸 ssh-agent 省事的地方。
方式 B:systemd user service(跨桌面环境、可控)
想自己掌控 Agent 生命周期,用 systemd 用户服务最稳:
# ~/.config/systemd/user/ssh-agent.service
[Unit]
Description=SSH Agent
[Service]
Type=simple
ExecStart=/usr/bin/ssh-agent -D -a %t/ssh-agent.sock
Environment=SSH_AUTH_SOCK=%t/ssh-agent.sock
[Install]
WantedBy=default.targetsystemctl --user daemon-reload
systemctl --user enable --now ssh-agent.service-D 让 agent 前台运行(systemd 需要它不 fork 退出),%t 会被 systemd 展开成用户运行时目录(通常是 /run/user/1000)。服务起来之后,同样在 ~/.ssh/config 里写:
IdentityAgent ${XDG_RUNTIME_DIR}/ssh-agent.sock
TIP
只要在 config 里显式指定了 IdentityAgent,就不用再管 SSH_AUTH_SOCK 环境变量了。 顺带一提,ssh_config 支持 ${VAR} 语法引用环境变量,所以上面这种写法可以直接用。
确认 Agent 状态
ssh-add -l会列出 Agent 里持有的凭据及其指纹。如果这里是空的,说明 Agent 没跑起来或者密钥没加进去 —— 这是排查 SSH 登录问题的第一站。
TIP
排查这类问题的顺序是:ssh-add -l 看 Agent 状态 → ssh -v user@host 看认证走的是哪条路径 → ssh -G host 打印最终生效的配置。ssh -G 这一招特别好用, 它把配置合并后的真实结果直接打出来,能一眼看出哪个参数被覆盖了。
四、一份 ssh_config 管所有设备
这是「一次配置」的落点。目标状态是:每台设备只放同一份配置,输 ssh <别名> 就能连。
用 Include 拆分配置
mkdir -p ~/.ssh/config.d ~/.ssh/cm
chmod 700 ~/.ssh/config.d ~/.ssh/cm~/.ssh/config:
Include ~/.ssh/config.d/*
Host *
AddKeysToAgent yes
IdentityAgent ${XDG_RUNTIME_DIR}/keyring/ssh
IdentitiesOnly yes
HashKnownHosts yes
ServerAliveInterval 60
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/cm/%r@%h:%p
ControlPersist 10m
WARNING
Include 必须放在文件最前面。 ssh 的规则是同一参数取最先取到的值, 所以被包含的文件排在前面、通用默认值排在后面,才不会互相覆盖。 如果把 Include 放到末尾,Host * 里的默认值会先把参数定死, config.d/ 里的机器专属设置就全部失效了。
这些全局参数的作用:
IdentitiesOnly yes—— 只用配置里指定的密钥,避免 Agent 里塞了太多密钥时服务端报Too many authentication failuresControlMaster+ControlPersist—— 连接复用:第一次 SSH 建立连接,后续 SSH 到同一台机器会复用这条 TCP 连接,省掉握手和重复认证ServerAliveInterval+ServerAliveCountMax—— 定期发心跳,防止 NAT 或防火墙掐断空闲连接HashKnownHosts yes—— 已知主机名做哈希存储,不在磁盘上留明文主机名
每台机器一个别名
~/.ssh/config.d/hosts:
Host edge
HostName 192.168.1.10
User admin
Port 22
IdentityFile ~/.ssh/id_ed25519
Host bastion
HostName bastion.example.com
User ops
Host prod-db
HostName 10.0.2.30
User dba
ProxyJump bastion
之后:
ssh prod-dbProxyJump 让这台机器穿过跳板机直连,不需要两段手工隧道,也不需要开启 Agent 转发(这一点很关键,见后文安全边界)。
TIP
这份配置是通用标准格式,不只给命令行用: VS Code 的 Remote-SSH、JetBrains 的 Remote Development、Docker 的 ~/.docker/config.json、 rsync/scp、甚至 ~/.config/git/config 里的 url 重写,都能复用同一套别名。 换设备时只要同步这一个目录,能力就完整迁移了。
五、安全边界:这几条别省
密钥和 Agent 都在本机,配置也在本机,但这些文件一旦泄露或权限放松,攻击者就直接拿到你的登录能力。
WARNING
1. Agent 转发默认关掉。 > ForwardAgent yes 或 ssh -A 会把 Agent socket 直接暴露给远端 —— 那台机器一旦被攻破,攻击者就能用你的身份去连其他机器。 需要跳板机时用上面的 ProxyJump,它比转发安全得多: 代理只转发加密流量,不转发你的签名能力。
2. StrictHostKeyChecking 用 accept-new,不要用 no。 > no 等于关掉中间人攻击防护,第一次连接时无条件信任。 accept-new 只自动接受从未见过的主机,已知主机变更时仍然会告警。 改完记得清掉旧记录让它重新采集:
ssh-keygen -R 192.168.1.103. authorized_keys 加上限制。 对敏感主机可以写死来源网段和禁用项:
from="10.0.0.0/24",no-port-forwarding,no-agent-forwarding ssh-ed25519 AAAA... you@work
4. 私钥权限别放松。 > ~/.ssh 必须是 700,私钥必须是 600。SSH 客户端会主动拒绝使用权限过开的私钥 —— 这不是麻烦,是保护。.ssh/cm/ 目录同理,ControlMaster 的 socket 放在里面,别用默认的 /tmp 共享路径。
5. sudo 免密要慎用。 > NOPASSWD:ALL 配合「复制粘贴即执行」的习惯, 等于把 root 权限交给任何进入你 shell 的人。用 visudo 精确授权具体命令。
六、迁移到新设备的最小清单
把这套方案用在第二台、第三台设备上时,按这个顺序做就行:
- 拿到密钥 —— 复制
~/.ssh/id_ed25519(带密码短语)和id_ed25519.pub - 装 Agent —— GNOME Keyring、systemd 服务,或直接
ssh-add - 同步配置 ——
~/.ssh/config和~/.ssh/config.d/一起带走 - 修权限 ——
chmod 700 ~/.ssh、chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config - 验证 ——
ssh-add -l看 Agent 里有没有凭据;ssh -v user@host看认证走的是哪条路径
ssh-add -l如果 ssh -v 显示还在用密码认证而不是公钥,多半是 IdentityAgent 路径写错、或 IdentitiesOnly 与 IdentityFile 没对上。
TIP
如果你连的是 GitHub,主机指纹和公钥上传的步骤我另外整理过一份, 可以看使用 SSH 连接 GitHub。
小结
把整套东西压成一句话:让「你是谁」和「你在哪台设备上」彻底分离。
- 密钥唯一 —— 一把 ed25519,私钥带密码短语,所有设备共用同一对
- Agent 唯一 —— GNOME Keyring、systemd 服务或
ssh-add,负责免密 - 配置唯一 ——
~/.ssh/config+config.d/,所有设备同一份
做到这三条之后,「装好环境」这件事就退化成了「装好 Agent + 同步配置」,再也不用重新生成密钥、重新拷贝公钥、重新输密码。
而这套方案的适用性也不止于 SSH —— ssh_config 的别名体系是通用标准格式,VS Code Remote-SSH、JetBrains Remote Development、rsync、scp、Git 的 url 重写全部复用同一套,换设备时同步一个目录就够了。
想记住的不是这几条命令,而是一个取舍原则:把一次性成本(配置)花在前面的地方,把每天重复的成本(输入密码、换设备重配)压到零。
