每日壁纸

多设备 SSH 鉴权:一次配置,处处通行

Published on
/17 mins read/---

先说结论:终极形态只有三个要素 —— 一把密钥 + 一个 Agent + 一份共享配置。

多设备 SSH 的痛,几乎全部来自三件重复劳动:

  1. 每台新设备都要重新 ssh-keygen 生成一份密钥
  2. 每台服务器都要手动往 authorized_keys 里拷公钥
  3. 每次连接都要重新输一遍密码短语

三件事都得做一遍,设备一多就立刻变成一种折磨。而这三件事其实都可以只做一次。

核心思路是——

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.target
systemctl --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 failures
  • ControlMaster + 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-db

ProxyJump 让这台机器穿过跳板机直连,不需要两段手工隧道,也不需要开启 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.10

3. 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 精确授权具体命令。


六、迁移到新设备的最小清单

把这套方案用在第二台、第三台设备上时,按这个顺序做就行:

  1. 拿到密钥 —— 复制 ~/.ssh/id_ed25519(带密码短语)和 id_ed25519.pub
  2. 装 Agent —— GNOME Keyring、systemd 服务,或直接 ssh-add
  3. 同步配置 —— ~/.ssh/config 和 ~/.ssh/config.d/ 一起带走
  4. 修权限 —— chmod 700 ~/.ssh、chmod 600 ~/.ssh/id_ed25519 ~/.ssh/config
  5. 验证 —— 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 重写全部复用同一套,换设备时同步一个目录就够了。

想记住的不是这几条命令,而是一个取舍原则:把一次性成本(配置)花在前面的地方,把每天重复的成本(输入密码、换设备重配)压到零。