Linux-17 SSH 密钥管理与多账号实战
上一篇讲了 SSH 的协议原理和端口转发,这一篇是纯操作向:怎么管理多个密钥、怎么让同一个域名支持多个账号、以及 known_hosts 的所有细节。
最典型的场景:一台开发机上同时有个人 GitHub、公司 GitHub、公司 GitLab、若干台服务器——它们都是 git@github.com 这样的相同用户名和域名,SSH 怎么知道该用哪个密钥。这就是本篇要解决的核心问题。
1. 密钥对:一句话回顾
原理在上一篇讲过,这里只保留结论:
私钥 ~/.ssh/id_ed25519 <- 绝对不能泄露,只存本机,权限必须 600
公钥 ~/.ssh/id_ed25519.pub <- 可以任意分发,上传到 GitHub / 服务器
认证时客户端用私钥对服务器发来的随机数据做签名,服务器用公钥验签——私钥从不离开本机。完整流程见 Linux-16 §3。
2. 生成密钥对
2.1 基本命令与参数
ssh-keygen -t ed25519 -C "your_email@example.com"
| 参数 | 说明 |
|---|---|
-t ed25519 |
算法类型 |
-C "注释" |
附加注释,写入公钥末尾,用于识别密钥归属 |
-f ~/.ssh/xxx |
保存路径(不指定则交互询问) |
-b 4096 |
密钥位数(仅 RSA 有效,Ed25519 固定 256 bit) |
-N "口令" |
直接指定 passphrase(空字符串 = 无口令,适合脚本) |
-C 注释不要省略。当 GitHub 上有五六个密钥、服务器 authorized_keys 里有十几行时,注释是唯一能分辨「这个密钥是谁的、干什么用的」的信息。没有注释的公钥在需要吊销时根本不知道该删哪一行。
2.2 算法选择
| 算法 | 密钥长度 | 安全性 | 速度 | 推荐程度 |
|---|---|---|---|---|
| Ed25519 | 256 bit | ★★★★★ | 最快 | 首选 |
| ECDSA | 256 bit | ★★★★ | 快 | 可用(部分老系统不支持) |
| RSA 4096 | 4096 bit | ★★★★ | 慢 | 兼容老系统时用 |
| RSA 2048 | 2048 bit | ★★★ | 较慢 | 不推荐新建 |
| DSA | 1024 bit | ★ | — | 已淘汰,OpenSSH 7.0+ 默认禁用 |
**结论:OpenSSH 6.5+(2014 年后)一律用 Ed25519。**它的公钥只有 68 个字符(RSA 4096 是 700 多),签名和验签都快,且没有 RSA 那种「位数不够就不安全」的问题。
注意:OpenSSH 8.8+ 默认禁用了
ssh-rsa(用 SHA-1 的 RSA 签名算法)。连接老设备时可能报no matching host key type found,临时解法是ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa。注意这只是签名算法被禁用,RSA 密钥本身仍然可用(用rsa-sha2-256/rsa-sha2-512)。
2.3 交互过程
# 第一步:保存路径
Enter file in which to save the key (/home/user/.ssh/id_ed25519):
- 直接回车:用默认路径
- 输入自定义路径:如
~/.ssh/github_work(多账号时必须自定义)
# 第二步:私钥口令(passphrase)
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
passphrase 的作用是加密私钥文件本身——即使私钥文件被复制走,没有口令也无法使用。这是私钥泄露时的最后一道防线。
该不该设 passphrase:
| 场景 | 建议 |
|---|---|
| 个人开发机上的 GitHub 密钥 | 设,配合 ssh-agent 只需输一次 |
| 笔记本(可能丢失/被盗) | 必须设 |
| CI/CD、自动化脚本用的密钥 | 不设(无法交互输入),但要配合 command= 限制(第 16 篇 §3.4) |
| 服务器上用于内部通信的密钥 | 不设,靠文件权限和主机安全保护 |
2.4 生成的文件与权限
~/.ssh/
+-- id_ed25519 <- 私钥,权限必须 600
+-- id_ed25519.pub <- 公钥,权限 644
chmod 600 ~/.ssh/id_ed25519
# 权限不对时 SSH 直接拒绝使用:
# Permissions 0644 for '/home/user/.ssh/id_ed25519' are too open.
# It is required that your private key files are NOT accessible by others.
# 查看公钥内容(上传到 GitHub 时复制这个)
cat ~/.ssh/id_ed25519.pub
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... me@example.com
# ^^^^^^^^^^^^^^ -C 指定的注释
3. 本地多密钥管理
3.1 目录结构规划
~/.ssh/
+-- config <- 核心:控制「哪个主机用哪个密钥」
+-- known_hosts <- 已信任的服务器指纹(自动维护)
|
+-- id_ed25519 <- 默认密钥(个人 / 通用)
+-- id_ed25519.pub
|
+-- github_work <- 公司 GitHub
+-- github_work.pub
|
+-- gitlab_personal <- 个人 GitLab
+-- gitlab_personal.pub
|
+-- server_prod <- 生产服务器
server_prod.pub
命名规范建议 <用途>_<环境>:github_work、server_prod、ci_deploy。不要用 id_ed25519_1、key2 这种——半年后你完全不知道哪个是哪个。
3.2 生成多个密钥
# 个人 GitHub
ssh-keygen -t ed25519 -C "personal@gmail.com" -f ~/.ssh/github_personal
# 公司 GitHub
ssh-keygen -t ed25519 -C "work@company.com" -f ~/.ssh/github_work
# 生产服务器(无口令,方便自动化脚本)
ssh-keygen -t ed25519 -C "deploy-key" -f ~/.ssh/server_prod -N ""
为什么要用多个密钥而不是一个通用密钥:
① 泄露隔离 —— 一个密钥泄露只影响一个服务,不用把所有地方的公钥都换掉
② 可独立吊销 —— 离职时只需删掉公司那一个,个人的不受影响
③ 审计清晰 —— 服务器日志里能看出是哪个密钥登录的
④ 合规要求 —— 很多公司禁止个人密钥用于公司系统
3.3 config:多密钥管理的核心
# ~/.ssh/config
# ============ GitHub 个人账号 ============
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes # 只用指定的密钥,不尝试其他 <- 关键
# ============ GitHub 公司账号 ============
# 用不同的 Host 别名区分【同一个域名】的不同账号
Host github-work
HostName github.com # <- 实际连的还是 github.com
User git
IdentityFile ~/.ssh/github_work
IdentitiesOnly yes
# ============ GitLab ============
Host gitlab.com
HostName gitlab.com
User git
IdentityFile ~/.ssh/gitlab_personal
IdentitiesOnly yes
# ============ 生产服务器 ============
Host prod
HostName 192.168.1.100
User ubuntu
Port 22
IdentityFile ~/.ssh/server_prod
# ============ 全局默认(必须放最后)============
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes # 首次用到时自动加入 ssh-agent
UseKeychain yes # macOS:口令保存到 Keychain
Host 是别名,HostName 才是真实地址——这是实现「同域名多账号」的关键机制。github-work 这个别名并不存在于 DNS 中,SSH 只是用它来查找配置,实际连接的是 HostName 指定的 github.com。
记住 Host * 必须放在文件最后(第 16 篇 §4.2 讲过:SSH 对每个参数取第一次遇到的值)。
3.4 多账号 clone / push
# 个人 GitHub(Host 就是 github.com,正常写)
git clone git@github.com:personal/repo.git
# 公司 GitHub(用 config 里的 Host 别名替换域名)
git clone git@github-work:company/repo.git
# ^^^^^^^^^^^^ 这里是 Host 别名,不是真实域名
# 已有仓库改 remote
git remote set-url origin git@github-work:company/repo.git
git remote -v # 确认
这是多账号最容易出错的一步:git clone 的时候忘了把 github.com 换成 github-work,就会用个人密钥去访问公司仓库,报 Permission denied 或者 Repository not found(GitHub 对无权限的私有仓库会报后者,容易误导)。
# 排查:确认这个仓库实际用的是哪个身份
cd repo
git remote -v
# origin git@github.com:company/repo.git <- ❌ 应该是 github-work
ssh -T git@github-work
# Hi work-username! ... <- 确认别名对应的账号
3.5 临时指定密钥的三种方式
不想改 config、或临时用一次:
# 方式一:GIT_SSH_COMMAND 环境变量(推荐,只影响当次)
GIT_SSH_COMMAND="ssh -i ~/.ssh/github_work -o IdentitiesOnly=yes" \
git clone git@github.com:company/repo.git
# 方式二:-c 内联参数
git clone -c "core.sshCommand=ssh -i ~/.ssh/github_work" git@github.com:company/repo.git
# 方式三:对单个仓库持久生效
cd repo
git config core.sshCommand "ssh -i ~/.ssh/github_work -o IdentitiesOnly=yes"
# 写入 .git/config,之后该仓库的所有 git 操作都用这个密钥
| 方式 | 作用范围 | 适用场景 |
|---|---|---|
~/.ssh/config |
按 Host 匹配,永久 | 多账号日常使用(首选) |
GIT_SSH_COMMAND |
单次命令 | 临时操作、CI 脚本 |
git config core.sshCommand |
单个仓库,永久 | 仓库维度隔离 |
CI 环境推荐 GIT_SSH_COMMAND——它不需要写 config 文件,一个环境变量就能搞定,而且能顺便加 -o StrictHostKeyChecking=accept-new 之类的选项。
4. 密钥选择的优先级
理解这个顺序能解决绝大多数「用了错误密钥」的问题。
4.1 选择顺序
命令行 -i 参数 (最高)
v
config 里的 IdentityFile
v
ssh-agent 中的密钥(按加入顺序逐个尝试)
v
默认密钥文件(按固定顺序)
v
交互询问密码 (最低)
4.2 默认密钥文件的查找顺序
没有任何配置时,SSH 按这个顺序找:
~/.ssh/id_ed25519 <- 第一个(现代算法优先)
~/.ssh/id_ecdsa
~/.ssh/id_rsa
~/.ssh/id_dsa <- 最后(已淘汰)
4.3 用 -v 观察实际选择过程
ssh -v git@github.com 2>&1 | grep -iE "will attempt|offering|accepts"
# debug1: Will attempt key: /home/user/.ssh/github_personal ED25519 SHA256:xxx explicit
# debug1: Offering public key: /home/user/.ssh/github_personal ED25519
# debug1: Server accepts key: /home/user/.ssh/github_personal ED25519
末尾的标记告诉你密钥的来源:
| 标记 | 来源 |
|---|---|
explicit |
config 的 IdentityFile 或命令行 -i |
agent |
ssh-agent |
default |
默认路径(~/.ssh/id_*) |
这是排查「明明配了 IdentityFile 却用了别的密钥」的直接手段——如果看到 agent 在 explicit 之前被尝试,就是缺 IdentitiesOnly yes。
4.4 IdentitiesOnly:多密钥场景的必需项
不加 IdentitiesOnly yes:
SSH 会把 ssh-agent 里的【所有】密钥都先尝试一遍,
然后才用 config 指定的 IdentityFile
加了 IdentitiesOnly yes:
只尝试 IdentityFile 指定的密钥
不加会导致两个实际问题:
问题一:Too many authentication failures
Received disconnect from 20.205.243.166 port 22:2:
Too many authentication failures
服务端有 MaxAuthTries 限制(默认 6),agent 里有 8 个密钥的话,还没轮到正确的那个就已经超限被断开了。
问题二:用错了身份
多账号时更严重——agent 里个人密钥排在前面,访问公司仓库时先用个人密钥认证成功了,于是你以个人身份操作公司仓库(如果两个账号都有权限),提交记录的作者就错了。
# ✅ 解法:所有指定了 IdentityFile 的 Host 都加上
Host github.com
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes
# 临时救急
ssh -o IdentitiesOnly=yes -i ~/.ssh/specific_key user@server
规律:只要 config 里写了 IdentityFile,就应该同时写 IdentitiesOnly yes。
5. ssh-agent
私钥设了 passphrase 后每次使用都要输入。ssh-agent 把解密后的私钥缓存在内存里,只需输入一次。
5.1 基本使用
# 启动 agent(通常系统登录时已自动启动)
eval "$(ssh-agent -s)"
# Agent pid 12345
# 添加私钥(会提示输入口令)
ssh-add ~/.ssh/github_personal
# Enter passphrase for /home/user/.ssh/github_personal: ****
# Identity added: /home/user/.ssh/github_personal (personal@gmail.com)
# 查看已缓存的密钥
ssh-add -l
# 256 SHA256:xxx personal@gmail.com (ED25519)
# 256 SHA256:yyy work@company.com (ED25519)
# 删除特定密钥 / 清空全部
ssh-add -d ~/.ssh/github_personal
ssh-add -D
# 设置超时(1 小时后自动移除,降低长期驻留风险)
ssh-add -t 3600 ~/.ssh/github_personal
# 每次使用都需要确认(配合 Agent Forwarding 时强烈建议)
ssh-add -c ~/.ssh/github_personal
ssh-add -c 值得强调:加了之后每次密钥被使用都会弹窗/提示确认。这能防住第 16 篇讲的 Agent Forwarding 滥用——攻击者借用你的 agent 时,你会立刻看到一个你没发起的确认请求。
ssh-agent 的原理:它是一个后台进程,通过 Unix socket 提供服务(第 8 篇讲的 IPC):
echo $SSH_AUTH_SOCK
# /tmp/ssh-XXXXXX/agent.12345
# ^ SSH 客户端通过这个 socket 请求 agent 用私钥签名
# 私钥本身【永远不通过 socket 传输】,只传签名结果
这个设计保证了即使有人能访问 socket,也只能「让 agent 帮他签名」,拿不到私钥文件本身——但这已经足够冒充你登录了,所以 socket 的权限保护很重要(默认 600,只有你自己能访问)。
5.2 macOS 自动加载
# 添加时保存口令到 Keychain
ssh-add --apple-use-keychain ~/.ssh/github_personal
# ~/.ssh/config 里配置自动加载
Host *
UseKeychain yes # 从 Keychain 读口令
AddKeysToAgent yes # 首次用到时自动加入 agent
配好之后重启电脑也不需要再输口令,Keychain 会自动提供。
5.3 Linux 自动加载
# 方式一:写入 ~/.bashrc / ~/.zshrc(简单但每个终端会话可能起一个 agent)
if [ -z "${SSH_AUTH_SOCK:-}" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/github_personal 2>/dev/null
fi
# 方式二:systemd user service(推荐,全系统只有一个 agent)
# ~/.config/systemd/user/ssh-agent.service
[Unit]
Description=SSH key agent
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK
[Install]
WantedBy=default.target
systemctl --user enable --now ssh-agent
# 让 shell 知道 socket 位置
echo 'export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"' >> ~/.bashrc
systemd --user 方案的优势(第 14 篇讲的 systemd 好处同样适用):只有一个 agent 实例、socket 路径固定、开机自动启动、systemctl --user status 能查状态。而 .bashrc 方案每开一个终端可能起一个新 agent,导致「这个终端加了密钥,另一个终端却没有」。
6. 公钥部署
6.1 部署到 GitHub / GitLab
# 复制公钥内容
cat ~/.ssh/github_personal.pub
cat ~/.ssh/github_personal.pub | pbcopy # macOS 直接进剪贴板
cat ~/.ssh/github_personal.pub | xclip -sel c # Linux
GitHub:Settings → SSH and GPG keys → New SSH key → 粘贴。
# 验证
ssh -T git@github.com
# Hi personal-username! You've successfully authenticated, but GitHub does not
# provide shell access.
# ^^^^^^^^^^^^^^^^^ 确认是【期望的那个账号】
ssh -T git@github-work
# Hi work-username! You've successfully authenticated...
ssh -T 显示的用户名是最好的验证——它直接告诉你这个密钥对应哪个 GitHub 账号。多账号配置完必须两个都测一遍。
6.2 部署到 Linux 服务器
# 方式一:ssh-copy-id(推荐,自动处理权限)
ssh-copy-id -i ~/.ssh/server_prod.pub ubuntu@192.168.1.100
# 方式二:手动追加
cat ~/.ssh/server_prod.pub | ssh ubuntu@192.168.1.100 \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
cat >> ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys"
authorized_keys 每行一个公钥,可以有多个:
# ~/.ssh/authorized_keys
ssh-ed25519 AAAA...abc deploy-key
ssh-ed25519 AAAA...def personal-laptop
ssh-ed25519 AAAA...ghi work-macbook
# ^^^^^^^^^^^^^ 注释在这里体现价值 —— 需要吊销时知道删哪行
吊销一个密钥就是删掉对应的一行:
# 按注释删除(离职、设备丢失时)
ssh user@server "sed -i '/work-macbook/d' ~/.ssh/authorized_keys"
# 按指纹确认要删的是哪一行
ssh-keygen -lf ~/.ssh/authorized_keys
7. known_hosts 全解
7.1 作用与原理
known_hosts 记录客户端曾经连接过的服务器的公钥,用于检测中间人攻击。原理见第 16 篇 §2.2,这里讲操作细节。
服务器的 host key 与你的个人密钥是两套不同的东西,这一点必须分清:
| 个人密钥对 | 服务器 host key | |
|---|---|---|
| 作用 | 证明你是你 | 证明服务器是服务器 |
| 私钥位置 | ~/.ssh/id_ed25519 |
/etc/ssh/ssh_host_ed25519_key |
| 公钥给谁 | 上传到 GitHub / 服务器的 authorized_keys |
连接时发给客户端,存入客户端的 known_hosts |
| 谁验证 | 服务器验证客户端 | 客户端验证服务器 |
# 服务器上的 host key(安装 sshd 时自动生成)
ls /etc/ssh/ssh_host_*
# /etc/ssh/ssh_host_ed25519_key <- 服务器私钥
# /etc/ssh/ssh_host_ed25519_key.pub <- 服务器公钥
# /etc/ssh/ssh_host_rsa_key
「指纹」就是公钥的 SHA256 哈希——公钥内容太长不便人工比对,哈希成一个短摘要:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# 256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU root@server (ED25519)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 这就是指纹
本质上和下载软件后校验 sha256sum 是同一个道理:公钥变一个字节,指纹就完全不同。
7.2 文件格式
cat ~/.ssh/known_hosts
每行一条记录:主机名/IP 算法 公钥
# 明文主机名
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
192.168.1.100 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
[192.168.1.100]:2222 ssh-ed25519 AAAA... <- 非默认端口的写法
# HashKnownHosts yes 时主机名被哈希,看不出连过哪些机器
|1|abc123==|xyz456== ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
HashKnownHosts yes 是一项隐私/安全加固:如果开发机被入侵,攻击者读 known_hosts 就能知道你连过哪些服务器(横向移动的目标清单)。哈希后就看不出来了。
# ~/.ssh/config
Host *
HashKnownHosts yes
代价是无法直接 grep 找某台机器,但 ssh-keygen -F 和 -R 仍然能正常工作。
7.3 常用操作
# 查询某主机的记录是否存在
ssh-keygen -F github.com
# 存在则输出该行,不存在则无输出(退出码非 0)
# 删除某台主机的记录
ssh-keygen -R 192.168.1.100
ssh-keygen -R "[192.168.1.100]:2222" # 非默认端口
# 扫描服务器的公钥
ssh-keyscan 192.168.1.100
ssh-keyscan -t ed25519 github.com # 只要 ed25519
# 批量预填充(可信内网环境)
ssh-keyscan -t ed25519 10.0.0.1 10.0.0.2 10.0.0.3 >> ~/.ssh/known_hosts
# 看有多少条记录
wc -l ~/.ssh/known_hosts
known_hosts 是普通文本文件,没有数量限制,几千台服务器都没问题。
7.4 host key 变化的处理
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Offending ECDSA key in ~/.ssh/known_hosts:5
^ 告诉你是第几行
先判断原因,再决定是否清除:
| 原因 | 判断依据 | 处理 |
|---|---|---|
| 服务器重装系统 | 你自己知道刚重装过 | 清除旧记录 |
| 云主机 IP 被回收后分配给别人 | 弹性 IP、动态 IP 场景 | 清除旧记录 |
| 容器/K8s Pod 重建 | 每次重建都生成新 host key | 清除,或配置固定 host key |
| 中间人攻击 | 以上都不成立 | 停止连接,排查网络 |
# 确认是正常原因后清除
ssh-keygen -R 192.168.1.100
# 或按行号删(知道行号时更快)
sed -i '5d' ~/.ssh/known_hosts
# 重新连接,重新确认新指纹
ssh ubuntu@192.168.1.100
容器场景的根治办法:把 host key 挂载进容器,让每次重建都用同一份,就不会反复报警:
# K8s:把 host key 放进 Secret 挂载到 /etc/ssh/
volumeMounts:
- name: ssh-host-keys
mountPath: /etc/ssh/ssh_host_ed25519_key
subPath: ssh_host_ed25519_key
7.5 跳过检查:仅限可信内网
# ~/.ssh/config
Host 10.0.0.*
StrictHostKeyChecking accept-new # ✅ 推荐:接受新主机,但【拒绝变化的】
# StrictHostKeyChecking no # ⚠️ 新主机自动信任,变化也只警告不阻止
Host build-agent-*
StrictHostKeyChecking no
UserKnownHostsFile /dev/null # 不留记录,每次都当新主机
LogLevel ERROR # 顺便压掉 "Permanently added" 警告
accept-new 比 no 安全得多(OpenSSH 7.6+):
accept-new -> 首次连接自动接受并记录,但如果【已记录的】host key 变了,仍然报警拒绝
no -> 首次自动接受,变化时也只是警告,仍然连接 <- 完全失去 MITM 防护
**规律:需要免交互时用 accept-new,不要用 no。**CI 环境里 UserKnownHostsFile /dev/null + StrictHostKeyChecking no 是常见写法,但它等于彻底关闭了中间人防护——更好的做法是在 CI 里预置正确的 host key:
# CI 脚本里:预置已知的 host key,而不是关闭检查
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$KNOWN_HOSTS_CONTENT" > ~/.ssh/known_hosts # 从 CI Secret 注入
chmod 644 ~/.ssh/known_hosts
8. 常见问题排查
8.1 确认当前用的是哪个密钥
ssh -vT git@github.com 2>&1 | grep -iE "offering|accepts|authenticated"
# debug1: Offering public key: /home/user/.ssh/github_work ED25519 SHA256:yyy explicit
# debug1: Server accepts key: /home/user/.ssh/github_work ED25519
# Hi work-username! You've successfully authenticated
# ^^^^^^^^^^^^^ 最终确认
8.2 Permission denied (publickey)
多账号场景下的排查顺序(服务器场景的排查见第 16 篇 §10.1):
# ① 公钥有没有上传到【对应的】账号
ssh-keygen -lf ~/.ssh/github_work.pub
# 256 SHA256:yyy work@company.com (ED25519)
# 去 GitHub Settings -> SSH keys 比对指纹是否存在
# ② remote 用的是别名还是真实域名
git remote -v
# origin git@github.com:company/repo.git <- ❌ 应该是 github-work
git remote set-url origin git@github-work:company/repo.git
# ③ 是不是 agent 里的密钥抢先了
ssh -v git@github-work 2>&1 | grep -i offering
# 如果 offering 的不是期望的密钥 -> 加 IdentitiesOnly yes
# ④ Too many authentication failures
ssh-add -l | wc -l # agent 里密钥太多
ssh -o IdentitiesOnly=yes -i ~/.ssh/github_work -T git@github.com
GitHub 的 Repository not found 也可能是密钥问题——对没有权限的私有仓库,GitHub 返回的是「不存在」而不是「无权限」(避免泄露仓库存在性)。所以看到这个报错先用 ssh -T 确认当前身份对不对。
8.3 其他密钥相关操作
# 查看密钥指纹(与 GitHub 上的记录比对)
ssh-keygen -lf ~/.ssh/github_personal.pub
# 256 SHA256:xxxx personal@gmail.com (ED25519)
# 修改 / 添加 / 删除私钥口令(不需要重新生成密钥对)
ssh-keygen -p -f ~/.ssh/github_personal
# Enter old passphrase:
# Enter new passphrase (empty for no passphrase):
# 从私钥还原公钥(丢了 .pub 文件时)
ssh-keygen -y -f ~/.ssh/github_personal > ~/.ssh/github_personal.pub
# 转换密钥格式(与其他工具互操作)
ssh-keygen -e -m PEM -f ~/.ssh/id_rsa.pub > id_rsa_pem.pub # 导出为 PEM
ssh-keygen -i -m PKCS8 -f pkcs8.pub > openssh.pub # 导入
ssh-keygen -y 能从私钥恢复公钥,这是因为公钥的信息完全包含在私钥文件里——所以「私钥丢了公钥还在」是无法恢复的,反之可以。
9. 实战:一台开发机上配置四个身份
场景:新入职,一台 MacBook 需要同时访问:个人 GitHub、公司 GitHub(不同账号)、公司 GitLab、三台生产服务器(走跳板机)。
① 生成四个密钥
cd ~/.ssh
ssh-keygen -t ed25519 -C "me@gmail.com" -f github_personal
ssh-keygen -t ed25519 -C "me@company.com" -f github_work
ssh-keygen -t ed25519 -C "me@company.com" -f gitlab_work
ssh-keygen -t ed25519 -C "me@company.com-srv" -f server_work
chmod 600 github_personal github_work gitlab_work server_work
chmod 644 *.pub
chmod 700 ~/.ssh
② 写 config
# ~/.ssh/config
# ---------- 个人 GitHub ----------
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes
# ---------- 公司 GitHub(别名区分)----------
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/github_work
IdentitiesOnly yes
# ---------- 公司 GitLab ----------
Host gitlab.company.com
HostName gitlab.company.com
User git
Port 2222
IdentityFile ~/.ssh/gitlab_work
IdentitiesOnly yes
# ---------- 跳板机 ----------
Host bastion
HostName bastion.company.com
User myname
Port 22
IdentityFile ~/.ssh/server_work
IdentitiesOnly yes
# ---------- 内网服务器(自动走跳板机)----------
Host web1
HostName 10.0.1.11
User deploy
IdentityFile ~/.ssh/server_work
IdentitiesOnly yes
ProxyJump bastion # 第 16 篇 §7,不用 -A
Host web2
HostName 10.0.1.12
User deploy
IdentityFile ~/.ssh/server_work
IdentitiesOnly yes
ProxyJump bastion
Host db1
HostName 10.0.2.11
User deploy
IdentityFile ~/.ssh/server_work
IdentitiesOnly yes
ProxyJump bastion
# ---------- 全局默认(必须最后)----------
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
UseKeychain yes # macOS
HashKnownHosts yes
ControlMaster auto # 第 16 篇 §4.3,连接复用
ControlPath ~/.ssh/cm-%C
ControlPersist 10m
用 10.0.1.* 通配符可以进一步简化,但显式列出每台机器的好处是 ssh <TAB> 能补全主机名。
③ 部署公钥
# GitHub / GitLab:网页上传
pbcopy < ~/.ssh/github_personal.pub # 粘到个人 GitHub
pbcopy < ~/.ssh/github_work.pub # 粘到公司 GitHub
pbcopy < ~/.ssh/gitlab_work.pub # 粘到 GitLab
# 服务器:先传到跳板机,再从跳板机分发(内网机器直连不了)
ssh-copy-id -i ~/.ssh/server_work.pub bastion
# 跳板机能连内网后,通过 ProxyJump 部署到内网机器
ssh-copy-id -i ~/.ssh/server_work.pub web1
④ 逐项验证
# 验证身份是否正确(最关键的一步)
ssh -T git@github.com
# Hi personal-name! ... ✅ 个人账号
ssh -T git@github-work
# Hi work-name! ... ✅ 公司账号,说明别名生效
ssh -T git@gitlab.company.com
# Welcome to GitLab, @work-name! ✅
# 验证服务器能通,且走的是跳板机
ssh web1 hostname
# web1 ✅
ssh -v web1 2>&1 | grep -i "proxy\|jump"
# debug1: Executing proxy command: exec ssh -l myname -W '[10.0.1.11]:22' bastion
# ^^^^^^^ 确认走跳板
# 验证连接复用生效
ssh web1 true # 第一次
time ssh web1 true # 第二次应该快很多
# real 0m0.048s ✅ 复用了
ssh -O check web1
# Master running (pid=54321) ✅
# 验证密钥选择正确(没有被 agent 里的密钥抢先)
ssh -v web1 2>&1 | grep -i offering
# debug1: Offering public key: /Users/me/.ssh/server_work ED25519 ... explicit
# ^^^^^^^^^^^ 正确 ^^^^^^^^ 来自 config
⑤ clone 时用对别名
# 个人项目
git clone git@github.com:me/my-project.git
# 公司项目 —— 注意用别名
git clone git@github-work:company/backend.git
# 顺便配置不同的提交身份(避免用个人邮箱提交公司代码)
cd backend
git config user.email "me@company.com"
git config user.name "My Name"
更好的做法是用 git 的条件包含,按目录自动切换身份:
# ~/.gitconfig
[user]
name = My Name
email = me@gmail.com # 默认用个人邮箱
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work # ~/work/ 下的仓库用公司配置
# ~/.gitconfig-work
[user]
name = My Name
email = me@company.com
这样把公司项目都 clone 到 ~/work/ 下,提交身份就自动正确了——不需要每个仓库手动 git config。
规律:多账号配置完,一定要用 ssh -T 逐个验证「显示的用户名是不是期望的那个」。配置看起来对但实际用错密钥,是这类问题最常见的形态。
10. 最佳实践清单
密钥生成
- 统一用 Ed25519
-C注释必填,写明用途或邮箱——吊销时靠它辨认- 不同用途用不同密钥,泄露隔离、可独立吊销
- 交互使用的密钥设 passphrase,配合 ssh-agent;自动化密钥不设但要用
command=限制
文件管理
- 私钥
600、公钥644、.ssh目录700、家目录不能 group/other 可写 - 所有路由规则集中在
~/.ssh/config,不要靠记命令行参数 - 同域名多账号用不同
Host别名区分
config 关键项
IdentitiesOnly yes # 多密钥必须加,防 agent 干扰和 Too many failures
ServerAliveInterval 60 # 防超时断开
StrictHostKeyChecking accept-new # 免交互时用这个,不要用 no
HashKnownHosts yes # 避免泄露连过哪些机器
ControlMaster auto # 连接复用,显著加速
AddKeysToAgent yes # 自动加入 agent
UseKeychain yes # macOS 持久化口令
排查三件套
ssh -v <host> # 看密钥选择过程和卡在哪个阶段
ssh-add -l # 看 agent 里缓存了哪些密钥
ssh-keygen -lf <key.pub> # 看指纹,与远端记录比对
ssh -G <host> # 看 config 最终生效的配置
11. 面试题
Q:一台机器上有个人和公司两个 GitHub 账号,怎么配置?
核心是用不同的 Host 别名指向同一个 HostName。在 ~/.ssh/config 里写两段:Host github.com 配个人密钥,Host github-work 配公司密钥但 HostName github.com。之后 clone 公司仓库时把域名换成别名:git clone git@github-work:company/repo.git。两个必须做的配套动作:① 每段都加 IdentitiesOnly yes,否则 ssh-agent 里的密钥会抢先被尝试,可能用错身份;② 用 git 的 includeIf "gitdir:~/work/" 按目录自动切换提交邮箱,避免用个人邮箱提交公司代码。配完用 ssh -T git@github-work 验证显示的用户名是否正确。
Q:IdentitiesOnly yes 是什么作用?不加会有什么问题?
不加时,SSH 会先把 ssh-agent 里的所有密钥逐个尝试,然后才用 config 里 IdentityFile 指定的那个。两个后果:① Too many authentication failures——服务端有 MaxAuthTries 限制(默认 6),agent 里有 8 个密钥就会在轮到正确密钥之前被断开;② 用错身份——多账号场景下,如果个人密钥在 agent 里排在前面且对目标仓库也有权限,认证会用个人身份成功,导致提交记录作者错误。加上 IdentitiesOnly yes 后只尝试指定的密钥。规律:config 里写了 IdentityFile 就应该同时写 IdentitiesOnly yes。
Q:SSH 选择密钥的优先级是什么?怎么确认实际用了哪个?
顺序是:命令行 -i > config 的 IdentityFile > ssh-agent 里的密钥 > 默认密钥文件(id_ed25519 → id_ecdsa → id_rsa → id_dsa)> 交互输密码。确认方法是 ssh -v <host> 2>&1 | grep -i offering,输出末尾的标记直接说明来源:explicit = 来自 config 或 -i,agent = 来自 ssh-agent,default = 默认路径。如果看到 agent 的密钥在 explicit 之前被尝试,就是缺 IdentitiesOnly yes。
Q:ssh-agent 的原理是什么?私钥会通过它泄露吗?
ssh-agent 是一个后台进程,把解密后的私钥缓存在自己的内存里,通过 Unix domain socket($SSH_AUTH_SOCK)对外提供服务。SSH 客户端需要认证时,通过 socket 请求 agent「用某个密钥对这段数据签名」,私钥本身永远不通过 socket 传输,只传签名结果。所以即使有人能访问 socket,也拿不到私钥文件——但已经足够冒充你登录任何地方,这就是第 16 篇讲的 Agent Forwarding 风险的来源。缓解手段:ssh-add -t 3600 设置自动过期、ssh-add -c 让每次使用都需要人工确认、以及优先用 ProxyJump 而不是 -A。
Q:known_hosts 里存的是什么?和 authorized_keys 有什么区别?
两者方向完全相反。known_hosts(客户端) 存的是服务器的 host key 公钥,用于客户端验证「这台服务器是不是我上次连的那台」,防中间人攻击;对应的私钥在服务器的 /etc/ssh/ssh_host_*_key。authorized_keys(服务端) 存的是用户的公钥,用于服务器验证「你是不是你」;对应的私钥在客户端的 ~/.ssh/。一句话:known_hosts 证明服务器是服务器,authorized_keys 证明你是你。这也是为什么 host key 变了报的是 Host key verification failed(第二阶段),而公钥不对报的是 Permission denied (publickey)(第三阶段)。
Q:StrictHostKeyChecking no 和 accept-new 有什么区别?CI 里该用哪个?
accept-new(OpenSSH 7.6+):首次连接自动接受并记录,但如果已记录的 host key 发生变化,仍然报警并拒绝连接——保留了中间人检测能力。no:首次自动接受,host key 变化时也只是警告、仍然继续连接——等于彻底关闭 MITM 防护。所以免交互场景应该用 accept-new。CI 里常见的 StrictHostKeyChecking no + UserKnownHostsFile /dev/null 写法虽然方便,但完全放弃了安全检查;更好的做法是在 CI Secret 里预置正确的 known_hosts 内容,写入文件后正常做严格检查。
Q:私钥的 passphrase 有什么用?什么场景该设、什么场景不该设?
passphrase 加密私钥文件本身——即使文件被复制走,没有口令也无法使用,是私钥泄露后的最后一道防线。该设的场景:个人开发机、笔记本(可能丢失或被盗)、任何有交互能力的环境,配合 ssh-agent 只需在会话开始时输一次,macOS 还能存进 Keychain 免输。不该设的场景:CI/CD 和自动化脚本用的密钥(无法交互输入口令)——但这类密钥必须用其他手段限制风险,最有效的是在 authorized_keys 里加 restrict,command="/opt/deploy.sh",让密钥即使泄露也只能执行一条固定命令、拿不到 shell(第 16 篇 §3.4)。另外 ssh-keygen -p -f <key> 可以随时给已有密钥加、改、删口令,不需要重新生成密钥对。
Q:私钥丢了但公钥还在,能恢复吗?反过来呢?
私钥丢了无法恢复——公钥是从私钥数学推导出来的,反向不可行,只能重新生成密钥对并重新部署所有公钥。反过来,公钥丢了可以从私钥恢复:ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub,因为公钥的全部信息都包含在私钥文件里。这也解释了为什么私钥文件比公钥大——它包含了公钥的内容。
上一篇:Linux-16 SSH 远程访问原理与实战 | 下一篇:Linux-18 性能分析工具链与方法论
xingliuhua