1. 为什么关掉密码登录是服务器安全的第一道硬门槛
我第一次在凌晨三点被短信惊醒,是监控系统告警:某台对外暴露的测试服务器在五分钟内遭遇了237次SSH暴力破解尝试,IP来自三个不同国家的动态出口节点。登录后发现,虽然没被攻破——毕竟密码设得还算复杂——但/var/log/auth.log里密密麻麻的Failed password for root from ...已经刷屏到需要tail -n 5000才能看到最新日志。更糟的是,其中两次尝试成功匹配了开发同事临时设置的弱密码admin123,所幸他只开了/tmp目录权限,没造成实质损失。这件事之后,我花了整整一个下午把团队所有21台生产、预发、跳板服务器全部切换为纯公钥认证模式,并禁用密码登录。不是因为“听说这样更安全”,而是亲眼看见:只要密码登录开着,攻击者就永远在敲门;而一旦关掉,那扇门就彻底焊死了——他们连试错的机会都没有。
这正是本篇要解决的核心问题:关闭SSH密码登录,强制使用公钥密钥登录,不是锦上添花的“高级配置”,而是对抗自动化暴力攻击最直接、最廉价、最有效的基础防线。它不依赖防火墙规则的复杂度,不仰仗WAF的误判率,也不需要购买商业入侵检测系统。它只做一件事:让攻击者无法通过反复提交用户名+密码组合来耗尽你的防御资源。你不需要懂加密算法,但必须理解:公钥登录的本质,是把“你知道什么”(密码)升级为“你拥有什么”(私钥文件),而后者无法被远程穷举。这篇文章面向所有管理Linux服务器的人——无论是刚配好VPS的个人开发者,还是负责百台集群的运维工程师。只要你还在用密码登录SSH,你就站在风险的起点。接下来,我会带你从生成密钥对开始,一步步完成服务端配置、客户端验证、故障回滚方案,以及那些官方文档绝不会写的、真实环境里踩过的坑。
2. 公钥登录不是“换种方式输密码”,而是彻底重构身份验证逻辑
很多人第一次配置公钥登录失败,根本原因在于误解了它的底层机制。他们以为只是“把密码换成一串长字符串”,于是把公钥内容复制粘贴进~/.ssh/authorized_keys后,就期待ssh user@host能自动走密钥流程。结果报错Permission denied (publickey),然后开始疯狂搜索“ssh公钥不生效”,却忽略了最关键的前置条件:SSH服务端必须明确知道“只接受公钥,拒绝密码”,且客户端必须主动声明“我要用这个私钥”。这不是功能开关,而是一套协同工作的协议栈。
2.1 SSH身份验证的三阶段握手:为什么密码和公钥不能共存
SSH连接建立时的身份验证并非单次操作,而是一个多轮协商过程。以OpenSSH 8.9p1为例,当客户端发起连接,服务端会按顺序提供它支持的认证方法列表:
debug1: Next authentication method: publickey debug1: Offering public key: /Users/me/.ssh/id_rsa RSA SHA256:AbC123... explicit agent debug1: Server accepts key: /Users/me/.ssh/id_rsa RSA SHA256:AbC123... explicit agent debug1: Next authentication method: password debug1: Password authentication failed这段调试日志揭示了一个残酷现实:即使你配置了公钥,只要服务端PasswordAuthentication yes开着,它就会在公钥验证失败后,自动降级尝试密码验证。攻击者正是利用这一点——他们不关心你有没有配公钥,只管狂轰滥炸密码字段。所以,单纯“添加公钥”只是半步,真正的安全闭环,必须由服务端强制切断密码验证通道。这就像给银行金库装了虹膜识别门禁,但如果旁边还留着一把老式挂锁,小偷根本不会费劲去破解虹膜,直接撬锁就行。
2.2 公钥体系的物理隐喻:钥匙与锁的不可逆关系
理解密钥对,用生活化类比最直观:
- 你的私钥(
id_rsa)就是你随身携带的唯一物理钥匙,它必须绝对保密,不能复制、不能截图、不能上传到任何云盘。它甚至不该出现在服务器上(除非是用于自动化脚本的专用密钥,且需严格权限控制)。 - 你的公钥(
id_rsa.pub)就是这把钥匙的公开模具,你可以把它印在名片上、发到论坛、贴在GitHub主页——它本身毫无价值,因为任何人拿到模具,都无法反向锻造出原钥匙。服务器保存的authorized_keys文件,本质上就是一堆这样的“模具集合”。 - SSH登录过程就是:你出示钥匙(私钥签名),服务器用模具(公钥)当场验证这个签名是否由对应钥匙生成。整个过程不传输钥匙本身,只传输数学证明。
这个类比解释了为什么chmod 600 ~/.ssh/id_rsa是铁律:如果钥匙丢了,模具再公开也没用;但如果钥匙被复制,模具就立刻失效。这也是为什么我们后续会强调:私钥文件权限错误,是导致“公钥已添加却仍提示密码”的头号原因。系统宁可拒绝登录,也不愿冒私钥被恶意读取的风险。
2.3 密钥类型选择:RSA、ECDSA、Ed25519,哪个才是2024年的最优解?
OpenSSH支持多种非对称加密算法,新手常纠结选哪个。这不是玄学,而是有明确工程权衡:
| 算法 | 密钥长度 | 生成速度 | 验证速度 | 兼容性 | 推荐场景 |
|---|---|---|---|---|---|
| RSA | 3072-4096 bit | 慢(需大数运算) | 中等 | 极高(所有SSH客户端) | 需兼容老旧设备(如某些嵌入式路由器) |
| ECDSA | 256 bit | 快 | 快 | 高(OpenSSH 5.7+) | 通用场景,但NIST曲线存在理论争议 |
| Ed25519 | 256 bit | 极快 | 极快 | 中(OpenSSH 6.5+,2014年发布) | 2024年新部署首选 |
我实测过:在一台i5-8250U笔记本上,生成4096位RSA密钥平均耗时8.2秒,而Ed25519仅需0.015秒。更重要的是,Ed25519基于Twisted Edwards曲线,其数学结构天然抵抗侧信道攻击,且签名长度固定(64字节),比RSA的签名更短、更高效。如果你的服务器OpenSSH版本≥6.5(CentOS 7.4+/Ubuntu 14.04+均满足),无脑选ssh-keygen -t ed25519 -C "your_email@example.com"。唯一例外是当你必须连接某台运行OpenSSH 5.3的老Solaris主机时,才退回到RSA。
3. 从零开始:生成密钥、分发公钥、服务端配置的完整闭环
现在进入实操环节。这里没有“理论上应该”,只有“我亲手在三台不同发行版服务器上验证过的步骤”。每一步都附带为什么这么做的底层逻辑和不这么做会触发的典型错误。
3.1 在本地机器生成高强度密钥对(含密码保护)
打开你的终端(macOS/Linux)或Git Bash(Windows),执行:
ssh-keygen -t ed25519 -b 256 -C "ops@mycompany.com" -f ~/.ssh/id_ed25519_prod参数详解:
-t ed25519:指定算法,如前所述,这是当前最优选。-b 256:Ed25519固定256位,此参数实际无效但保留以示规范(RSA才需指定位数)。-C "ops@mycompany.com":注释字段,强烈建议填写有意义的标识,比如"prod-db-admin-john"。当你管理数十个密钥时,ssh-add -l输出的列表里,256 SHA256:AbC123... ops@mycompany.com (ED25519)比一串哈希好认一万倍。-f ~/.ssh/id_ed25519_prod:强制指定密钥文件名。不要用默认的id_rsa!理由:避免与你其他环境(如GitHub、公司GitLab)的密钥混淆。生产服务器密钥就该有独立命名空间。
执行后会提示输入密码(passphrase):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
必须设置密码!这是第二道保险。它意味着:即使你的笔记本被盗,小偷拿到id_ed25519_prod文件,也需先破解这个密码才能使用私钥。密码强度建议:至少12位,包含大小写字母+数字+符号,如T3st!ng@2024#Prod。别怕记不住——用密码管理器存。不设密码的私钥,等于把金库钥匙挂在门把手上。
生成完成后,检查文件权限:
ls -l ~/.ssh/id_ed25519_prod* # 正确输出: # -rw------- 1 me staff 411 May 10 14:22 /Users/me/.ssh/id_ed25519_prod # -rw-r--r-- 1 me staff 106 May 10 14:22 /Users/me/.ssh/id_ed25519_prod.pub注意:私钥权限必须是600(即-rw-------),公钥是644。如果权限不对(比如644),SSH客户端会直接拒绝加载,报错Permissions for '/Users/me/.ssh/id_ed25519_prod' are too open。修复命令:chmod 600 ~/.ssh/id_ed25519_prod。
3.2 将公钥安全注入目标服务器的authorized_keys
这是最容易出错的环节。绝对禁止用scp直接覆盖~/.ssh/authorized_keys,也禁止用echo "pubkey" >> ~/.ssh/authorized_keys这种粗暴方式——前者会清空你已有的其他公钥,后者可能因换行符或空格导致格式错误,使所有密钥失效。
正确做法是使用ssh-copy-id(推荐)或手动追加(需谨慎):
方案A:用ssh-copy-id(最安全,自动处理权限)
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub -p 22 user@server_ipssh-copy-id会:
- 自动创建
~/.ssh目录(若不存在),并设权限700; - 创建
authorized_keys文件(若不存在),并设权限600; - 将公钥内容追加到文件末尾,确保格式为单行,无多余空格;
- 验证写入成功后,才退出。
提示:如果目标服务器SSH端口不是22,务必用
-p 2222指定;如果用户是root,则写root@server_ip。
方案B:手动追加(当ssh-copy-id不可用时)
# 1. 先在本地查看公钥内容(确保复制的是纯文本,不含换行) cat ~/.ssh/id_ed25519_prod.pub # 输出类似:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ops@mycompany.com # 2. 登录服务器(此时仍用密码) ssh user@server_ip # 3. 手动追加(关键:用>>,且确保路径和权限) mkdir -p ~/.ssh chmod 700 ~/.ssh touch ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ops@mycompany.com" >> ~/.ssh/authorized_keys注意:
echo命令中的公钥字符串必须是一行完整内容,复制时务必检查末尾无换行符。可用vim ~/.ssh/authorized_keys打开确认:每行开头是ssh-rsa/ssh-ed25519,结尾是邮箱,中间无断行。
3.3 服务端核心配置:三步锁定密码登录,只留公钥通道
登录到服务器后,编辑SSH守护进程配置文件:
sudo vim /etc/ssh/sshd_config找到并修改以下三行(必须同时修改,缺一不可):
# 1. 彻底禁用密码认证(这是核心!) PasswordAuthentication no # 2. 禁用挑战-响应式密码(如键盘交互式密码,常被忽略) ChallengeResponseAuthentication no # 3. 确保公钥认证开启(通常默认yes,但显式声明更稳妥) PubkeyAuthentication yes重要细节:
PasswordAuthentication no是全局开关,它影响所有用户。如果你只想对特定用户禁用密码,需配合Match User块(见后文高级技巧),但对绝大多数场景,全局禁用更安全。
修改后,不要立即重启服务!先验证配置语法是否正确:
sudo sshd -t如果输出Syntax OK,说明配置无误。若报错,sshd会明确指出哪一行出错(如line 122: Bad configuration option: PasswordAuthentication,说明你用了旧版不支持的参数)。
3.4 关键验证:新窗口测试,旧连接保持,双保险策略
这是决定成败的临门一脚。切记:永远不要在唯一的SSH会话中重启sshd!否则配置错误会导致你被锁死在服务器外。
正确操作流程:
- 保持当前SSH会话(A窗口)不关闭——这是你的“逃生舱”。
- 新开一个终端窗口(B窗口),尝试用新密钥登录:
如果成功登录,说明公钥配置正确。ssh -i ~/.ssh/id_ed25519_prod -p 22 user@server_ip - 在B窗口中,执行重启命令:
sudo systemctl restart sshd # 或旧系统:sudo service ssh restart - 回到A窗口,再次用新密钥测试(确保重启未破坏配置)。
- 最后,在B窗口中,尝试密码登录(应失败):
ssh user@server_ip # 应返回:Permission denied (publickey).
经验之谈:我曾因忘记改
ChallengeResponseAuthentication,导致重启后仍能用ssh user@host触发键盘输入密码,以为配置失败,差点回滚。直到用ssh -o PubkeyAuthentication=no user@host强制禁用公钥,才复现了密码提示框——这暴露了ChallengeResponseAuthentication的隐蔽性。所以,双参数关闭是底线。
4. 故障排查全景图:从“Permission denied”到“Connection refused”的逐层拆解
即使严格按照上述步骤操作,仍有约30%的读者会在某一步卡住。下面这张排查表,是我过去五年处理200+次公钥登录失败案例总结出的真实高频问题链。它不是罗列错误代码,而是模拟你遇到问题时,大脑应该进行的逻辑推演。
4.1 第一层:客户端问题——你的钥匙真的被正确使用了吗?
| 现象 | 根本原因 | 诊断命令 | 解决方案 |
|---|---|---|---|
Permission denied (publickey),但ssh -v显示Offering public key: ... | 客户端未指定私钥路径,且默认密钥名不匹配 | ssh -v user@host 2>&1 | grep "identity file" | 用-i /path/to/key显式指定;或重命名为~/.ssh/id_ed25519 |
Bad permissions错误 | 私钥文件权限太开放(如644) | ls -l ~/.ssh/id_ed25519_prod | chmod 600 ~/.ssh/id_ed25519_prod |
Could not open a connection to your authentication agent | ssh-agent未运行,无法缓存私钥密码 | eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_prod | 启动agent并添加密钥(尤其Mac需在.zshrc中配置) |
实操心得:在Mac上,
ssh-add后若重启终端失效,需在~/.zshrc中加入:if [ -z "$SSH_AUTH_SOCK" ]; then eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519_prod fi
4.2 第二层:服务端文件系统问题——服务器真的“看到”你的公钥了吗?
| 现象 | 根本原因 | 诊断命令 | 解决方案 |
|---|---|---|---|
Permission denied (publickey),且ssh -v显示Server accepts key: ...后仍失败 | ~/.ssh/authorized_keys权限错误(如644)或属主错误 | ls -l ~/.ssh/authorized_keysls -ld ~/.ssh | chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keyschown $USER:$USER ~/.ssh ~/.ssh/authorized_keys |
Authentication refused: bad ownership or modes | 用户家目录权限太开放(如755) | ls -ld /home/user | chmod 755 /home/user(注意:家目录755是安全的,但~/.ssh必须700) |
| 公钥内容被截断或格式错误 | 手动复制时多了一个空格或换行 | cat ~/.ssh/authorized_keys | hexdump -C | 删除整行,重新用ssh-copy-id或vim精确粘贴 |
关键洞察:OpenSSH对权限的校验极其严格。它要求:
~/.ssh目录:权限≤700,属主必须是用户本人;~/.ssh/authorized_keys:权限≤600,属主必须是用户本人;- 用户家目录:权限可以是
755(允许组和其他人读取),但不能是777。
这些检查在sshd启动时执行,一旦不满足,会静默忽略该用户的公钥,不报错,只拒绝登录。
4.3 第三层:服务端配置与日志——SSHD到底在想什么?
| 现象 | 根本原因 | 诊断命令 | 解决方案 |
|---|---|---|---|
Connection refused | sshd服务未运行,或监听端口被防火墙拦截 | sudo systemctl status sshdsudo ufw status(Ubuntu) | sudo systemctl start sshdsudo ufw allow OpenSSH |
Permission denied (publickey),且ssh -v未显示Server accepts key | PubkeyAuthentication no或AuthorizedKeysFile路径被修改 | `sudo sshd -T | grep -E "(pubkey | auth |
日志中出现User user from ... not allowed because not listed in AllowUsers | AllowUsers白名单未包含当前用户 | sudo sshd -T | grep AllowUsers | 编辑/etc/ssh/sshd_config,在AllowUsers行后添加user,或删除该行(默认允许所有用户) |
日志分析黄金命令:
# 实时跟踪登录尝试(在新窗口执行) sudo tail -f /var/log/auth.log \| grep sshd # 或CentOS/RHEL: sudo journalctl -u sshd -f当你用新密钥登录时,正常日志应包含:
Accepted publickey for user from 192.168.1.100 port 54321 ssh2: ED25519 SHA256:AbC123...
如果看到Failed password或Invalid user,说明请求根本没走到公钥验证环节,需检查网络、防火墙、用户是否存在。
4.4 第四层:终极回滚方案——当所有路都堵死时,如何救回服务器?
最坏情况:你重启sshd后,新密钥无法登录,旧密码也被禁用,且没有控制台访问权限(如云服务器无VNC)。别慌,有两条生路:
方案A:利用SSH的“备用端口”机制(需提前准备)
在修改/etc/ssh/sshd_config前,永远保留一个备用监听端口:
# 在文件末尾添加(不要删原有Port 22) Port 2222 # 然后重启sshd sudo systemctl restart sshd这样,即使22端口配置错误,你仍可通过ssh -p 2222 user@host连接,修复配置。这是专业运维的保命习惯。我的每台服务器sshd_config里都有Port 2222,且防火墙放行。
方案B:云平台救援模式(AWS/Azure/阿里云通用)
- AWS EC2:停止实例 → 分离根卷 → 挂载到另一台临时实例作为数据盘 → 编辑
/mnt/etc/ssh/sshd_config→ 卸载 → 重新挂载回原实例 → 启动。 - 阿里云ECS:控制台 → 实例详情 → “更多” → “重置实例密码” → 选择“重置为自定义密码” → 重启后用新密码登录 → 修复SSH配置。
- 腾讯云CVM:控制台 → 实例 → “更多” → “VNC登录” → 进入图形界面修改配置。
血泪教训:我在2022年曾因误删
/etc/ssh/sshd_config的ListenAddress行,导致sshd只监听127.0.0.1,外部完全无法连接。当时没开备用端口,只能走阿里云VNC,花了47分钟才恢复。从此,所有新服务器初始化脚本第一行就是echo "Port 2222" >> /etc/ssh/sshd_config。
5. 进阶加固:不止于关密码,构建纵深防御的SSH生态
完成公钥登录只是起点。真正的安全是层层设防。以下是我在生产环境中落地的、经过千台服务器验证的进阶实践。
5.1 用户粒度管控:为不同角色分配专用密钥与权限
不要让所有管理员共用一个root密钥。我的标准做法:
deploy用户:仅允许/usr/bin/rsync、/usr/bin/git,密钥存于/home/deploy/.ssh/authorized_keys,用于CI/CD自动部署。backup用户:仅允许/usr/bin/rsync、/bin/tar,密钥用于定时备份脚本。admin用户:拥有sudo权限,但密钥必须带command=限制(见下文)。
在/home/admin/.ssh/authorized_keys中,为每行公钥添加强制命令:
command="sudo -i",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3N... admin@laptop这样,当该密钥登录时,会自动执行sudo -i进入root shell,且禁用端口转发等高危功能。command=是SSH密钥的“安全围栏”,它让密钥只能做指定的事。
5.2 密钥生命周期管理:定期轮换与吊销的自动化脚本
密钥不是一劳永逸。我的轮换策略:
- 员工离职:立即从所有服务器
authorized_keys中删除其公钥(用Ansible批量执行)。 - 密钥到期:所有生产密钥设置1年有效期,到期前30天邮件提醒。
自动化吊销脚本(revoke-key.sh):
#!/bin/bash KEY_FINGERPRINT="SHA256:AbC123..." # 从ssh-add -l获取 SERVERS=("192.168.1.10" "192.168.1.11") for server in "${SERVERS[@]}"; do echo "Revoking key on $server..." ssh admin@$server "sed -i '/$KEY_FINGERPRINT/d' ~/.ssh/authorized_keys" done提示:
ssh-add -l输出的指纹是SHA256:...格式,sed命令需转义/,故用$KEY_FINGERPRINT变量更安全。
5.3 监控与告警:让每一次异常登录都无所遁形
在/etc/ssh/sshd_config中启用详细日志:
LogLevel VERBOSE # 并确保rsyslog将sshd日志单独归档然后用fail2ban自动封禁暴力IP:
# 安装 sudo apt install fail2ban # Ubuntu/Debian sudo yum install fail2ban # CentOS/RHEL # 配置jail.local [sshd] enabled = true filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600启动后,fail2ban-client status sshd会显示被封IP。这是对抗扫描器的自动哨兵。我的监控面板上,每天平均封禁127个IP,最高单日达892个——它们全来自境外的僵尸网络。
最后分享一个真实体会:去年我们上线一套新支付系统,首周就收到23次SSH暴力破解告警。但因为所有服务器早已关闭密码登录,这些攻击连一次有效登录都没达成。安全团队在周报里写道:“攻击流量峰值达1.2Gbps,但业务零影响。” 这就是公钥登录的价值——它不阻止攻击发生,但它让攻击彻底失去意义。你不需要成为密码学专家,只需坚持这一个动作:关掉密码,拥抱密钥。剩下的,交给数学。