前两年接手一套多机环境做日常巡检,最消耗耐心的不是业务故障本身,而是每次登录都得老老实实敲一遍密码。机器一多,这个动作就会被无限放大:上午巡检二十台,下午又要批量同步配置,每台机器输一次密码,手指头都快磨出腱鞘炎。最惨的一次是连续输错三回,被服务器临时限制登录,当场傻眼。后来我把手头所有 Linux 机器统一配置成 SSH 免密登录,才真正体会到什么叫“一次配置,长期受益”。这篇内容不是网上那种干巴巴的三行命令,而是一套完整可复现的流程,覆盖密钥原理、权限细节、多机管理和报错排查。如果你也在为密码登录头疼,或者之前照着教程配完却不生效,这篇内容应该能帮上大忙。
1. 密码登录的隐形成本:哪些场景离不开免密
1.1 高频巡检、变更、回滚:登录动作本身就是时间黑洞
单人管理三五台机器时,密码登录的副作用还不明显。可一旦规模上来,每天要巡检日志、查磁盘空间、看进程状态,你会发现登录动作本身就是最大的开销。假设每台机器密码是十六位复杂口令,输入一次加上等待验证大约要八到十秒,二十台机器就是三分钟起步。听起来不多?但这只是理想情况。实际操作里经常发生输错、等待重试、切到正确的跳板机再登录一遍,真正的时间消耗往往是心理预期的两倍以上。
遇到变更窗口时更明显,比如凌晨做一次集群服务发布,需要逐台滚动重启,每台机器都要进控制台确认进程状态。没有免密的话,节奏会被密码输入打得支离破碎:刚输完一台的密码,又要去翻下一条凭据记录。反复掏密码、粘贴、回车,非常容易出错。免密登录配上脚本之后,一个 for 循环就能把几十台机器全部巡检一遍,日志集中拉回来慢慢看,效率根本不在一个量级。
1.2 自动化任务里,密码认证就是安全隐患
免密不是为了偷懒才存在的,真正的刚需在自动化。很多运维脚本、定时任务、日志采集、备份同步都会用到 scp、rsync 这类工具。如果全程依赖密码认证,脚本里就得携带明文密码,或者借助某些工具把密码作为参数传进去。这带来的问题很明显:密码会被 shell history 记录,进程列表里也可能短暂可见,日志审计时等于把凭据敞开了给人看。
密钥认证在自动化中的做法干净很多:只要指定私钥文件路径,或者让 ssh-agent 提供密钥,脚本本身不出现任何机密内容。即便私钥文件被人看到了,没有口令短语的文件也还不能直接用;要是设置了口令短语,还需要额外的解锁步骤。密码则完全没有这层隔离。从安全角度来看,自动化任务优先使用密钥认证不只是方便,更是一种底线要求。
1.3 多人协作场景:密钥让授权与回收变得可控
在一个多人共管的机器集群里,密码很容易失控。大家共用一个账号密码,离开的人有没有私下保存、改没改密码,你根本不知道。密钥认证配合独立账号,可以做到一人一钥:某人的公钥从 authorized_keys 里摘掉,他的访问权限即刻消失;没摘掉之前,他用自己的私钥就能完成登录,全程不需要暴露密码。
这种模式还方便做最小化授权。给不同人员分配不同账号,服务器上只放对应公钥,谁登录过、在哪个时间点登录,都能通过系统日志追踪。有些团队会把公钥纳入配置管理工具统一发布,新增一台机器时公钥自动同步过去,新机器即刻具备团队所有人的免密访问能力。这一层考虑真正让免密的价值超越了“省事”本身。
2. 客户端和服务端只认“证”不认人:密钥对校验的底层逻辑
2.1 一把锁与一把钥匙:公钥和私钥的角色定位
很多人把免密理解成“把公钥放到服务器,然后就不用密码了”,这没错,但容易让人误解机制。更准确的理解是:公钥像一把锁,私钥像一把钥匙。锁放在服务端,钥匙留在客户端。服务器不需要认识你的长相,它只看你能不能用手里的钥匙打开这把锁。
验证时,服务器生成一段随机的挑战数据,客户端用私钥对这个数据进行签名,服务器用自己保存的公钥验签。只要签名正确,就证明客户端确实持有与公钥配对的私钥。整个过程里,私钥不会通过网络发送,密码在这个过程中根本不出现。这就是密钥认证和密码认证最本质的区别:密码需要交给对方验证,私钥只需要在本地完成一个签名动作就够了。
2.2 一次免密登录的握手流程拆解
把一次免密 SSH 登录完整拆开来看。客户端发起连接后,双方先协商 SSH 协议版本,再交换密钥协商算法,比如常见的 curve25519-sha256、diffie-hellman-group-exchange-sha256 等。接下来进入用户认证阶段,客户端会把自己持有的公钥标识发给服务端,等价于说“我想用这把公钥对应的身份登录”。服务端在 authorized_keys 里搜索这个公钥,如果不存在,认证直接失败,退回密码输入;如果存在,就进入真正的验证环节。
服务端生成随机数据发给客户端,客户端用私钥签名后返回,服务端用 authorized_keys 里保存的公钥去验签,匹配就判定通过。之后再建立会话、分配伪终端。如果打开 ssh -v 的详细日志,能看到 debug1: Offering public key 就是客户端在“报身份”,而 Authentications that can continue 表示还没通过、正在尝试其他方式。看到什么日志就知道卡在哪一步,排查方向也清晰很多。
2.3 私钥永不出门的原理与安全边界
理解了私钥签名机制,也就明白了为什么私钥不能带离客户端。服务器保存的公钥只能验证签名,它无法反过来推导出私钥,更无法伪造一次有效签名。所以哪怕服务器的 authorized_keys 内容被其他人拿到,最多知道有哪些公钥,依然不能冒充真实用户去登录其他机器。这也是密钥认证比密码认证更安全的重要基础。
但这里有一个边界:私钥文件本身是普通文件,如果本机被入侵拿到 root 权限,私钥照样可以被拷走。所以安全实践上,私钥建议加口令短语,或者配合 ssh-agent 缓存使用;更严格的团队会把私钥放在硬件令牌或内网专用跳板机里,不让私钥文件进入普通办公电脑。不要只图配置省事,就把私钥变成遗留文件放在危险目录里。
3. 让第一台机器真正免密的完整落地步骤
3.1 生成密钥对:算法选择与文件细节
第一步一定是在客户端生成密钥对。以常见的 OpenSSH 客户端为例,最简单的命令是:
ssh-keygen -t rsa -b 4096 -C "deploy@example" -f ~/.ssh/id_rsa_prod参数里 -t 指定算法,-b 指定位数,-C 是注释,-f 是输出文件路径。如果打算用 ed25519 算法,可以执行:
ssh-keygen -t ed25519 -C "deploy@example" -f ~/.ssh/id_ed25519_proded25519 密钥短、速度快、安全性好,多数新版 Linux 发行版都支持;如果环境里还有较老的网络设备、内网系统或者定制 SSH 服务端,RSA 4096 的兼容面更广。我个人的习惯是:纯 Linux 集群直接用 ed25519,可能连接老设备的环境保留 RSA 4096。生成过程中会提示设置口令短语,回车可以跳过,但我推荐设置一个,因为不影响后续免密体验,可以配合 ssh-agent 保存。
生成完毕后 ~/.ssh 下会出现两个文件。没有扩展名的是私钥,权限默认 600;以 .pub 结尾的是公钥,内容是一行文本,结构为“算法类型 密钥主体 注释”。公钥可以公开,私钥必须藏好。
3.2 用 ssh-copy-id 把公钥安全地送过去
接下来要把公钥放到目标服务器的 ~/.ssh/authorized_keys 里。很多系统自带 ssh-copy-id 命令,用法很直接:
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub ops@192.168.1.10它会提示输入一次目标机器的密码,验证通过后自动创建 ~/.ssh 目录(如果不存在),把公钥追加到 ~/.ssh/authorized_keys,并设置合理权限。第一次执行时如果出现无法确认主机指纹的提示,输入 yes 即可,指纹会记录到本机的 known_hosts 文件。
对于没有 ssh-copy-id 的发行版或精简环境,可以手动完成同样的事:
cat ~/.ssh/id_ed25519_prod.pub | ssh ops@192.168.1.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"这条管道命令做了四件事:创建目录、收紧目录权限、追加公钥、收紧文件权限。看起来啰嗦,但在某些老系统上可能是唯一办法,建议背下来当兜底方案。执行完之后,authorized_keys 里就有了以 ssh-ed25519 或 ssh-rsa 开头的一行公钥。
3.3 测试登录,确认没有掉回密码认证
分发完公钥后,不要急着关掉当前会话,先开一个新终端测试:
ssh -i ~/.ssh/id_ed25519_prod ops@192.168.1.10如果能直接进入 shell 而不问密码,说明认证已经生效。想更细致确认的话,用 ssh -v 打开详细日志,看到 debug1: Server accepts key 这类输出,就说明服务端认可了客户端提供的公钥。如果依然提示输入密码,先别怀疑命令写错,大概率是权限或配置问题,按后面的排查思路走。
还有一个动作建议做:测试通过后,再考虑关闭目标服务器的密码认证。编辑 /etc/ssh/sshd_config,找到 PasswordAuthentication 改成 no,然后重启 sshd。这样服务器只接受密钥认证,密码被兜底的可能性归零。但改之前务必确认密钥认证已经稳定可用,否则一旦密钥出问题,你连密码登录的后路都没了。
4. 免密失败最常见的原因:权限、主机指纹与系统策略逐一排查
4.1 权限红线:.ssh 目录与 authorized_keys 的任何多余写入权限都会被拒绝
免密不生效,八成死在权限上。OpenSSH 服务端对用户 .ssh 目录和 authorized_keys 文件的权限非常敏感,目的是防止其他用户往你的 authorized_keys 里塞公钥或者篡改文件。一旦属主以外的用户拥有写入权限,服务端会直接拒绝使用该公钥登录,日志里会出现 bad ownership or modes。
这组权限是标准答案:
- .ssh 目录权限为 700
- authorized_keys 文件权限为 600
- 家目录权限不能过于宽松,至少不能让其他人有写权限,一般 755 或更严格
修复命令:
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chmod 755 ~执行完再重试 ssh。很多“密钥和公钥都生成了、也复制过去了,还是不行”的报错,一条 chmod 就解决。需要提醒的是,如果之前手滑对整体目录执行过 chmod -R 777,那不只是 .ssh 目录,整个家目录都会出现权限风险。这种情况下不仅要修正 .ssh 相关权限,还应该检查家目录里有没有不该暴露的文件。
4.2 known_hosts 与 StrictHostKeyChecking:首次连接和指纹变更的阻断
客户端也有权限类问题,主要是 ~/.ssh/known_hosts。这个文件记录的是你曾经连过的主机指纹,用来防止中间人攻击。第一次连接一台服务器时,终端会显示一大段指纹并要求确认,输入 yes 后写入 known_hosts。如果服务器重装系统或更换了主机密钥,再次连接时客户端会提示指纹不符并拒绝连接,这是安全机制在正常工作,却常被误读为配置错误。
自动化脚本里出现这种交互提示很讨厌,所以很多人会临时加 -o StrictHostKeyChecking=no 跳过。这个参数能解决自动连接问题,但会弱化主机指纹校验,适合私有网络环境,公网环境下不建议长期关闭。更折中的方案是:
ssh -o StrictHostKeyChecking=accept-new ops@192.168.1.10它允许自动记录新主机的指纹,同时对已知主机保留严格校验,相当于只信任新增记录。
如果指纹已经变化,需要先删除旧记录。比如主机 IP 是 192.168.1.10,执行:
ssh-keygen -R 192.168.1.10再重新连接就会加入新指纹。这个操作我翻过车:一次性批量执行巡检脚本,跑到一半突然因为某台机器指纹变了中断,排查半天才发现那台机器之前被重装过系统,known_hosts 里遗留的指纹早已失效。所以这里的报错不一定代表配置有问题,也可能是安全机制在保护你。
4.3 日志、SELinux 与 sshd_config 系统策略的最后一公里
权限正确、known_hosts 也没问题,登录还是失败,下一步要看服务端日志。systemd 系发行版可以执行 journalctl -u sshd,传统环境则看 /var/log/secure 或 /var/log/auth.log。常见的关键报错除了 bad ownership,还有 no matching key found、Authentication refused 等,每种指向不同方向。
SELinux 开启的环境里,新生成的 .ssh 文件可能被打上不正确的安全上下文。手动创建或复制过来的文件有时会继承错误类型,导致 sshd 没有权限读取。遇到这种情况,恢复上下文即可:
restorecon -R -v ~/.ssh这个命令会把 .ssh 下的文件类型修正为 ssh_home_t 等正确类型。很多人在 CentOS 类机器上配完免密始终不生效,一次 restorecon 就解决了,成本极低,强烈建议遇到“什么都对但就是不行”的时候先跑一遍。
配置文件层面还要检查 sshd_config。确认 PubkeyAuthentication 没有被注释或设为 no,确认 AuthorizedKeysFile 没有被改成奇怪路径。很多人会启用 PermitRootLogin prohibit-password,它允许 root 用密钥登录但不允许密码,这个配置本身不影响普通用户的密钥认证,但如果你正好尝试 root 免密失败,记得把它和全局 PasswordAuthentication 分开看,别改错位置。
5. 多主机、批量分发与日常维护的效率经验
5.1 ssh-agent:让带口令短语的私钥免去重复输入
私钥设置了口令短语之后,每次 SSH 连接都会要求输入一次。有些人觉得这还不如密码认证方便,其实不对,可以用 ssh-agent 把私钥先解锁一次,后续会话自动复用。ssh-agent 相当于一个内存中的密钥保管员,私钥先加载进去,之后连接时由它替你做签名动作,不再重复询问口令。
启动和添加密钥的常用方式:
eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519_prod第一个命令启动 agent 并设置环境变量,第二个命令把私钥加入托管。加入后测试连接目标机器,正常情况下不再要求输入私钥口令,也不再要求输入登录密码。如果用的是图形桌面环境,很多发行版会自动启动 ssh-agent,可以用 ssh-add -l 查看当前已加载的密钥列表。这个组合用起来之后,免密体验才算是真正的完整。
5.2 用 ~/.ssh/config 管理多台主机的连接参数
机器一多,每次 ssh 都要加 -i 指定私钥、-p 指定端口,命令又长又容易错。更好的做法是编辑 ~/.ssh/config 文件,为每台主机定义别名。比如:
Host prod-web HostName 192.168.1.10 User deploy Port 2222 IdentityFile ~/.ssh/id_ed25519_prod配置完成后直接执行 ssh prod-web 即可。这个文件还能设置全局默认参数,比如默认超时时间、默认连接方式等。对不同环境做隔离时,用不同 IdentityFile 指向不同私钥,公钥分别放到对应环境的 authorized_keys 里,登录时就彻底不用记哪台机器用什么密码了。配置文件本质上把散落的连接参数全部集中起来,长期维护成本会低很多。
5.3 批量分发公钥:从循环脚本到配置管理工具
需要一次性给几十台机器分发公钥时,可以写一个简单循环脚本:
#!/bin/bash for host in $(cat hosts.txt); do ssh-copy-id -i ~/.ssh/id_ed25519_ops.pub ops@$host done执行过程会逐台询问目标机器密码。如果各机器密码不统一,建议先处理成一致的临时口令,批量完成后再收走,否则脚本会卡在交互输入上。更工程化的做法是借助配置管理工具。只要有一台机器已经支持密钥登录,就能通过配置文件的角色定义,把 authorized_keys 的变更下发到所有服务器,整个过程不再需要交互式密码。
对于新上架的机器,常见做法是先建立一台“种子机”,从它开始批量同步剩余集群。这个循环脚本加配置管理工具的组合,我在实际环境里验证过,几十台上千条公钥的同步效率比手工一台一台弄快一个数量级。当然,批量操作前最好先做主机探活和端口检查,避免个别离线 IP 拖慢整个流程。
5.4 误删私钥与密钥轮换:一次事故的复盘教训
有次我为了清理 ~/.ssh 目录,顺手把旧的未命名私钥文件删掉了,结果直接导致多台旧服务器的密钥登录全部失效,却完全想不起哪些机器还在用那把公钥。当时唯一能做的就是重新生成一对新密钥,登录到所有受影响机器手动替换 authorized_keys,折腾了大半天。这次教训让我形成了两条规矩。
第一,私钥要么不做备份,要做就加密备份,绝不直接放在服务器上。第二,密钥轮换不要一刀切。正式变更前,先在 authorized_keys 文件里同时保留旧公钥和新公钥,等所有客户端切到新私钥之后再摘除旧公钥。这个过渡方式在多人环境尤其重要,否则其他人还没换完新私钥,机器就会突然失联。
如果你也遇到私钥丢失的情况,常规应对路径是重新生成密钥对,用仍然能登录的方式(密码登录或者另一台已配置免密的机器)登录目标服务器,把新公钥追加到 authorized_keys。如果批量崩溃,那真的要感谢没有过早关闭密码认证,不然连救援入口都没有。这也是我前面反复强调不要急着退掉 PasswordAuthentication 的原因:免密的鲁棒性建立在权限、备份和轮换策略都成熟之后,而不是把后路全部堵死。