目录

SSH 密钥管理

SSH 密钥管理

1. 密钥对原理回顾

SSH 密钥认证使用非对称加密:本机持有私钥,远端(GitHub / 服务器)保存公钥

私钥  ~/.ssh/id_ed25519       ← 绝对不能泄露,只存本机
公钥  ~/.ssh/id_ed25519.pub   ← 可以任意分发,上传到 GitHub / 服务器

认证流程(私钥从不离开本机):

Client                                 Server
  |  ① 发起连接,声明用户名              |
  |-------------------------------------> |
  |                                       |
  |  ② 找到对应公钥,生成随机挑战 R       |
  |     用公钥加密 R,发回                |
  |<------------------------------------- |
  |                                       |
  |  ③ 用本地私钥解密 R                   |
  |     计算 hash(R + session_id) 发回   |
  |-------------------------------------> |
  |                                       |
  |  ④ 验证 hash 是否正确                 |
  |<---------- 登录成功 / 失败 ---------- |

2. 生成密钥对

2.1 基本命令

ssh-keygen -t ed25519 -C "your_email@example.com"

各参数含义:

参数 说明
-t ed25519 指定算法类型
-C "注释" 附加注释,会写入公钥末尾,方便识别密钥归属
-f ~/.ssh/id_ed25519 指定保存路径(不指定则交互询问)
-b 4096 密钥位数(RSA 专用,Ed25519 固定 256 bit 忽略此参数)
-N "passphrase" 直接指定私钥口令(空字符串表示无口令)

2.2 算法选择

算法 密钥长度 安全性 速度 推荐程度
Ed25519 256 bit ★★★★★ 最快 首选
ECDSA 256 bit ★★★★ 可用(部分老系统不支持)
RSA 4096 4096 bit ★★★★ 兼容老系统时用
RSA 2048 2048 bit ★★★ 较慢 不推荐新建
DSA 1024 bit 已淘汰,禁用

结论: 现代系统(OpenSSH 6.5+,即 2014 年后)一律用 Ed25519

2.3 交互过程详解

执行 ssh-keygen -t ed25519 -C "me@example.com" 后:

# 第一步:选择保存路径
Enter file in which to save the key (/home/user/.ssh/id_ed25519):
  • 直接回车:使用默认路径 ~/.ssh/id_ed25519
  • 输入自定义路径:如 ~/.ssh/github_work(多账号时必须自定义)
# 第二步:设置私钥口令(passphrase)
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
  • 口令的作用:私钥文件本身会被加密,即使文件被盗,没有口令也无法使用
  • 留空:无口令,使用方便但文件安全性低
  • 建议:重要密钥(如 GitHub 个人账号)设置口令,配合 ssh-agent 使用

生成结果:

Your identification has been saved in /home/user/.ssh/id_ed25519
Your public key has been saved in /home/user/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:ABC123...xyz me@example.com
The key's randomart image is:
+--[ED25519 256]--+
|        .o+o.    |
...
+----[SHA256]-----+

2.4 生成的文件

~/.ssh/
├── id_ed25519       ← 私钥,权限必须 600
└── id_ed25519.pub   ← 公钥,权限 644

私钥权限必须是 600,否则 SSH 会拒绝使用:

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

3. 本地多密钥管理

实际使用中通常需要多个密钥:个人 GitHub、公司 GitHub、各台服务器……

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

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 ~/.ssh/config 配置文件

config 文件是多密钥管理的核心,为不同主机指定使用哪个密钥

# ============ GitHub 个人账号 ============
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_personal
    IdentitiesOnly yes         # 只用指定的密钥,不尝试其他

# ============ GitHub 公司账号 ============
# 用不同的 Host 别名区分同一个域名的不同账号
Host github-work
    HostName 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

IdentitiesOnly yes 的作用:不加这行时,SSH 还会把 ssh-agent 中的所有密钥都尝试一遍,在多密钥场景下容易混乱,加上后只用 IdentityFile 指定的密钥。

3.4 多账号 clone / push

配置好 config 后,clone 公司仓库时把 github.com 替换为 config 中的别名:

# 个人 GitHub(Host 是 github.com,直接用)
git clone git@github.com:personal/repo.git

