一、为什么密钥管理比密码管理更难
很多站长把服务器安全理解成"把密码设复杂一点",但真正把机器交出去的那一刻,通常不是密码被猜到,而是某一把 SSH 私钥泄露了。密码泄露,你改一次口令就收口了;私钥泄露,你不知道它已经被复制到多少台机器、多少个备份、多少个聊天窗口里。更麻烦的是,私钥本来就是为了"免密登录方便"而生的,方便和失控往往只隔一层。
下面这套做法是我在一台跑着十几个小站的机器上逐年磨出来的。它不追求企业级堡垒机那样的完备,但覆盖了个人站长真实会遇到的问题:谁能登录、怎么登录、出了事怎么知道、事后怎么收拾。
二、先把"谁能登录"这件事变成可枚举的
动手之前先做一次盘点,把"可能进得来的人"全部列出来。这一步很多人跳过,结果改了半天配置,发现还有个 root 密码登录的口子留着。
第一类是密钥登录的用户,看每个账号的授权文件:
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
[ -f "$f" ] || continue
echo "== $f"
awk '{print NR": "substr($1,1,20)"... "$NF}' "$f"
done第二类是密码登录的口子。注意不只是 sshd 的 PasswordAuthentication,还有面板、数据库、web 管理端。先用一条命令确认 sshd 的实际生效值,别只看配置文件写了什么,因为 Include 和 Match 块会让配置文件"看起来关了、其实是开的":
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|pubkeyauthentication|permitemptypasswords|maxauthtries|allowusers|allowgroups'sshd -T 输出的是合并所有配置后的最终值,这是唯一可信的口径。第三类是"绕过 sshd"的口子:面板的网页终端、某些监控 agent 自带的反向 shell、以及 cron 里跑的 ssh 命令用的那把私钥。第三类最容易被忘,也最容易在事后复盘时找不到源头。
三、私钥的存放:口令 + 权限 + 不落地
一把没有口令的私钥,等价于一张写着服务器密码的纸,谁捡到谁能用。给私钥加口令(passphrase)之后,即使文件被拿走,对方还得爆破口令。命令行生成时直接指定加密算法,比用默认值更可控:
ssh-keygen -t ed25519 -a 100 -C "ops@laptop-2026" -f ~/.ssh/id_ed25519_site-a 100 是 KDF 轮数,让口令验证变慢,从而让离线爆破变贵。日常使用配合 ssh-agent,只在登录会话时输入一次口令:
eval "$(ssh-agent -s)"
ssh-add -t 8h ~/.ssh/id_ed25519_site
ssh-add -l-t 8h 是让密钥在 agent 里最多停留 8 小时,过期自动清除。这个参数比"登录一次就永久有效"安全得多,尤其是笔记本合盖、共享桌面这类场景。
权限方面,OpenSSH 对权限的要求非常严格,权限宽松会直接拒绝使用密钥。正确的权限是目录 700、文件 600:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_site ~/.ssh/config
chmod 644 ~/.ssh/id_ed25519_site.pub ~/.ssh/authorized_keys
chmod 600 ~/.ssh/known_hosts注意 authorized_keys 是 644 而不是 600,公钥文件本身不敏感,600 反而会让某些严格模式的守护进程读不到。相反 known_hosts 建议 600,它记录了你连过哪些机器,本身就是一份情报。
最后一条原则是"私钥不落地到服务器"。很多人图省事,在 A 机器上生成密钥,再用 scp 把私钥传到 B 机器,让 B 去连 C。这样一来 B 一被拿下,C 也保不住了。正确做法是让 B 生成自己的密钥对,把 B 的公钥加到 C 上,形成"每个节点一把钥匙"的结构:任何一把丢了,只需要在 C 上删掉对应那一行公钥。
四、authorized_keys 里的选项:把一把钥匙关进笼子
很多人以为公钥只能"全权限登录",其实 authorized_keys 每行前面可以加一串选项,把一个账号削成只能做一件事。备份脚本用的账号最适合这么处理:
command="/usr/local/bin/backup-receive.sh",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3Nza... backup@node1关键选项的含义分别是:command= 强制登录后只执行这一条命令,不管对方请求什么 shell;no-pty 禁止分配交互终端;no-port-forwarding 和 no-agent-forwarding 禁止把这条连接当成隧道用,从而杜绝"用备份账号做跳板"的可能。
还有一个非常好用的限制是来源地址:
from="203.0.113.10,10.0.0.0/8" ssh-ed25519 AAAAC3Nza... ops@laptopfrom= 把这个 key 绑定到特定源 IP 或网段,即使私钥被复制走,对方不在那个网段里也用不了。这是个人站长能做到的、性价比最高的一道保险:家里/办公室固定 IP 的运维 key,加上 from= 之后风险直接降一个量级。
需要提醒的是,command= 后面的脚本必须自己校验参数 $SSH_ORIGINAL_COMMAND,否则对方可以传任意参数给这条命令。例如 rsync 的接收端要通过 rrsync 这类包装器限定目录,否则一条 command= 也可能被用来读写任意路径。
五、把 SSH 端口暴露面收窄
服务端的配置调整记两条原则:一是密钥优先、密码兜底关掉;二是 root 只允许密钥登录,或者干脆禁止 root 直接登录。核心片段如下:
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin prohibit-password
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 20
AllowUsers deploy opsAllowUsers 是白名单,只要写了它,名单外的账号连认证阶段都进不去。它带来的一个副作用是:某些自动化工具会创建临时账号,加了白名单之后这些账号会连不上。所以上线时先在一台机器上试,确认监控、备份、部署脚本都还正常,再推到其它机器。
另外 MaxAuthTries 3 配合 PerSourcePenalties(OpenSSH 9.8 之后才有)可以显著减少爆破噪音。老版本的机器用 fail2ban 兜底也行,但记住一点:爆破只是噪音,真正要防的是密钥泄露,别把精力全花在封 IP 上。
改完配置务必先用 sshd -t 校验语法,再 systemctl reload sshd。热重载不会断开现有连接,这给了你一条退路:如果新配置有问题,当前这个会话还能救回来,千万别用 restart,更不要在只有一条连接的情况下直接改。
六、泄露之后:撤销、轮换与审计
私钥泄露的第一反应不应该是"赶紧改密码",而是"先撤销这把钥匙,再确认它被用过什么"。撤销就是在 authorized_keys 里删掉那一行,步骤很短,但要注意批量:
grep -v 'AAAA…有问题的key指纹尾部…' ~/.ssh/authorized_keys > /tmp/ak && mv /tmp/ak ~/.ssh/authorized_keys
ssh-keygen -lf ~/.ssh/authorized_keys确认要删的那一把,最好用指纹而不是凭肉眼比字符串。ssh-keygen -lf 会打印每把 key 的 SHA256 指纹和注释,与生成后当场的指纹对照,能避免删错行。
然后是"确认它被用过什么"。SSH 登录日志能告诉你时间、来源 IP、用的是什么认证方式:
journalctl -u ssh --since "7 days ago" | grep -i accepted
last -a | head -20Accepted publickey for deploy from 203.0.113.10 这类行是审计主线。如果泄露的那把 key 在可疑时间段内出现过登录,那么接下来要做的就不是换钥匙,而是按入侵处理:查 ~/.bash_history、查 cron、查新增的系统账号和 authorized_keys,以及查是否有新的出站连接。
最后是轮换。密钥轮换不必搞得像企业那样复杂,个人站长的可行做法是:每年一次全量换新,或者每季度检查一遍 authorized_keys 里有没有已经不用的 key。判断"不用了"有一个简单的办法:给每把 key 的注释带上生成日期和用途,例如 -C "backup-2026q3@node1",半年后回头看注释就知道该不该清。
另外建议在 ~/.ssh/config 里为生产机和测试机配不同的 key,不要一把钥匙通吃:
Host prod-*
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
ServerAliveInterval 30
Host *
HashKnownHosts yes
AddKeysToAgent confirmIdentitiesOnly yes 防止 agent 把其它 key 一起递过去(有些服务器会因为尝试次数超限而拒绝),AddKeysToAgent confirm 会让 agent 在每次使用前询问,等于加了一道人工确认。
七、用一套检查清单收尾
配置改完之后,用下面四条做验收,都过了才算收工:
- 用没有私钥的环境(例如另一台机器、或者
ssh -o IdentitiesOnly=yes -i /dev/null)尝试连接,必须失败,否则说明还有密码或其它认证方式活着; sshd -T复查关键项,确认passwordauthentication no、permitrootlogin是期望值;- 逐个登录白名单里的账号,确认运维、备份、部署三类都能正常工作,避免"安全了但业务断了";
- 把当前
authorized_keys的内容和指纹导出存档,下次出事时这就是对照基线。
最后一句经验:密钥管理真正难的不是加密算法,而是"记住有几把钥匙、它们分别能开哪扇门"。把这件事写进一个文件、每年对一次账,比换成任何高级认证方式都管用。