两年前我第一次给几十台服务器配免密登录的时候,以为就是把公钥扔到 authorized_keys 里完事,结果第二天一大半机器依然提示输密码。更尴尬的是,有一台机器明明已经能免密了,重启之后又失灵,查了半天发现是 /root 目录权限被某次脚本改成了 777。这篇文章我把 SSH 免密登录从原理到实操、从常见翻车点到批量管理方案完整梳理了一遍,所有命令都是我实际验证过的,直接复制就能用。
1. 免密登录到底解决什么问题:从"天天输密码"说起
很多刚开始用 SSH 的人以为免密登录只是省去输密码的麻烦,其实它的核心价值远不止"方便"这么简单。尤其是你负责的服务器数量超过五台、或者需要写脚本做自动化部署的时候,有没有配置免密,效率差距是数量级的。
1.1 密码登录的三大痛点
先说最常见的痛点。第一是密码记忆成本。生产环境的密码通常要求足够复杂,大小写字母加数字加特殊符号,12 位以上,这种密码人类根本记不住,最后只能存在密码管理器里,每次登录都要翻出来复制粘贴。第二是自动化脚本跑不起来。你写了一个批量更新配置的脚本,循环登录十台服务器,如果都靠密码,脚本就得在每台机器上停下来等你输密码,完全失去了自动化的意义。第三是安全风险。密码在网络传输中理论上存在被截获的可能,而且密码一旦泄露,攻击者就可以在你不知情的情况下登录服务器。
1.2 密钥认证的工作方式
免密登录解决这些问题的底层逻辑是非对称加密。我尽量用大白话解释:每台机器上会生成一对密钥——一个公钥和一个私钥。公钥可以公开,放在你要登录的目标服务器上;私钥是绝密的,只留在你自己的电脑上。
当你执行 ssh user@server 的时候,服务器会向你的客户端发送一个用你的公钥加密的挑战密文,你的客户端用私钥解密后返回给服务器,服务器确认你能解开这个密文,就认定你是合法的,直接放行。整个过程你的私钥从来没有通过网络传输,安全性远高于每次发送密码的方式。
一句话总结就是:密码是"你知道什么",密钥是"你拥有什么"。前者可以被猜、被截获、被撞库,后者只要你不把私钥文件复制给别人,基本没有泄露渠道。
1.3 适用场景与不适合的场景
免密登录适合绝大多数日常运维场景:个人开发机连服务器、跳板机转发、自动化部署脚本、Git 拉取代码、集群批量操作等。但有一个例外:高安全级别的生产环境,比如涉及金融核心系统、支付网关等要求强制密码 + 动态令牌的场景,通常不会开放免密登录,这是合规要求,不是技术做不到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 密钥对生成与公钥分发:一条命令背后的完整链路
理解了原理,实操就顺理成章了。免密登录的标准流程只有三步:生成密钥对、把公钥放到目标服务器、验证登录。但每一步都有细节,任何一个环节出错都会导致免密失败。
2.1 生成密钥对:选对算法和参数
以最常见的 Linux 或 macOS 开发机为例,打开终端执行:
bash复制ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519
生成过程中会提示你设置 passphrase(私钥口令),这里有个关键选择。如果你设了口令,那么每次使用私钥时都要求输入这个口令,表面上看起来和输密码没什么区别,但它保护的是你的私钥文件本身——即使你的私钥文件被偷走,没有口令也无法使用。如果你追求完全自动化、脚本免交互,可以不设口令直接回车跳过。
关于算法选择:ed25519 是目前综合安全性、速度和密钥长度最优的选择,生成的密钥文件只有 300 多字节,比传统的 RSA 短得多。如果你需要兼容非常老旧的系统(比如几年前的 CentOS 6 或某些老交换机),再用 rsa:
bash复制ssh-keygen -t rsa -b 4096 -C "your_email@example.com" -f ~/.ssh/id_rsa
-b 4096 指定密钥长度,老系统对 ed25519 支持不完整,RSA 4096 是通用性最保险的选择。
生成完成后,~/.ssh/ 目录下会出现两个文件:
id_ed25519(或id_rsa):私钥,权限必须是600id_ed25519.pub(或id_rsa.pub):公钥,权限是644,内容是一个长字符串,可以安全地分发
2.2 把公钥放到目标服务器:三种主流方式
方式一:ssh-copy-id(最推荐)
bash复制ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
该命令会以密码方式登录一次,自动把公钥追加到目标服务器的 ~/.ssh/authorized_keys 文件,并设置好相关权限。建议加 -i 指定公钥路径,避免某些系统默认使用错误的密钥。
方式二:手动追加
当目标机器不允许密码登录、或者 ssh-copy-id 不可用时(比如有的精简版系统没装这个工具),手动操作:
bash复制cat ~/.ssh/id_ed25519.pub | ssh user@server_ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
这条命令做了四件事:创建 .ssh 目录(如果不存在)、设置目录权限为 700、把公钥内容追加到 authorized_keys、设置文件权限为 600。每一步都不能少。
方式三:批量分发脚本
当你管理几十台机器时,手动跑 ssh-copy-id 也嫌慢,可以写一个简单的循环:
bash复制for host in 10.0.0.11 10.0.0.12 10.0.0.13; do
sshpass -p 'your_password' ssh-copy-id -o StrictHostKeyChecking=no root@$host
done
sshpass 可以自动输入密码(Debian/Ubuntu 用 apt install sshpass 安装,CentOS/RHEL 用 yum install sshpass),配合 StrictHostKeyChecking=no 跳过首次连接时的指纹确认,实现真正的一键批量分发。
注意:批量分发时会在命令行暴露密码,历史记录里能看到。如果公司有堡垒机或审计要求,建议只用在测试环境,生产环境慎用。
2.3 验证是否配置成功
配置完成后,直接执行:
bash复制ssh user@server_ip
如果一切正常,你会直接进入服务器的 shell,不再提示输入密码。如果仍然提示密码,别急,接着往下看,90% 的情况出在权限问题上。
3. 权限组合错了,免密直接失灵
我在开头提到的那个"重启后免密失效"的案例,根因就是权限。SSH 对密钥相关文件的权限有极其严格的检查,任何一个文件的权限过宽,出于安全考虑,sshd 会直接忽略你的密钥,然后回退到密码认证。
3.1 必须死记硬背的权限清单
涉及免密登录的核心文件权限如下:
| 对象 | 所在位置 | 建议权限 |
|---|---|---|
| 用户家目录 | /home/user 或 /root |
不能有组和其他用户的写权限(755 可以,750 也可以) |
~/.ssh 目录 |
~/.ssh |
700(只有自己可读写执行) |
authorized_keys 文件 |
~/.ssh/authorized_keys |
600(只有自己可读可写) |
| 私钥文件 | ~/.ssh/id_ed25519 |
600 |
| 公钥文件 | ~/.ssh/id_ed25519.pub |
644(可选的,安全性要求高的环境设成 600 也行) |
一条命令修正所有权限:
bash复制chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519
chmod 755 ~
注意最后一行,家目录的权限是很多人容易忽略的。如果家目录被其他用户可写,攻击者可以在你的家目录里放一些配置文件(比如 .bashrc),劫持你的登录会话,SSH 会认为这是严重的安全漏洞,直接拒绝信任。
3.2 为什么 SSH 对权限这么敏感
SSH 严格检查权限是为了防止所谓的 "会话劫持" 和 "密钥替换" 攻击。举个例子:如果 authorized_keys 的权限是 644,任何其他用户都能读它(这还好),但如果你的 .ssh 目录权限是 777,其他用户就可以往你的 authorized_keys 里追加他自己的公钥——这意味着他可以在你毫不知情的情况下获得这台机器的登录权限。
更严重的是,如果家目录权限过宽,其他用户可以替换你的 ~/.ssh/authorized_keys 文件,把你的公钥删掉换成他的,从而实现永久后门。所以 sshd 的权限检查不是故意给你添麻烦,而是抵御这类攻击的最后一道防线。
3.3 权限排查一句话脚本
如果你不确定哪里的权限有问题,在目标服务器上执行:
bash复制namei -l ~/.ssh/authorized_keys
这个命令会列出从根目录到文件每一级的权限,哪个目录权限异常能一目了然。另外还有一个快速判断方法:在登录时加上 -v 参数,如果输出中能看到 Offering public key 但认证失败,并且后面跟着 Authentication refused: bad ownership or modes 之类的提示,那基本就是权限问题。
4. 免密登录失效的完整排查链路:从 ssh -vvv 说起
权限问题是免密失败最常见的原因,但绝不是唯一原因。实际工作中,免密登录失效的情况五花八门,我总结了一套从客户端到服务端的完整排查链路,按照这套流程从头到尾走一遍,基本能定位 99% 的问题。
4.1 客户端侧排查:确认用的是不是正确的私钥
首先在客户端执行:
bash复制ssh -vvv user@server_ip
-vvv 会输出最详细的调试信息,信息量很大,但你要关注的是下面几个关键位置:
第一,看 debug1: Offering public key 之后跟的密钥路径。这里有 id_rsa_xxx、id_ed25519 等多个密钥,如果能看到:
code复制debug1: Offering public key: /home/user/.ssh/id_ed25519 ED25519 SHA256:xxxx
debug1: Server accepts key: /home/user/.ssh/id_ed25519 ED25519 SHA256:xxxx
说明客户端发出了正确的密钥,并且服务器接受了这个密钥,但后续认证仍然失败,问题大概率在服务端。
如果只看到:
code复制debug1: Skipping ssh-keygen: key /home/user/.ssh/id_ed25519 ...
说明客户端根本没找到密钥文件,或者权限不对(私钥必须是 600,如果设置了咦?让我检查一下格式——如果设了其他权限,SSH 会拒绝使用)。
第二,确认没有别的 config 文件把密钥路径指到了别处。很多人装了多套开发环境,.ssh/config 里可能给同一台服务器配置了不同的 IdentityFile,导致连接时用的不是你以为的那把密钥。
4.2 服务端侧排查:sshd 到底拒绝了什么
如果客户端调试信息显示密钥被提交了但认证失败,接下来到服务端看日志:
bash复制sudo tail -f /var/log/auth.log # Debian / Ubuntu
sudo tail -f /var/log/secure # CentOS / RHEL / Fedora
重新在客户端发起一次登录,观察服务端日志的输出。常见错误有以下几种:
情况一:权限错误
code复制Authentication refused: bad ownership or modes for file /root/.ssh/authorized_keys
这直接说明权限有问题,按前面第 3 章的权限清单修改即可。
情况二:密钥不匹配
code复制Failed publickey for root from 10.0.0.100 port 51234 ssh2: ED25519 SHA256:xxxx
如果客户端明明发出了公钥,但服务端日志没有 Accepted publickey,说明 authorized_keys 里的公钥内容和客户端持有的私钥不是一对。检查是不是往 authorized_keys 里粘贴公钥时弄错了。
情况三:公钥格式问题
有的工具(比如一些在线生成的密钥对)会把公钥换行,导致 authorized_keys 里的一行被截断,SSH 解析失败。确保 authorized_keys 的每一行是一条完整的公钥(以 ssh-ed25519、ssh-rsa 开头,以注释文字结束)。
4.3 sshd 配置文件里的开关:一个容易被忽略的选项
还有一个容易踩的坑是 /etc/ssh/sshd_config 中的配置。检查以下几项:
bash复制sudo grep -E "PubkeyAuthentication|AuthorizedKeysFile|PasswordAuthentication" /etc/ssh/sshd_config
正常的配置应该是:
code复制PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
如果 PubkeyAuthentication 被设为 no,无论你的密钥和权限多么正确,服务器也不会接受公钥认证。另外,有的系统出于安全加固要求,会把 PermitRootLogin 设为 prohibit-password,这表示 root 不允许密码登录但允许密钥登录——很多人在这一步配置反了,看到 root 无法登录就以为是免密的问题,结果把密钥登录也关了。
修改配置后需要重启 sshd 服务:
bash复制sudo systemctl restart sshd # systemd 系统
sudo service ssh restart # SysVinit 系统(有些旧版 Debian/Ubuntu)
4.4 完整排查链路速查表
为了方便以后快速排错,我整理了一张速查表:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 客户端提示没有可用密钥 | 客户端私钥不存在或权限不对 | ls -l ~/.ssh/,确认 id_* 存在且为 600 |
| 服务端拒绝公钥 | authorized_keys 没有对应公钥或权限异常 |
检查服务端权限和密钥内容是否匹配 |
| 服务端没有响应公钥请求 | PubkeyAuthentication no |
检查 sshd_config |
| 连接直接超时或被拒绝 | 防火墙或 hosts.allow/deny 限制 | telnet server_ip 22 测试端口连通性 |
| 能连但立即断开 | AllowUsers、AllowGroups 限制 |
查看服务端日志明确拒绝原因 |
| 密钥格式错误 | 复制公钥时换行或加了多余字符 | 对比公钥文件和客户端 .pub 内容是否完全一致 |
5. Windows 与 VS Code 远程开发的免密配置
很多开发者的日常工作平台是 Windows,上面那一套命令虽然也可以借助 WSL 或 Git Bash 使用,但原生的 Windows 环境有几个不同的点值得单独说清楚。尤其是热词里频繁出现的 VS Code Remote SSH,是现在远程开发最主流的方案之一。
5.1 Windows 上生成密钥的坑
Windows 10 1809 及以后版本自带 OpenSSH 客户端,你可以直接在 CMD 或 PowerShell 里执行:
powershell复制ssh-keygen -t ed25519 -C "your_email@example.com"
这个命令和 Linux 上完全一致,密钥默认生成在 C:\Users\你的用户名\.ssh\ 目录。但这里有个很常见的坑:如果你之前用过旧版 Git for Windows 自带的 OpenSSH,或 PuTTY 生成过密钥,系统环境变量里的 GIT_SSH 或用户目录下可能同时存在多套密钥。SSH 会按照默认文件名优先级去查找,有时候找到的不是你新生成的那一把。
解决办法是显式指定私钥路径。比如我在 Windows 上维护多套密钥(公司 Git、个人服务器、云主机各一把),会在用户目录下的 config 文件(没有就新建,路径 C:\Users\你的用户名\.ssh\config)里写清楚:
code复制Host myserver
HostName 10.0.0.47
User root
IdentityFile ~/.ssh/id_ed25519_company
PreferredAuthentications publickey
这样执行 ssh myserver 就会自动使用指定密钥,不会和其他密钥混淆。
5.2 VS Code Remote SSH 的免密配置流程
VS Code 的 Remote-SSH 插件本质上是调用本机 SSH 客户端建立连接,所以免密配置的核心和命令行完全一样。关键步骤是:
- 在 Windows 上生成密钥对(如果还没有)。
- 把公钥内容复制到远程服务器的
~/.ssh/authorized_keys。 - 在 VS Code 中按
F1,输入Remote-SSH: Connect to Host,填写user@server_ip。
此时如果命令行已经能免密登录,VS Code 也会直接连上,不会再让你输密码。
但有一个 VS Code 特有的問題需要注意:如果之前的 VS Code 连接是通过密码方式建立的,它会在远程服务器上的 ~/.vscode-server 目录存放扩展和会话数据。当免密配置完成后,如果扩展加载异常,建议先在 VS Code 里执行 Remote-SSH: Kill VS Code Server on Host,断开重连即可。
另外,报错信息里常见的:
code复制此扩展在此工作区中被禁用,因为其被定义为在远程扩展主机中运行
这不是连接问题,而是某些扩展被设计为只能在本地窗口运行,或只能在远程运行。在 VS Code 的扩展面板里可以看到每个扩展的安装位置标识,需要根据标识切换安装位置,和 SSH 免密没有关系。如果你搜到这个报错,不用去折腾密钥,直接去扩展设置里调整安装位置即可。
5.3 使用 config 文件管理多台主机
随着服务器增多,每次连接都输入 user@ip 很别扭。在 ~/.ssh/config 里定义好别名,连接体验会大幅提升。一个完整的配置示例:
code复制Host jump
HostName 192.168.1.10
User admin
Port 22022
IdentityFile ~/.ssh/id_ed25519_jump
Host target
HostName 10.0.0.80
User root
ProxyJump jump
IdentityFile ~/.ssh/id_ed25519_prod
这个配置的亮点是 ProxyJump:当你直接 SSH 到 target 时,会自动先经过 jump 这台跳板机。免密配置好之后,从本地到跳板机、从跳板机到目标机的两段认证都不需要输密码,体验非常顺滑。
6. 批量管理场景下的密钥策略与常见翻车点
当服务器数量超过一定规模,免密登录就从"一次性配置"变成了"需要体系化管理"的事情。很多人在这个阶段会踩到一些更隐蔽的坑,这里分享一下我自己的实践和教训。
6.1 不要所有机器共用一把密钥
接手过一个项目,运维同学图省事,把同一个公钥批量发到了几十台服务器上。表面上看管理简单了——一台机器加了新服务器,把公钥扔过去就能登录。但安全隐患很大:一旦这把私钥泄露(比如开发人员的笔记本失窃、或有人离职时带走了私钥),所有服务器全部失守,需要一台一台地更换密钥,灾难级别。
我现在的策略是按安全级别分密钥:
- 测试环境:所有测试服务器共用一把密钥,定期更换,丢了也不心疼
- 生产环境:每台服务器或每个业务组单独一把密钥,互不影响
- 堡垒机/跳板机:单独一把高安全等级的密钥,配合 passphrase 使用
配合 ~/.ssh/config 管理,虽然密钥数量多了,但连接时通过别名自动选择,并不会增加操作负担。
6.2 ssh-agent 与多密钥环境下的认证顺序
如果你有多个密钥,SSH 客户端默认的行为是逐个尝试私钥文件(按 ~/.ssh/ 目录下的文件名顺序),直到服务器接受其中一个。当密钥数量很多时,这种逐个尝试的方式会让登录变慢,而且有些服务器有"尝试次数限制",超过一定次数直接断连。
解决方案是明确指定密钥,或者使用 ssh-agent 缓存。以 macOS 为例(Linux 做法相同),在客户端执行:
bash复制eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_prod
ssh-add -l
ssh-add -l 会列出所有已加载到 agent 中的密钥。加入 agent 之后,SSH 连接时优先使用 agent 中关联的密钥,不需要逐个去盘文件系统。这对频繁连接大量服务器、尤其是通过跳板机中转的场景,性能提升非常明显。
6.3 免密登录配置后的安全加固
配置完免密后,很多人就不管了,但有几个安全加固点值得做。
限制 root 密钥登录的来源 IP
在 /etc/ssh/sshd_config 里加一行:
code复制AllowUsers root@10.0.0.0/24 ops@*
控制哪些用户、哪些来源 IP 可以使用 SSH 登录。这个配置是白名单逻辑,AllowUsers 里没写到的用户一律拒绝。
开启 fail2ban 防护
虽然免密登录本身对暴力破解攻击的抵抗力很强,但你无法确保每台机器都只开放密钥登录。有的服务器因为兼容性问题仍然开着密码认证,这时候建议安装 fail2ban:
bash复制sudo apt install fail2ban # Debian/Ubuntu
sudo yum install fail2ban # CentOS/RHEL
默认规则会监控 SSH 登录失败的日志,同一个 IP 连续失败 5 次会被封禁 10 分钟。对公网服务器来说是刚需,毕竟是个人服务器的话,日志里几乎每天都能看到来自世界各地的扫描尝试。
定期轮换密钥
密钥和密码一样,需要定期更换。我自己是每半年换一次生产环境密钥。轮换流程不复杂:本地生成新密钥,重新用 ssh-copy-id 推送到服务器,确认能登录后,把旧公钥从 authorized_keys 中删除,同时更新 ~/.ssh/config 中的 IdentityFile 路径。
6.4 内网穿透与 NAT 环境下免密的连接排障
热词里出现了 tailscale 相关的搜索,这里也多说一句。当你通过 Tailscale 这类组网工具(或内网穿透工具)访问位于内网的机器时,SSH 免密配置的原理和直连完全一样,唯一的差别是目标地址变成了虚拟私有 IP。常见的问题是:
- 通过 Tailscale 的 IP 连接时发现免密失败,但直连局域网 IP 就正常——排查一下是不是
~/.ssh/config里配置的HostName是局域网 IP,而 Tailscale 分配给机器的 IP 变了。 - 在
authorized_keys里限制了来源 IP(使用from=前缀),如果来源 IP 是 Tailscale 分配的虚拟 IP,可能会被规则拒绝。
bash复制# authorized_keys 中限制来源 IP 的公钥示例
from="10.0.0.0/8" ssh-ed25519 AAAA... user@host
这类限制虽然能提高安全性,但在组网、IP 变化频繁的场景下很容易误伤,建议先在无限制条件下跑通,再加限制规则。
6.5 一个批量部署 mkdir 权限的补充坑
最后分享一个我亲身踩过的批量部署坑。写脚本给新用户配置免密时,如果目标用户的 ~/.ssh 目录不存在,ssh-copy-id 会自动创建并设置权限,这个没有问题。但如果你是自己写脚本手动创建用户,然后从另一个用户执行 su 切换后创建密钥文件,目录的所有者可能会变成 root,导致目标用户无法正常使用。确保一切用目标用户身份执行,或者创建后强制修改所有者:
bash复制mkdir -p /home/newuser/.ssh
chown -R newuser:newuser /home/newuser/.ssh
chmod 700 /home/newuser/.ssh
chmod 600 /home/newuser/.ssh/authorized_keys
权限不对、所有者不对,免密登录就是会莫名失败,这类问题不看到 chown 根本想不到。
7. 最后分享两个我自己的排障习惯
这套流程跑下来,绝大多数免密登录问题都能解决,最后再分享两个我日常排障时的小习惯。
一个习惯是每次报错先看 -vvv 输出。很多人一遇到免密失败就直接去改服务端配置,但 -vvv 的输出通常能在五秒内告诉你问题在客户端还是服务端,省掉大量盲目试错。具体怎么看前面已经讲过了:重点看 Offering public key(客户端发出密钥)、Server accepts key(服务端接受了这个密钥)这两行,再结合服务端日志,问题范围能缩小到非常小。
另一个习惯是每次修改 sshd 配置前先备份。我吃过一次亏——改了 /etc/ssh/sshd_config 后忘记语法检查就重启服务,结果 sshd 直接启动失败,幸好人在机房里才没造成事故。正确做法是修改后用 sshd -t 做语法检查,通过后再重启:
bash复制sudo sshd -t
sudo systemctl restart sshd
养成这个习惯之后,基本再也没出现过配置错误导致服务挂掉的情况。SSH 免密登录本身不复杂,把原理吃透、把权限管好、把排查链路记熟,以后遇到任何相关问题都能从容应对。