# 公司 GitHub(Host 别名是 github-work)
git clone git@github-work:company/repo.git
#            ^^^^^^^^^^^^ 这里用 config 里的 Host 别名

# 已有仓库修改 remote
git remote set-url origin git@github-work:company/repo.git

3.5 clone 时临时指定密钥

不想改 config、或者临时用一次,有三种方式:

方式一:GIT_SSH_COMMAND 环境变量(推荐)

GIT_SSH_COMMAND="ssh -i ~/.ssh/github_work" git clone git@github.com:company/repo.git

只作用于当次命令,不影响全局。

方式二:-c core.sshCommand 内联参数

git clone -c "core.sshCommand=ssh -i ~/.ssh/github_work" git@github.com:company/repo.git

效果与方式一相同,区别是 -c 的写法更适合在脚本里组合其他 git 参数。

方式三:clone 后对该仓库持久生效

git clone git@github.com:company/repo.git
cd repo

# 只对这个仓库生效,写入 .git/config
git config core.sshCommand "ssh -i ~/.ssh/github_work -o IdentitiesOnly=yes"

# 之后 git pull / push 都自动用这个密钥
git pull

三种方式对比:

方式 作用范围 适用场景
~/.ssh/config 按 Host 匹配,永久 多账号日常使用(首选)
GIT_SSH_COMMAND 单次命令 临时操作、脚本 CI
git config core.sshCommand 单个仓库,永久 仓库维度隔离

4. 密钥优先级

SSH 连接时会按照固定顺序选择密钥,理解这个顺序能避免很多"用了错误密钥"的问题。

4.1 选择顺序

命令行 -i 参数  (最高优先级)
      ↓
config 文件中的 IdentityFile
      ↓
ssh-agent 中的密钥(按加入顺序逐个尝试)
      ↓
默认密钥文件(按固定顺序)
      ↓
交互询问密码  (最低)

4.2 默认密钥文件顺序

当没有任何 config 指定时,SSH 按以下顺序查找默认密钥:

~/.ssh/id_ed25519        ← 第一个尝试(推荐算法,优先)
~/.ssh/id_ecdsa
~/.ssh/id_rsa
~/.ssh/id_dsa            ← 最后(已淘汰算法)

4.3 -i 参数(临时指定)

# 临时使用指定密钥,不走 config
ssh -i ~/.ssh/server_prod ubuntu@192.168.1.100

# 等价于在 config 中对该 Host 配置了 IdentityFile

4.4 用 -v 观察密钥选择过程

ssh -v git@github.com 2>&1 | grep -E "(identity|offer|key)"

输出示例:

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 指定;agent 说明来自 ssh-agent;default 说明是默认路径。

多加一个 -v 可以看更多细节:ssh -vvssh -vvv

4.5 IdentitiesOnly 的重要性

# 假设 ssh-agent 里有 5 个密钥,config 里指定了 IdentityFile
# 不加 IdentitiesOnly yes:会先尝试 agent 里所有密钥,再尝试指定的
# 加了 IdentitiesOnly yes:只尝试 IdentityFile 指定的密钥

服务器有最大认证尝试次数限制(通常 3-6 次),agent 里密钥太多会导致:

Received disconnect from ... : Too many authentication failures

此时加上 IdentitiesOnly yes 即可解决。


5. ssh-agent(密钥代理)

私钥设置了口令后,每次使用都要输入。ssh-agent 将解密后的私钥缓存在内存中。

5.1 基本使用

# 启动 agent(通常系统登录时自动启动)
eval "$(ssh-agent -s)"

# 添加私钥(会提示输入口令)
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

5.2 macOS 自动加载(推荐)

macOS 可以将口令保存在 Keychain,重启后自动加载:

# 添加时保存到 Keychain
ssh-add --apple-use-keychain ~/.ssh/github_personal

# ~/.ssh/config 中配置自动加载
Host *
    UseKeychain yes
    AddKeysToAgent yes

5.3 Linux 自动加载

# 方式一:写入 ~/.bashrc / ~/.zshrc
if [ -z "$SSH_AGENT_PID" ]; then
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/github_personal 2>/dev/null
fi

# 方式二:使用 systemd user service(更优雅)
# ~/.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

6. 公钥部署

6.1 部署到 GitHub / GitLab

  1. 复制公钥内容:
