做运维这几年,要问我哪个技能用得最多、最值得早点吃透,我会毫不犹豫说:SSH免密登录。它不光是省去输密码那点事,更是后面批量管理服务器、写自动化脚本、配置Ansible、同步文件、走Git SSH协议的基础中的基础。前期把免密配置好,后期你运维的效率会直接上一个台阶;反过来,如果哪台机器密钥没配好,“明明密钥放了为什么还是连不上”这种问题,能卡住你一个下午。这篇我就按自己实际操作的顺序,把SSH免密登录的原理、配置过程、批量操作、故障排查和安全加固完整过一遍,新手可以照着一步步来,老手可以直接跳到故障排查和批量配置部分捡漏。
说明一下适用范围:这里讲的命令在Linux和macOS终端、以及Windows 10以上的OpenSSH客户端里都能跑通,服务端以OpenSSH为主,版本在7.0以上基本都适用。整篇没有用那些“企业级神秘技巧”,都是日常一台台机器摸出来的经验。
1. SSH免密登录的原理,一次讲透
1.1 公钥和私钥到底是怎么配合工作的
很多人第一次配免密时容易误会,以为“免密”就是不用管密码了。其实不是,SSH免密登录用的是非对称加密里的“签名验证”机制,跟单纯消除密码完全是两码事。
客户端本地保存的是私钥,服务器端保存的是你的公钥。登录时,服务器会生成一串随机数据(challenge)发给客户端,客户端用自己的私钥对这个随机数据进行签名,再把签名结果返回给服务器;服务器拿到签名后,用之前存在authorized_keys里的公钥去验证,验证通过就确实相信你是私钥的合法持有者,直接放行。整个过程私钥始终没有离开客户端,也不会在网络上传密码,所以它比输密码更安全。
要类比的话,公钥像一把挂锁,私钥是唯一能打开这把锁的钥匙。你把挂锁(公钥)挂在服务器上,之后拿着钥匙(私钥)来就能开。公钥被人抄走了没关系,因为别人没有钥匙,照样进不去。这也解释了一个常见问题:为什么公钥文件可以到处发,而私钥文件必须像银行卡密码一样保护得严严实实。
从认证流程上看,SSH连接大概分这么几步:客户端发起连接;服务器发送自己的主机公钥(识别服务器身份);双方协商加密算法和会话密钥;客户端向服务器提供自己私钥对应的公钥信息;服务器从authorized_keys里找到匹配项;服务器发challenge;客户端签名返回;服务器验证签名通过,登录成功。运行ssh -vvv时,你能看到“Offering public key”和“Accepted publickey”这两条关键日志,它们分别对应签名前和验证通过后的状态。
1.2 免密登录解决的痛点是什么
早期管理服务器就是输密码,机器数量少还好说,机器一多问题就全冒出来了:密码记不住、记混、临场输错,写进脚本里容易泄露,而且批量执行命令时每台都要手动输一次,效率非常低。
免密登录带来的实际改变是很具体的:
- 批量执行命令:for ip in $(cat list); do ssh $ip "uptime"; done 这种循环能直接跑。
- 自动化脚本同步文件:rsync -avz 配合免密,定时任务里同步日志、备份数据根本不担心卡在密码输入上。
- 配置管理工具落地:Ansible、SaltStack这类工具在被管理机上跑任务,前提就是控制机能免密登录。
- Git走SSH协议:GitHub、GitLab上配置SSH公钥后,git clone、git push都不需要反复输账号密码。
说白了,免密登录是自动化运维的“第一个台阶”,这个台阶不搭好,后面全是空的。
1.3 密钥登录的边界与其他免密方式
SSH免密登录通常指密钥认证,但它不是唯一的免密码方案。在大规模集群场景里,还有用SSH CA统一签发和吊销证书的证书登录方式;在域环境下,也有基于GSSAPI的Kerberos认证。但这两种的部署和维护成本都比较高,对个人和中小团队来说,密钥认证已经能解决绝大部分实际问题,配置复杂度也最低。
我现在自己维护大概几十台机器,全部用密钥认证,坚持了几年,没出过问题。所以别再纠结要不要上更复杂的东西,先把密钥认证玩明白,等真有大规模集群需求再考虑证书体系也不迟。
2. 从零开始配置SSH免密登录
真正配置的时候,我一般分三步走:生成密钥对、推送公钥、验证登录。每步都有值得注意的细节,别直接复制完命令就以为完事了。
2.1 生成密钥对:算法选择与参数说明
生成密钥对的命令很简单,但算法我建议直接用Ed25519:
ssh-keygen -t ed25519 -a 100 -C "your_comment"参数解释一下:
- -t指定算法。ed25519基于Curve25519曲线,安全强度高,密钥短(公钥就几十字节),生成和验证速度快,是目前主流推荐。
- -a是KDF(密钥派生函数)的迭代次数,默认值通常较小,手动调高到100能让暴力破解私钥口令更困难。
- -C只是注释,会写到公钥文件末尾,建议写成机器名加用途,比如ops-vm-01-manage-key,方便日后认领。
执行后终端会问保存路径,默认是~/.ssh/id_ed25519,一般直接回车就行。接着会问要不要设置passphrase,也就是给私钥再加一层口令保护。
我的建议很直接:纯内网自动化场景可以留空,因为自动化任务需要无交互;跑公网服务器或者机器上有敏感数据,建议设置passphrase,然后配合ssh-agent只输入一次,既安全又不用每次敲口令。
如果你的远程服务器系统比较老,比如CentOS 6或者更旧的OpenSSH版本不支持ed25519,就退回RSA:
ssh-keygen -t rsa -b 4096生成完,~/.ssh目录下会多出两个文件:
- 私钥id_ed25519,权限必须600,除了当前用户任何人都不能读。
- 公钥id_ed25519.pub,权限644即可,这个文件内容可以发给任何人、贴到任何服务器上。
2.2 用ssh-copy-id一键推送公钥
生成完密钥,下一步就是把公钥推送到目标服务器的authorized_keys文件里。最稳妥的工具是ssh-copy-id,用法:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip这个工具会自动完成三件事:连上服务器、把公钥内容附加到~/.ssh/authorized_keys、顺手修复目录和文件权限。首次连接会让你输入目标服务器用户的密码,之后连接就不用了。
ssh-copy-id在macOS和绝大多数Linux发行版里自带。如果你用的系统没有这个命令,比如某些精简版Windows的OpenSSH环境,就手动操作,下面一节会说。
如果在推送时用了非默认的端口,命令要加-P参数(注意是大写P):
ssh-copy-id -i ~/.ssh/id_ed25519.pub -P 2222 user@server_ip2.3 手动配置authorized_keys的标准流程
有些环境不允许你用ssh-copy-id,这时手动配置也不复杂。在目标服务器上,切到你要登录的那个用户,执行:
mkdir -p ~/.ssh chmod 700 ~/.ssh touch ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys echo "公钥内容" >> ~/.ssh/authorized_keys注意,这组操作一定要用你要免密登录的那个用户身份来执行。比如你想免密登录root,就在root用户下操作;想登录普通用户,就切到普通用户下操作。很多排查了半天才发现问题出在“公钥放错用户家目录”上。
几个细节容易踩坑:
- authorized_keys里每行放一个公钥,有多台机器就依次换行追加。
- 从Windows复制公钥内容时,容易带上回车符\r,导致服务器端解析失败,后面会在故障排查里细说。
- 手动配置完,建议用tail -n 2 ~/.ssh/authorized_keys确认内容没有错行、没有串行。
2.4 Windows客户端和VSCode场景的配置要点
Windows 10 1809以后的系统自带了OpenSSH客户端,PowerShell里直接能用ssh命令。它的用户主目录是C:\Users\用户名.ssh,操作逻辑和Linux一致,只不过有些命令的表现会有差异。
我平时在Windows上最常用的其实是Git Bash,里面各种Linux命令都齐,ssh-copy-id也能正常使用。如果你只装OpenSSH客户端,没有ssh-copy-id,手动配置就好。
另外一个高频场景是VSCode的Remote-SSH插件。这个插件连接远程服务器时一样走密钥认证,只要本地~/.ssh下的私钥配置好了,VSCode打开远程目录就不会提示输密码。有一个容易混淆的点:VSCode里提示“此扩展在此工作区中被禁用,因为其被定义为在远程扩展主机中运行”,这是扩展归属问题,跟免密配置没有关系,别把两者扯到一起。
Windows下如果偶发密钥权限问题,可以通过icacls修复:
icacls "$env:USERPROFILE\.ssh\*" /inheritance:r /grant:r "$env:USERNAME:(OI)(CI)F"这个命令的作用是去掉继承的宽松权限,把.ssh目录下的文件权限收拢给当前用户,避免OpenSSH for Windows因为权限太开放而拒绝使用密钥。
3. 多台服务器批量配置与自检
机器少的时候,手动推一遍没问题,机器一多就要讲究批量方法了。我自己维护的服务器从小几十台开始,就摸索出了一套不算优雅但很稳的批量流程。
3.1 循环批量推送公钥
先把需要配置的IP地址或主机名写进一个文件,比如servers.txt,每行一个。然后写个简单的for循环:
for ip in $(cat servers.txt); do echo "==> $ip" ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@$ip done这个方式虽然每台机器第一次还是要输一次密码,但好处是思路清楚、不会漏机器。如果不想一台台输密码,可以用expect脚本配合,但expect脚本里需要写密码,我一般只在一键初始化的时候临时用,不建议长期保存这种带密码的脚本。
另一种思路是先手动配好第一台“跳板机”,然后从跳板机继续向外推送。这种方式在跨网段环境里经常用,但要注意跳板机本身的安全性,别被攻破后连累整个内网。
3.2 避免首次确认提示的自动化技巧
第一次ssh连接某个IP时,客户端会问:Are you sure you want to continue connecting? 这个确认是为了防止中间人攻击。在自动化批量操作时,这个交互提示会很碍事,可以临时用参数跳过去:
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null user@$ip "hostname"这里StrictHostKeyChecking=no跳过主机指纹确认,UserKnownHostsFile=/dev/null表示不把指纹写入known_hosts文件。要注意,这种做法只适合临时测试、一次性初始化等场景,不建议日常长期开启,否则一旦有机器被冒充,客户端根本发现不了。
更稳妥的做法是:在批量操作前,先把目标机器的指纹批量写入known_hosts,或者把StrictHostKeyChecking设回yes,让流程在第一次连接时进行指纹校验。
3.3 配置完成后的自检清单
批量推完公钥,别急着跑自动化。我习惯先做一轮快速自检,检查项包括:
- 能免密登录:ssh user@ip直接进去,不需要密码。
- known_hosts里没有异常告警。
- 私钥文件权限正确。
- 目标用户的.ssh目录属主是那个用户本身。
批量自检可以这样一行搞定:
for ip in $(cat servers.txt); do timeout 5 ssh -o BatchMode=yes -o ConnectTimeout=3 user@$ip "hostname" && echo "$ip OK" || echo "$ip FAIL" doneBatchMode=yes很关键,它表示禁止交互式输入密码。如果某台机器免密没生效,命令会立刻失败,而不是卡在那里等你输密码。用这个参数做批量巡检,一台机器3秒超时,几十台机器一分钟内就能筛出问题机器。
4. 故障排查实录:常见问题定位
配置免密登录这块,我踩过的坑几乎都可以写本书了。下面按“出问题概率从高到低”的顺序整理,方便你照着查。
4.1 权限问题:最常见也最容易忽略
最大的概率坑是权限问题。OpenSSH对文件权限非常敏感,权限不对宁可拒绝公钥认证,也不会冒险让你登录。
先在本机看私钥权限:
ls -l ~/.ssh/私钥应该是600,公钥644。然后在服务器上看对应用户下的目录权限:
ls -ld /home/user ls -ld /home/user/.ssh ls -l /home/user/.ssh/authorized_keys要求很严格:
- 用户主目录不能太开放,最好755或700,不能777,也不能被组或其他用户可写。
- ~/.ssh目录必须是700。
- ~/.ssh/authorized_keys必须是600或400。
- 文件归属必须是目标用户。
遇到chmod 777的,马上改回来:
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys很多新手问“为什么我把authorized_keys改成了777反而连不上”,正是因为SSH觉得“这么开放的文件可能被篡改过”,直接不信任它。
4.2 authorized_keys内容与属主细节
权限没问题后,下一个检查点是authorized_keys文件里的内容。
先确认公钥是否真的在文件里:
grep -F "公钥开头的几个字符" ~/.ssh/authorized_keys再检查有没有隐藏的回车符\r,尤其是从Windows复制过来的公钥,末尾经常带着\r,把一行拆成两行,导致解析失败。
用cat -A看一下行尾:
cat -A ~/.ssh/authorized_keys | tail如果行尾出现^M,就说明有\r,用sed清洗一下:
sed -i 's/\r$//' ~/.ssh/authorized_keys另外,多个公钥之间如果没有换行,也会导致全部失效。authorized_keys的标准格式是每行一个公钥,开头是ssh-ed25519或ssh-rsa这种算法标识。手动粘贴的时候一定要确认换行。
还有一个坑是属主。比如你用root权限把公钥写到了普通用户家目录,但文件属主还是root,普通用户登录时OpenSSH会认为这个文件不可信,拒绝使用。解决办法:
chown user:user ~/.ssh/authorized_keys ~/.ssh4.3 sshd_config配置项排查
如果文件和权限全部正常,仍然连不上,问题很可能出在服务器端sshd_config配置上。
登录服务器后查看这几个关键项:
PubkeyAuthentication yes RSAAuthentication yes AuthorizedKeysFile .ssh/authorized_keys PasswordAuthentication yes老版本OpenSSH还需要RSAAuthentication,新版本一般只关注PubkeyAuthentication。AuthorizedKeysFile要指向正确的路径,默认是用户家目录下的.ssh/authorized_keys。
修改完配置后,先用语法检查再重启服务:
sudo sshd -t sudo systemctl restart sshd还有一种容易被忽视的情况:sshd_config里的Match User或Match Group条件块覆盖了全局配置,导致某些用户或用户组被强制禁用公钥登录。如果全局配置看起来没问题,一定往下看看Match段。
4.4 SELinux、防火墙与网络层干扰
RHEL、CentOS、Fedora这类带SELinux的系统,就算把权限和配置全部改对,也可能因为SELinux上下文不对而拒绝读取authorized_keys。这时候先执行:
restorecon -R -v ~/.ssh如果还不行,去/var/log/audit/audit.log里看有没有selinux阻断记录:
ausearch -m AVC -ts recent | grep sshd网络层也不能忽略。先确认22端口通不通:
nc -vz server_ip 22如果端口不通,表现为“连接超时”而不是“密码错误”,这是和认证失败最大的区别。防火墙规则拦了的话,需要先放行22端口或者你的自定义SSH端口。
4.5 客户端与Windows环境隐蔽问题
排查完服务器还要回头看客户端。如果客户端有多个私钥,或者私钥不在默认路径,需要用-i参数指定:
ssh -i ~/.ssh/id_ed25519 -v user@server_ip调试级别-v、-vv、-vvv可以逐级提高信息量。我常用-vvv,能直接看到当前正在尝试哪种认证方式、是否读取了私钥、服务器返回了什么结果。
Windows还有几个容易踩的隐蔽点:
- VSCode Remote-SSH扩展提示“此扩展在此工作区中被禁用,因为其被定义为在远程扩展主机中运行”,跟免密配置无关,但经常会一起冒出来,别混为一谈。
- OpenSSH Authentication Agent服务没启动时,使用带口令的私钥会每次都要求输入passphrase。这时候去Windows服务里把OpenSSH Authentication Agent设为自动并启动。
- Git Bash和PowerShell对~/.ssh的解析路径不同,别把公钥放错位置,导致两边互相找不到。
还有一个容易混淆的问题:ssh执行远程命令时如果连接断开,命令还会不会继续跑?答案是:通常不会。远程终端收到SIGHUP信号后命令会中断。想让命令不被中断,应该用nohup或者screen/tmux。这个跟免密配置无关,但排查问题的时候经常被一起问出来,这里顺手说清楚。
4.6 万能调试法:-vvv和日志配合
最后给一个排查万能套路,按这个顺序基本上能覆盖90%的问题:
- 客户端执行ssh -vvv user@server_ip,把输出贴到终端里。
- 观察有没有“Authentications that can continue: publickey”这类关键词。
- 观察有没有“Offering public key: ...ED25519...”。
- 服务器端另开终端,执行journalctl -u sshd -f,或者tail -f /var/log/secure(RHEL系)以及/var/log/auth.log(Debian系),实时看日志。
- 当看到“Accepted publickey”时,说明认证已经通过。
日志里几个关键关键词的含义:
- Failed password,表示密码阶段失败。
- Connection closed by authenticating user,出现频率极高,多半是权限问题或公钥不匹配。
- Accepted publickey,表示公钥登录成功。
只要把客户端-vvv输出和服务器端日志对照着看,问题基本都能定位到具体环节,而不是漫无目的地一项项猜。
5. 安全加固与日常维护
免密登录配置成功后,很多人的状态是“终于不用输密码了,收工”。但我建议你再花点时间做安全加固,否则免密配置可能成为一把双刃剑。
5.1 私钥保护与ssh-agent的正确用法
免密之后,私钥文件就成了你登录所有机器的“总钥匙”。哪天私钥泄露,攻击者等于拿到了你整个服务器矩阵的通行证。所以私钥文件要像一卡通一样对待:不随便拷到U盘、不上传公有仓库、不给其他用户任何权限。
如果生成了带passphrase的私钥,可以用ssh-agent来降低反复输入口令的负担:
eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519执行一次后,当前会话里再ssh连接就不会要求输入passphrase了。Git Bash和Linux下都可以这样用。Windows环境则需要确认OpenSSH Authentication Agent服务已经启动。
注意ssh-agent转发(AgentForwarding)建议只在临时需要时才开启。所谓转发,就是允许远程服务器“借用”你本地的agent去连接其他机器。一旦管理机被攻破,攻击者可能通过agent转发获取你的认证能力,去横向连接内网更多机器。默认配置里不要开ForwardAgent,确需使用的时候,用完立刻关掉。
5.2 禁用密码登录的操作顺序
免密配好之后,不少人会把密码登录直接关掉,这是安全加固的正确方向,但操作顺序一定要讲究。
我的建议顺序是:
- 确认至少有一台机器已经能稳定免密登录,再开始改配置。
- 修改sshd_config前先备份原文件。
- 修改后先执行sudo sshd -t做语法检查。
- 重启sshd服务,但保持当前连接不要断开。
- 新开一个终端测试免密登录,确认无误后再关掉之前的连接。
关键的加固配置:
PasswordAuthentication no ChallengeResponseAuthentication no PermitRootLogin prohibit-passwordPermitRootLogin prohibit-password表示允许root用密钥登录,但禁止密码登录,适合需要root直接管理的场景。如果完全不需要root远程登录,可以更严格地设成PermitRootLogin no。
修改后记得reload配置:
sudo systemctl reload sshd这里最大的禁忌就是盲目关掉密码登录后,才发现私钥不在手边或者私钥文件损坏,结果把自己锁在服务器外,只能通过机房或云控制台紧急救援入口处理。
5.3 密钥轮换与公钥清理
密钥不是配好就能一劳永逸的。我的维护习惯是:
- 每半年到一年轮换一次管理机密钥。
- 员工离职时,把他机器上的公钥从所有服务器的authorized_keys里移除。
- 定期检查所有authorized_keys,删掉不再使用的公钥。
批量清理命令可以这么写:
for ip in $(cat servers.txt); do ssh user@$ip "sed -i '/需要删除的公钥备注/d' ~/.ssh/authorized_keys" done轮换密钥的步骤,我坚持一个原则:先推新的,验证通过后,再删旧的。具体来说:
- 新生成一对密钥。
- 把新公钥推送到所有服务器。
- 用新私钥登录验证。
- 确认全部机器都能通过新私钥登录后,再删除本地旧私钥,并从服务器authorized_keys里移除旧公钥。
这个顺序能避免“新密钥还没推完,旧密钥已经被删掉”这种把自己锁在门外的尴尬。
6. 写在最后:几个实操体会
临时想到一个小技巧,排查时特别好用:把ssh-copy-id和-v参数结合,ssh-copy-id本身也支持调试输出。遇到反复查不明白的问题,不要对着一个命令反复试,直接开两个终端,一个盯着服务器日志,一个本地跑ssh -vvv,日志不会骗人,问题出现的环节一眼就能看出来。
我个人实际使用中还有一个习惯,就是每次生成密钥时,-C参数一定写成机器名加用途,比如ops-vm-01-manage-key。这样半年后你在授权文件里看到一堆公钥时,也能迅速认出是谁的、干什么用的,清理的时候不会误删。
SSH免密登录虽然基础,但正因为太基础,很多人跳过原理直接复制命令,出了故障就开始瞎猜。把这套逻辑和排查路径弄明白,后面无论是批量运维、远程开发还是Git操作,都会顺畅得多。希望这篇经验能帮你少走一些重复弯路。