做自动化运维这行,第一课往往不是学Playbook语法,而是先把控制的通道打通。Ansible默认走SSH协议去连被管服务器,你写一百个任务,最后都得落到“这台机器能连上那台机器”这个基础上。SSH免密登录做不扎实,ansible命令就会卡在各种密码交互上,轻则效率低下,重则把整个批量流程直接卡死。
这篇文章是Ansible自动化运维系列里关于SSH免密登录控制的部分,核心就一件事:在Ansible的控制节点与被管节点之间,用SSH密钥机制彻底替代密码登录,实现免密、批量、可控的服务器访问。如果你是刚开始搭Ansible环境,或者已经跑通但偶尔还要输密码、总觉得权限管理有点混乱,这篇内容应该能帮你把底层通道彻底理清楚。整篇会从原理讲到实操,再讲到问题排查,尽量让新手跟得上,也能让有基础的人补上几个平时容易忽略的细节。
1. 为什么搞Ansible前要先打通SSH免密通道
1.1 Ansible的通信机制决定了免密是刚需
Ansible和很多自动化工具不一样,它不在被管服务器上装Agent。它靠的是控制节点通过SSH协议去连接目标机器,把要执行的模块推过去,跑完再拿结果回来。这个设计让Ansible很轻量,但代价是它对SSH的依赖特别强,几乎每一条任务都对应一次SSH连接。
这意味着控制节点到每台被管节点的连接链路必须稳定、可复用。如果靠密码交互去连,Ansible会卡在“等待输入密码”这一步,自动化就变成半自动了。尤其是执行几十台、上百台机器的批量操作时,每台机器弹一次密码提示,整条流水线就被拖成手工活了。所以,把SSH免密登录配好,是Ansible落地的前置条件。
我见过一些人用ansible_ssh_pass直接在Inventory里写密码,还有人用sshpass包一层密码参数,短时间内确实能跑起来。但密码本身会过期、会被安全策略禁止远程登录、还会因为特殊字符转义出各种幺蛾子。生产环境最稳的路径,就是把密钥认证跑通,让SSH连接对调用方完全透明。
1.2 密码登录为什么在批量场景下撑不住
单个密码登录日常用没什么问题,一旦进入自动化运维场景,痛点会集中爆发。
第一是交互阻塞。SSH密码登录是交互式的,它需要从终端读取输入。Ansible在非交互模式下没法直接输入密码,虽然可以借助参数绕过,但本质上是把密码躺在配置里,安全隐患不小。第二是密码本身的生命周期问题。企业中一般有密码策略,90天强制改密一次,如果自动化链路依赖密码,一到改密窗口整个Ansible全部瘫痪,这个场面我经历过一次就不想再经历了。第三是审计和管理混乱。谁用了哪个密码去连哪台机器,很难追溯,权限回收更是麻烦。
相比之下,密钥认证天然适合批量场景。密钥对是一次生成的,私钥保存在控制节点,公钥统一分发到所有被管服务器;只要私钥不泄露,访问权限就始终可控。就算某台被管的公钥需要撤销,从authorized_keys里删掉对应条目就行,不用所有机器一起改密码。
1.3 SSH密钥认证的底层逻辑:一把钥匙一把锁
SSH密钥认证的原理,可以理解成“一把钥匙一把锁”的关系。
整个体系里有两个关键文件:公钥和私钥。公钥是挂锁,分发到被管服务器的~/.ssh/authorized_keys文件里;私钥是开锁的钥匙,留在控制节点本地。你拿私钥去连服务器时,SSH服务端会先用authorized_keys里的公钥生成一个挑战(challenge),发给你;控制节点用私钥对这个挑战做签名响应,服务端再用公钥验证这个签名。签名验证通过,连接就建立了。整个过程里私钥永远不会离开控制节点,更不会在网络里传输。
这就是密钥认证比密码认证更利于自动化的原因。服务器判断的是“你是不是持有那把唯一的私钥”,而不是“密码字符串对不对”。一旦配置好,后续连接完全不需要人工介入,这才是自动化工具需要的通道。理解了这一点,后面排查问题就知道往哪个方向看:要么是私钥对不上,要么是公钥没放对位置,要么是权限类问题导致SSH服务端不认。
2. 实验环境规划与基础检查
2.1 服务器清单与角色划分
动手之前,先规划好环境。以最常见的初始化场景为例:
- 控制节点:1台,安装Ansible,保存SSH私钥,负责下发任务。
- 被管节点:2~3台,安装SSH服务端,保存控制节点的公钥,处于待命接收指令的状态。
假设控制节点IP是192.168.1.10,被管节点是192.168.1.11、192.168.1.12、192.168.1.13。操作系统建议统一用同一发行版,至少SSH版本要接近,不然在一个环境踩的坑换到另一个环境可能又多出几个新问题。当然这只是最小实验拓扑,生产环境中被管节点可能几十上百台,但配置思路是一样的。
在实际项目中,我更推荐先把主机清单列出来,按角色分组:哪些是Web节点、哪些是数据库节点、哪些是应用节点。分组本身是后面Inventory设计的一部分,提前规划好,后面给Ansible配置组变量时就顺手了。
2.2 检查SSH服务端状态与必要组件
在走密钥分发流程前,先确认被管节点SSH服务是正常工作的。用命令行检查:
systemctl status sshd如果服务没跑起来,先启动再设置开机自启:
systemctl start sshd systemctl enable sshd接着确认sshd_config里的关键配置没有被改坏。默认情况下PubkeyAuthentication是yes,但如果之前有人优化过安全策略,可能被关掉,那后面怎么配都不可能免密登录成功。检查方法:
grep -E "PubkeyAuthentication|PasswordAuthentication" /etc/ssh/sshd_config如果你在测试阶段不想折腾太多,只要PubkeyAuthentication yes存在就行;PasswordAuthentication暂时保持yes,因为初次分发公钥还要靠密码登录,后面再收紧策略。
控制节点那边,确认一下Ansible装好没有。装的方法每个发行版不太一样,比如CentOS/RHEL和Debian/Ubuntu的包管理器命令不同,但核心就一条:ansible --version能正常输出版本信息。另外确认控制节点有ssh-keygen和ssh-copy-id命令,前者基本是OpenSSH自带的,后者可能在部分最小化安装中需要单独装。
2.3 密钥类型选型:Ed25519还是RSA
很多入门教程直接就是ssh-keygen -t rsa一条命令回车到底,能用是能用,但我觉得值得花两分钟考虑一下密钥类型。
现在主流的两个选择是RSA和Ed25519。
RSA的优势是兼容性好,老版本Linux、老网络设备基本都认。但为了保证安全,RSA密钥长度至少建议2048位,更多时候推荐3072或4096。密钥文件大,生成速度慢,验证过程也更费资源。
Ed25519是椭圆曲线签名算法,OpenSSH 6.5以上就支持了,到现在十多年了,主流Linux发行版基本都覆盖。它的密钥长度短、生成快、验证性能好,安全性上也被广泛认可。我的建议是:如果被管节点都是近些年安装的系统,优先选Ed25519;如果环境里还有很老的操作系统或网络设备,那就退回到RSA 4096。
这里给出一个对比表格,方便按实际情况选:
| 对比项 | Ed25519 | RSA(2048+) |
|---|---|---|
| 密钥长度 | 固定,较短 | 可选,越长越安全 |
| 认证速度 | 快 | 较长密钥下稍慢 |
| 兼容性 | OpenSSH 6.5+ | 几乎所有平台 |
| 适用场景 | 现代Linux/Unix | 老系统、网络设备 |
| 推荐级别 | 优先 | 兼容性兜底 |
生产环境中如果统一用新系统,我就推荐Ed25519。一个典型的场景是给几十台新装好的CentOS 9或Ubuntu 22.04服务器做初始化,用Ed25519整体体验很顺。如果混入一台2012年的老服务器,那台单独补一个RSA密钥也就完事了。
3. SSH密钥的生成与批量分发(核心实操)
3.1 用ssh-keygen生成密钥对(含参数逐项解析)
在控制节点上,以想要执行Ansible的用户登录(比如专门建一个ansible用户,或者直接用root,取决于你的风险偏好),执行:
ssh-keygen -t ed25519 -C "ansible-control-node" -f ~/.ssh/ansible_ed25519 -N ""逐项拆解一下这几个参数,因为很多教程直接跳过解释,导致后面出了问题不知道从哪里看:
-t ed25519:指定密钥类型,前面已经聊过选型逻辑,这里用的是Ed25519。-C "ansible-control-node":给密钥加注释。这个注释最后会出现在公钥文件的末尾,相当于给这把钥匙做个标识。运维环境里如果有多套密钥体系,注释写清楚是哪个控制节点、什么用途,能省去很多排查时间。-f ~/.ssh/ansible_ed25519:指定私钥保存的路径和文件名。拆开写的好处是让密钥名一眼就能认出用途,方便管理。-N "":设置私钥的passphrase,也就是私钥口令。这里先留空,是为了让Ansible调用SSH时不需要再额外输入口令,保证自动化链路全自动。
生成后会在~/.ssh目录下出现两个文件:
ls -l ~/.ssh/ansible_ed25519*ansible_ed25519是私钥,ansible_ed25519.pub是公钥。此时可以顺便看一下公钥内容:
cat ~/.ssh/ansible_ed25519.pub输出大概是:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxx ansible-control-node后缀那段ansible-control-node就是我们用-C加的注释。后面分发到被管服务器后,可以通过这个注释快速识别是来自哪个控制节点的公钥。
这里有一个很重要的权限点:私钥文件的权限必须是600,即只允许当前用户读写。如果权限太宽松,SSH客户端会直接拒绝使用这个私钥。生成后可以检查一下:
ls -l ~/.ssh/ansible_ed25519正常输出应该是-rw-------。如果不是,手动修正:
chmod 600 ~/.ssh/ansible_ed25519注意:
~/.ssh目录本身的权限也建议设置为700。这个细节很容易被忽略,但SSH对目录权限很敏感,目录权限过宽照样会报错。
3.2 首次分发:ssh-copy-id的正确姿势
密钥对生成后,下一步是把公钥放到被管节点的authorized_keys里。最推荐的方式是使用ssh-copy-id命令,它会自动处理文件是否存在、内容是否重复、权限是否正确这些细节。
以192.168.1.11为例:
ssh-copy-id -i ~/.ssh/ansible_ed25519.pub -p 22 root@192.168.1.11命令执行后,SSH会提示输入目标机器的root密码。输入正确密码后,公钥会被追加到目标机器的~/.ssh/authorized_keys文件中。此时可以顺手验证一下免密登录是否生效:
ssh -i ~/.ssh/ansible_ed25519 -p 22 root@192.168.1.11如果能直接进入目标机器的shell而不需要输入密码,说明密钥认证已经成功了。退出后,对192.168.1.12和192.168.1.13重复同样的操作。
ssh-copy-id内部做了三件事:检查本地公钥文件是否存在;检查目标机器authorized_keys文件是否存在;把公钥内容追加进去并设置正确权限。这比手动用管道执行cat id_ed25519.pub >> authorized_keys要稳得多,因为手动追加容易踩到权限和路径不一致的坑。
3.3 批量分发公钥的实战脚本
被管节点只有两三个时,手工一条条执行没问题。一旦数量上来,手工操作就效率太低了。这里给一个批量分发的脚本思路:
#!/bin/bash NODES="192.168.1.11 192.168.1.12 192.168.1.13" SSH_PASSWORD="YourPassword" for NODE in $NODES; do sshpass -p "$SSH_PASSWORD" ssh-copy-id -i ~/.ssh/ansible_ed25519.pub \ -o StrictHostKeyChecking=no -p 22 root@$NODE done这段脚本用到了sshpass,它允许在命令行直接指定密码,让首次复制公钥的过程不需要人工干预。但这里有两个点必须提醒:
第一,sshpass是明文把密码暴露在命令行里的,在测试环境图省事可以,在正式环境这么做等于把密码拱手送人。生产环境我一般建议只在初始化的内网环境用,用完立刻改密码,或者改用更安全的凭据管理平台来配合分发。
第二,-o StrictHostKeyChecking=no是为了跳过首次连接时的主机指纹确认。如果你不想降低安全级别,可以先用ssh-keyscan把所有被管节点的主机指纹预存到控制节点的known_hosts里,这样既不用交互确认,也没有完全关闭指纹校验:
ssh-keyscan -p 22 192.168.1.11 192.168.1.12 192.168.1.13 >> ~/.ssh/known_hosts再强调一次:批量分发公钥只是初始化过程,前期的目标是快速打通通道。通道一旦打通,后续运维应当切换到纯密钥认证模式,密码登录该关就关。
4. 接入Ansible:Inventory与连通性验证
4.1 最小可用的Inventory配置
公钥分发完成、免密登录验证通过后,Ansible部分就可以开始配置了。第一步是写Inventory,也就是告诉Ansible要管理哪些机器。
创建一个最小化的Inventory文件hosts.ini:
[web] 192.168.1.11 ansible_user=root 192.168.1.12 ansible_user=root [db] 192.168.1.13 ansible_user=root这里分成了web和db两个组,组名可以按你的业务架构自定义,后面Playbook可以按组去批量执行任务。ansible_user=root指定登录用户,如果前面分发公钥时用的是root,这里就是这个;如果用的是普通用户加sudo,后面还需要配置become等参数,但那个属于另一个话题了。
如果被管节点的SSH端口不是默认的22,可以在主机后面补上端口参数:
[web] 192.168.1.11 ansible_user=root ansible_port=22224.2 ansible.cfg关键参数说明
Inventory写好后,最好在控制节点写一个ansible.cfg,把常用的连接参数固定下来,不然每次敲ansible命令都要在命令行加一堆参数。
[defaults] inventory = ./hosts.ini host_key_checking = False private_key_file = ~/.ssh/ansible_ed25519 remote_user = root [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s几个参数说明一下:
inventory:指定Inventory文件路径,可以是相对路径或绝对路径。这样ansible all -m ping就不用每次加-i。host_key_checking = False:关闭首次连接时的主机指纹交互确认,避免首次连接卡在“yes/no”提示上。生产环境如果条件允许,更稳妥的做法是用ssh-keyscan预存指纹,但至少在Ansible内网环境中,关闭这个检查是普遍操作。private_key_file:指定Ansible用来连接被管节点的私钥文件路径。这里必须跟前面生成的私钥路径一致。ssh_args:开启SSH连接复用。ControlMaster=auto和ControlPersist=60s的意思是建立一个连接后在60秒内复用同一个SSH连接,做批量操作时能明显减少握手耗时,大并发任务尤其有效。
4.3 用ping模块验证全链路连通
配置写完后,执行:
ansible all -m ping如果一切正常,输出会是类似这样的:
192.168.1.11 | SUCCESS => { "changed": false, "ping": "pong" } 192.168.1.12 | SUCCESS => { "changed": false, "ping": "pong" } 192.168.1.13 | SUCCESS => { "changed": false, "ping": "pong" }看到三台机器都返回pong,说明从控制节点到被管节点的SSH密钥通道已经打通,Ansible的Inventory配置也没问题。到这一步,整个免密登录控制的基础工作就算完成了。
如果某台机器报错,可以把报错信息对着下一节的问题排查表过一遍。ping模块能做到的是最底层的连通性验证,这个通了,后面写Playbook就不会在连接层面浪费时间了。
5. 高频问题排查与避坑实录
5.1 认证失败的定位思路
最常见的问题就是Permission denied (publickey)。这个报错看起来吓人,但本质上就那几类原因:
- 公钥没有正确追加到目标机器的
authorized_keys。解决方法是登录到被管节点,确认~/.ssh/authorized_keys里确实有控制节点的公钥内容。 authorized_keys文件权限不正确。SSH对权限很敏感,文件权限不能是其他用户可写,一般建议600。如果发现权限过宽,马上:chmod 600 ~/.ssh/authorized_keys。- 私钥权限不正确。这个前面提过,私钥文件要600,权限过宽SSH会拒绝使用。可以执行
chmod 600 ~/.ssh/ansible_ed25519修复。 - sshd_config中
PubkeyAuthentication被设为no。用root登录被管节点,检查配置并改为yes后重启sshd。 - SELinux导致SSH读取
authorized_keys受限。这种情况在CentOS/RHEL系比较常见,可以检查SELinux的提示,或者临时用restorecon -R -v ~/.ssh恢复上下文。
排查时我一般会先跑一个带详细日志的命令:
ssh -i ~/.ssh/ansible_ed25519 -vvv -p 22 root@192.168.1.11-vvv会把SSH客户端的调试信息全部打出来,看它卡在哪一步。如果看到Offering public key后面跟Authentications that can continue: publickey,那大概率是公钥认证没通过;如果看到Connection closed by ...,可能是sshd本身拒绝了连接。日志能帮你快速缩小范围,省得瞎猜。
5.2 known_hosts与首次连接确认问题
我第一次做批量分发的时候,遇到过不少Host key verification failed的报错。这个报错的原因是控制节点的known_hosts文件里已经有了目标IP的记录,但目标机器的SSH主机指纹变了,或者压根没有这条记录导致的交互确认被自动化环境卡住了。
在Ansible场景下,这个报错很烦人,因为它会中断整个批量任务。解决方法有三个:
一是临时关闭指纹检查,在ansible.cfg里设置host_key_checking = False。这是最简单的方式。
二是用ssh-keygen -R删掉旧指纹。如果目标机器的指纹变了,可以执行:
ssh-keygen -R 192.168.1.11然后重新连接,让它把新指纹写入known_hosts。
三是用ssh-keyscan提前预存所有目标机器的指纹:
ssh-keyscan -p 22 192.168.1.11 192.168.1.12 192.168.1.13 >> ~/.ssh/known_hosts三种方式按需选择。测试环境用第一种最省事;生产环境如果安全要求严格,用第三种更合理。
5.3 非22端口、多用户场景的处理
SSH默认端口是22,但有些企业的安全策略会要求改成其他端口。如果被管节点SSH端口不是22,分发和连接都有对应调整。
分发公钥时:
ssh-copy-id -i ~/.ssh/ansible_ed25519.pub -p 2222 root@192.168.1.11Ansible连接时,Inventory里加端口参数,或者命令行指定:
ansible all -m ping -e "ansible_port=2222"多用户场景下,公钥要放到对应用户的~/.ssh/authorized_keys里。比如你想用devops用户跑Ansible,那就把公钥放到/home/devops/.ssh/authorized_keys,而不是放root的。Inventory里也相应改:
[web] 192.168.1.11 ansible_user=devops如果用普通用户,而任务需要root权限,那就涉及Ansible的become机制。这里提醒一句:公钥分发的用户跟Ansible执行任务的用户必须一致,否则虽然密钥认证成功了,但换一个用户还是提示权限不足,我第一次在这个问题上绕了蛮久。
5.4 批量分发时的密码交互处理与安全取舍
批量分发公钥时,sshpass是一个很方便但也很敏感的工具。它的本质是把密码以明文形式传给SSH客户端,这在命令行里、脚本历史记录里、ps的输出里都可能暴露密码。
如果你想用,至少要注意这几点:
- 不要在多人共享的机器上执行带明文密码的批量分发脚本。
- 执行完马上把脚本里的密码占位符改掉或删除。
- 优先考虑从已经免密的机器再往其他机器分发公钥,避免大面积使用密码。
举个例子:如果有一台192.168.1.10已经免密登录了192.168.1.11,而192.168.1.11又能免密登录192.168.1.12,那你可以通过链式分发避免集中暴露密码。这个方式在安全要求较高的场景下更合适,但配置复杂度也更高。
6. 密钥的日常管理与安全加固建议
6.1 密钥权限、存放与备份规范
密钥认证打通后,最怕的事情就是私钥丢失或泄露。私钥一旦泄露,所有配了对应公钥的服务器都等于被人拿走了钥匙。所以日常管理有几个习惯必须养成。
第一,私钥存放位置固定,权限必须是600。不要把私钥放到一个所有用户都能读的目录里,比如/tmp或者项目的公共目录。
第二,建议给私钥设置passphrase。前面我为了Ansible全自动运行,把passphrase留空了。但在生产环境,尤其是私钥需要拷贝到多人协作的开发机时,passphrase是最后一道防线。留空的风险是:任何能读到私钥文件的人都能直接使用它。设置了passphrase之后,即使私钥文件被复制出去,对方也没有口令可用。当然,如果设置了passphrase,Ansible那边就得配合ssh-agent来缓存口令,不然还是会卡交互。这个取舍要结合团队的安全策略来定。
第三,私钥要做好备份。备份文件建议加密存储,比如放到公司的密钥管理平台,或者用加密压缩包存到离线位置。别到时候一台控制节点硬盘坏了,整套自动化通道就得全部重建。
还有一个细节:公钥虽然不带敏感信息,但也不是完全无所谓。公钥注释字段里如果有明确的机器名、人员名,别人扫一眼就能知道哪台机器是哪个控制节点的,有信息暴露风险。所以注释写得规范但不啰嗦,就够了。
6.2 服务端加固:禁密码、限来源
密钥认证全部验证通过之后,就可以考虑把被管节点的密码登录关掉了。这个动作在sshd_config里完成:
vim /etc/ssh/sshd_config把以下几项配置好:
PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-passwordPasswordAuthentication no:关闭密码认证,只允许密钥认证。PubkeyAuthentication yes:确保公钥认证是开启的。PermitRootLogin prohibit-password:允许root通过密钥登录,但禁止直接密码登录。如果你的安全策略不允许root远程登录,可以改成no,但那样的话Ansible需要在Inventory里配一个普通用户加become,处理方式会不太一样。
修改后重启sshd:
systemctl restart sshd警告:在关闭密码登录前,务必先在另一个终端里确认密钥登录是正常的。我身边就有人把
PasswordAuthentication改成no之后,一不留神把当前连接断开,结果新连接又因为密钥配置有问题死活进不去,最后只能跑去机房或者通过带外管理口救机。这个坑一旦踩进去,代价相当大。
如果被管节点有防火墙,可以考虑把SSH端口限制为只允许控制节点IP访问。例如用firewalld:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.10" port port="22" protocol="tcp" accept' firewall-cmd --reload这样即使其他机器拿到了私钥,也没法从任意来源发起连接。
6.3 密钥轮换与账号离职回收
长期不换密钥,就跟长期不换密码一样,风险会随时间累积。密钥轮换一般分两种节奏:
一种是定期轮换,比如每半年或一年给控制节点生成一套新密钥对,同时把旧公钥从所有被管节点移除。这个动作虽然简单,但量大的时候需要自动化辅助。你可以写一个临时Playbook,先分发新公钥,验证新连接正常后,再清理authorized_keys里的旧公钥条目。
另一种是事件驱动轮换。比如发现私钥可能泄露、某个运维人员离职、或者某台跳板机被入侵,这时候必须立刻换。换的流程和上面一样,但动作要快,不能拖到第二天。
账号离职回收也依赖密钥管理。如果用的是个人私钥,离职时要确保他的公钥从所有机器的authorized_keys里删除。这个操作可以用Ansible批量执行,但前提是你自己还有一套管理通道。更规范的做法是:生产环境中用跳板机统一管理访问,控制节点本身不直接暴露公网,这样密钥的回收半径就小很多。
我在实际项目里一般会维护一个“密钥登记表”,记录每套密钥对应的控制节点、用途、负责人、创建时间、轮换时间。这个表看起来原始,但排障和审计的时候非常有用。尤其是多套密钥混用的时候,没有登记表,很容易把A项目的公钥分发到B项目的机器上,后面排查认证问题就全凭猜了。
最后再分享一个小技巧。SSH密钥免密登录配置好之后,你可以在Ansible的每个Playbook开头加一个ansible all -m ping的验证步骤,或者直接在跑任务前先快速ping一圈。这样一旦有节点因为网络变动、密钥被误删等原因掉线,你能在执行正式任务前就发现问题,而不是等Playbook跑到一半才爆一堆连接超时。这个习惯帮我提前拦截过不少问题,强烈建议保留。