cat ~/.ssh/github_personal.pub
# 或 macOS 直接复制到剪贴板
cat ~/.ssh/github_personal.pub | pbcopy
  1. GitHub:Settings → SSH and GPG keys → New SSH key → 粘贴公钥

  2. 验证连接:

ssh -T git@github.com
# Hi username! You've successfully authenticated, but GitHub does not provide shell access.

# 公司账号用别名验证
ssh -T git@github-work
# Hi work-username! You've successfully authenticated...

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"

服务器端 ~/.ssh/authorized_keys 的格式:每行一个公钥,可以有多个。

# ~/.ssh/authorized_keys
ssh-ed25519 AAAA...abc deploy-key
ssh-ed25519 AAAA...def personal-laptop
ssh-ed25519 AAAA...ghi work-macbook

7. known_hosts

7.1 作用

known_hosts 记录了客户端曾经连接过的服务器的公钥指纹,用于防止中间人攻击。

SSH 的服务器公钥是自签发的,没有 CA 背书,客户端无法自动判断"这台服务器是不是真的"。known_hosts 的逻辑是:

第一次连接时你确认了这台服务器,之后每次连接都拿保存的指纹比对,一旦不匹配就报警。

7.2 服务器指纹是什么

服务器自己也有一对密钥对,叫 host key(主机密钥),在安装 SSH 服务时自动生成:

ls /etc/ssh/ssh_host_*
# /etc/ssh/ssh_host_ed25519_key      ← 服务器私钥
# /etc/ssh/ssh_host_ed25519_key.pub  ← 服务器公钥
# /etc/ssh/ssh_host_rsa_key
# /etc/ssh/ssh_host_rsa_key.pub

这对密钥和你的个人密钥作用不同:

个人密钥对 服务器 host key
作用 证明你是你 证明服务器是服务器
私钥位置 ~/.ssh/id_ed25519 /etc/ssh/ssh_host_ed25519_key
公钥给谁 上传到 GitHub / 服务器 连接时发给客户端

指纹就是服务器公钥的 SHA256 哈希值,公钥内容长、不易比对,哈希后得到一个短摘要便于人工核验:

# 在服务器本机执行
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# 256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU root@server (ED25519)
#            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 这就是指纹

连接时的完整流程:

客户端                                服务器
  |  ① 发起连接                          |
  |-------------------------------------> |
  |  ② 服务器把自己的公钥发过来            |
  |<------------------------------------- |
  |  ③ 客户端计算公钥的 SHA256            |
  |     与 known_hosts 里的记录比对       |
  |     · 有记录且一致 → 直接建立连接     |
  |     · 有记录但不一致 → 报警(可能被攻击)|
  |     · 没有记录(第一次)→ 展示指纹让你确认|
  |-------------------------------------> |

本质上和下载软件后校验 sha256sum 是同一个道理——公钥内容只要变了一个字节,指纹就完全不同。

7.3 格式

cat ~/.ssh/known_hosts

每行一条记录,格式为:主机名/IP 算法 公钥

# 明文主机名
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
192.168.1.100 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...

# 开启 HashKnownHosts 后,主机名被哈希,无法直接看出连的是哪台机器
|1|abc123==|xyz456== ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...

7.4 第一次连接的确认提示

第一次连接一台新服务器时:

The authenticity of host '192.168.1.100 (192.168.1.100)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

输入 yes 后,该服务器的指纹写入 ~/.ssh/known_hosts,下次不再提示。

如何验证指纹是否可信(防止第一次就被中间人):

# 在服务器本机执行,获取服务器自己的公钥指纹
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# 256 SHA256:+DiY3wvvV6T... root@server (ED25519)

# 与连接提示里的指纹比对,一致才输入 yes

7.5 服务器公钥变了:REMOTE HOST IDENTIFICATION HAS CHANGED

服务器重装系统、更换 IP 复用、或遭遇中间人时会报:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Offending ECDSA key in ~/.ssh/known_hosts:5

错误里会告诉你冲突记录在第几行(上面是第 5 行)。

确认是服务器重装导致的(不是攻击),删除旧记录:

# 方式一:按主机名/IP 删除(推荐)
ssh-keygen -R 192.168.1.100
ssh-keygen -R myserver.com

# 方式二:手动删除,知道行号时更快
sed -i '5d' ~/.ssh/known_hosts

# 删除后重新连接,重新确认新指纹
ssh ubuntu@192.168.1.100

7.6 常用操作

# 查询某主机的记录是否存在
ssh-keygen -F github.com
# 存在则输出该行内容,不存在则无输出

# 查看文件共有多少条记录
wc -l ~/.ssh/known_hosts

# 删除某台主机的记录
ssh-keygen -R 10.0.0.50

# 查看服务器对外暴露的所有公钥类型
ssh-keyscan 192.168.1.100
ssh-keyscan -t ed25519 github.com   # 只扫 ed25519 类型

ssh-keyscan 也可以用来批量预填充 known_hosts,省去第一次连接时逐台确认:

# 批量信任内网一批机器(在可信环境下使用)
ssh-keyscan 10.0.0.1 10.0.0.2 10.0.0.3 >> ~/.ssh/known_hosts

7.7 没有数量限制

known_hosts 是普通文本文件,无数量限制,连几千台服务器都没问题。

时间长了可以清理失效记录:

# 逐个删除已下线的机器
ssh-keygen -R 10.0.0.99

# 或直接清空(下次连接时重新确认)
> ~/.ssh/known_hosts

7.8 跳过 known_hosts 检查(内网批量场景)

对于完全可信的内网,可在 config 里关掉检查:

Host 10.0.0.*
    StrictHostKeyChecking no     # 新主机自动信任并记录,不提示
    # StrictHostKeyChecking accept-new  # 更保守:只接受新主机,拒绝指纹变化的主机

Host bastion.internal
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null # 不写入 known_hosts,连记录都不留(每次当新主机)

注意: StrictHostKeyChecking no 会绕过中间人检测,只在完全可信的内网使用,公网绝对不要开


8. 常见问题排查

8.1 验证当前使用的是哪个密钥

# 测试 GitHub 连接,看详细日志
ssh -vT git@github.com 2>&1 | grep -E "(Offering|Accepted|identity)"

8.2 Permission denied (publickey)

# 逐步排查
ssh -v git@github.com

# 常见原因:
# 1. 公钥没有上传到对应账号
#    → 去 GitHub Settings 确认 SSH keys 列表

# 2. config 里用了 github-work 别名但仓库 remote 还是 github.com
#    → git remote -v 检查,改为正确别名

# 3. authorized_keys 权限不对(服务器场景)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# 4. Too many authentication failures
#    → config 里加 IdentitiesOnly yes

8.3 查看密钥指纹(用于比对 GitHub 上的记录)

# 查看本地公钥指纹
ssh-keygen -lf ~/.ssh/github_personal.pub
# 256 SHA256:xxxx personal@gmail.com (ED25519)

# GitHub 上 Settings → SSH keys 里也能看到对应指纹,比对确认是否匹配

8.4 修改私钥口令

# 修改(或添加/删除)私钥的口令,不需要重新生成密钥对
ssh-keygen -p -f ~/.ssh/github_personal
# Enter old passphrase:
# Enter new passphrase (empty for no passphrase):
# Enter same passphrase again:

8.5 从私钥还原公钥

# 只有私钥时,可以重新导出公钥
ssh-keygen -y -f ~/.ssh/github_personal > ~/.ssh/github_personal.pub

9. 最佳实践总结

密钥生成:

  • 统一用 Ed25519,-C 注释写明用途(如邮箱或描述)
  • 不同用途使用不同密钥,泄露隔离
  • 重要密钥设置 passphrase,配合 ssh-agent

文件管理:

  • 私钥权限必须 600.ssh 目录权限 700
  • 所有路由规则集中在 ~/.ssh/config 管理
  • 同一域名多账号用不同 Host 别名区分

config 关键配置:

# 多账号必须加,防止 agent 密钥干扰
IdentitiesOnly yes

# 防止长连接超时断开
ServerAliveInterval 60

# macOS 自动加载
UseKeychain yes
AddKeysToAgent yes

排查工具:

  • ssh -v 查看密钥选择过程
  • ssh-add -l 查看 agent 已缓存密钥
  • ssh-keygen -lf 查看密钥指纹