☰
Ansible离线批量升级OpenSSH:从依赖准备到Playbook落地的完整方案
2026/10/14 5:14:12 网站建设 项目流程

1. 为什么说手动升级OpenSSH,是在给自己挖坑

先说说我自己的经历。前阵子公司安全那边突然丢过来一份扫描报告:全网几百台Linux服务器存在OpenSSH相关中高危漏洞,要求在整改期限内全部修复。第一反应是,这有什么难的?下载个新版本rpm包,一台一台传上去,停掉sshd,装好再启动,完事。我甚至还在脑子里规划了一下,一天处理二三十台,一周怎么都搞定了。

结果第一台机器就给了我一个下马威。上传rpm包倒是挺快,执行rpm -Uvh的时候提示依赖缺失,openssl版本对不上、pam开发包没装,命令行直接报错。换了一台机器,用yum本地安装,依赖倒是自动解决了,可是安装完重启sshd服务,SSH连接直接断掉,控制台登录一查,原来是新版本OpenSSH生成的host key权限和SELinux上下文有问题,服务压根起不来。更要命的是,我手动升级到第六台的时候,前面那几台到底哪台成功了、哪台是半吊子状态,我脑子里已经是一团浆糊了。因为有个别机器升级后sshd起不来,客户端连接直接超时,我甚至记不清哪几台需要立刻去机房或带外管理口处理。

这就是手动升级的真实体验:重复劳动极高、每台机器的环境差异让你防不胜防、状态全靠人脑记忆、出了问题只能靠运气排查。如果你的环境里有几十台、上百台机器,手动方案在执行过程中基本就是一场漫长的折磨。

Ansible进场之后,事情就变得完全不一样了。它天然是做“批量操作”的,把“在一台机器上做什么”抽象成一份声明式任务清单,然后用并行方式推到任意数量的机器上去执行。再加上幂等设计——同一个任务重复执行结果一致,你完全可以先把所有机器的OpenSSH升级流程写成一份Playbook,先跑两台验证没问题,再放量到几十台,最后全员执行。每一台机器的最终状态由Ansible的返回结果统一汇总,谁成功了、谁失败了、失败在哪一步,清清楚楚。

尤其在内网隔离、无法访问公网软件仓库的离线环境里,一台台手动传包、手动装包的痛苦会被无限放大,Ansible这种“控制端打包分发、被管端本地执行”的模式反而成了最优解。这正是我接下来要完整拆解的东西:如何用Ansible在离线环境下,批量升级上百台机器的OpenSSH。

2. 离线升级方案的整体设计思路

2.1 离线环境里,最麻烦的根本不是Ansible本身

很多刚接触Ansible的人会有个误解:离线环境下Ansible是不是就跑不了了?其实不是。Ansible对控制端和被管端的连接依赖的是SSH协议,只要你的控制端能通过SSH连到各台目标机器,Playbook就能正常下发和执行。真正麻烦的是软件包从哪来。

在线环境里,一条yum install -y openssh就能让服务器自己从镜像源拉取所有需要的rpm包;离线环境没有这个条件,你必须事先把OpenSSH以及它依赖的一整套rpm包准备好,再想办法分发到每台机器上。这里有两个方案可选:

第一种,把rpm包直接放到Playbook的文件分发目录里,通过Ansible的copy模块逐个推到目标机器,然后用shell命令就地安装。这个方案适合机器数量较少、依赖关系比较简单的情况,缺点是一旦rpm包之间依赖复杂,比如A依赖B、B又依赖C,你还得手动排好安装顺序,否则容易按照字母顺序安装到一半就报错。

第二种,在内网搭一个本地Yum仓库,把所有需要的rpm包扔进仓库里,目标机器配置指向这个内网源。Ansible只负责把repo配置文件推过去、执行yum安装,依赖解析交给yum自己处理。这个方案前期准备稍麻烦一点,但后续扩容、升级其他软件都能复用同一条链路,依赖自动解析,不容易出幺蛾子。百台规模我强烈推荐第二种。

2.2 控制端与被管端的角色规划

整个方案的物理拓扑很简单:一台能连通所有目标机器的控制端,加上N台被管服务器。

控制端的操作系统建议和你管理的目标机器保持同一系列,比如都用某Linux发行版的7或8系列,这样用createrepo生成仓库元数据时不会有兼容性问题。控制端上安装Ansible的方式很多,离线环境里推荐直接用发行版自带的软件源安装,如果没有,找一台有外网的机器下载Ansible对应发行版的rpm包,再拷贝进去,效果一样。

有个细节容易忽略:Ansible控制端本身需要能通过SSH免密登录所有目标机器。如果你在离线环境里临时为上百台机器配置免密,逐台ssh-copy-id依然繁琐,可以先写个小脚本批量处理,或者借助现有的统一账号管理平台下发密钥。我这里不再展开,但务必在跑Playbook之前把连通性验证做完,避免执行到一半才发现一半机器连不上。

2.3 升级路径与回滚预案

OpenSSH升级这件事,我最担心的是升级到一半sshd挂掉、远程连接断开,整台机器变成“只能进机房”的状态。所以在设计方案时,回滚和容错必须前置考虑。

回滚预案的核心是:升级前先把当前版本的openssh、openssh-server、openssh-clients这三个主要rpm包备份下来。一旦新版本启动失败或验证不通过,可以在带外管理口或控制台登录后,快速用rpm -Uvh --oldpackage把老版本装回去。这个动作同样可以写成Ansible的一个独立Playbook,平时不用,出问题的时候一键执行。

另外说一下升级路径的策略:我建议先小范围灰度(2到3台非核心机器),再扩到一批低峰期任务型机器,最后才是核心业务机器。不要上来就直接all in一百台,那是对自己的不负责。

3. 升级前的离线依赖准备

3.1 从软件源拉取RPM包的正确姿势

离线升级OpenSSH的第一步,是把新版本的rpm包以及它的全部依赖包收集齐。这里最容易踩的坑是:以为只要下载openssh相关的那三四个rpm就够了。OpenSSH是个对系统底层依赖很强的软件,它跟openssl、pam、zlib、selinux-policy这些基础库都有千丝万缕的联系,漏掉任何一个都会让你在离线安装时寸步难行。

正确做法是找一台和目标机器同操作系统、同大版本的机器,在能访问软件源的情况下,先准备一个干净的目录,然后用命令下载全部依赖:

mkdir -p /data/openssh-rpms cd /data/openssh-rpms yum install --downloadonly --downloaddir=. openssh yum install --downloadonly --downloaddir=. openssh-server yum install --downloadonly --downloaddir=. openssh-clients

这样下载下来的rpm包会包含当前系统依赖解析后的所有相关包。需要注意,不同小版本的系统,比如7.5和7.9,解析出来的依赖可能略有差异,稳妥起见可以在几个不同小版本的机器上各执行一次,把下载到的rpm合并去重。

如果完全没有任何外网机器可用,还有一个替代路子:从官网开源镜像站或者第三方构建源,手动下载指定版本OpenSSH的rpm包,再根据rpm包头信息里的Requires字段反向查找依赖。这种方式比较费时,而且手工容易漏,能走yum downloadonly就不要手挑。

3.2 校验与整理依赖清单

rpm包下载完成之后,不要急着建仓库,先做一个完整性校验。重点检查这几个维度:

第一,包版本是否一致。一套依赖里的openssl、openssh版本必须协同工作,如果你混用了不同大版本体系下的包,很可能出现符号链接或协议版本不对的诡异问题。

第二,包架构是否正确。x86_64机器不能混装i686的包,这个错误在手工下载时很容易发生。

第三,签名校验。有条件的情况下用gpg --verify检查一下包的签名,确认这些包确实来自官方源而不是被篡改过的。离线环境虽然风险低一些,但这个习惯值得保留。

整理完之后的正常状态是:目录下大概会有十到二十几个rpm包,数量视系统版本而定。接下来把这批包统一拷到一个固定目录,作为Yum仓库的基础内容。

3.3 本地Yum仓库的搭建(可选但推荐)

初始化一个本地Yum仓库,只需要两件事:createrepo生成元数据,Nginx或httpd提供Web访问。

# 在控制端上,假设rpm包已放置在 /var/www/html/openssh-repo/ createrepo /var/www/html/openssh-repo/ systemctl start nginx

然后把仓库的repo配置准备好:

[openssh-local] name=OpenSSH Local Repo baseurl=http://控制端IP/openssh-repo/ enabled=1 gpgcheck=0

这个repo文件就是接下来要被Ansible推送到所有目标机器上的关键物料。gpgcheck建议先置为0,等验证完整个链路没问题再考虑开启签名校验,否则离线环境很容易被签名问题卡住。

你可能会问:既然可以用Ansible的copy模块直接传rpm包然后本地安装,为什么还要多此一举搭仓库?我的回答是:依赖解析这件事交给yum远比你自己写shell判断要可靠,尤其是百台规模下机器环境参差不齐,有的机器缺这个依赖、有的机器缺那个依赖,一个本地仓库能让你少写几百行无用代码。

4. 编写Ansible Playbook的完整过程

4.1 Inventory分组与连接设置

在实际执行之前,先把Inventory规划好。我习惯把机器按重要程度分三个组:

[openssh-gray] node-1.example.com node-2.example.com [openssh-common] node-3.example.com node-4.example.com # ... [openssh-core] node-11.example.com # ...

分组的价值在于,你可以分批次放量,灰度组验证没问题后,再跑common组,最后处理core组。Ansible执行时可以只针对某个组操作:

ansible-playbook -i hosts upgrade_openssh.yml --limit openssh-gray

连接设置方面,批量执行时建议在ansible.cfg里显式配置SSH连接参数:

[defaults] host_key_checking = False timeout = 30 [ssh_connection] pipelining = True

host_key_checking关闭的原因不是不安全,而是首次连接上百台机器时,逐个交互确认host key会直接卡住整个流程;如果你对安全性有严格要求,可以先在灰度阶段用known_hosts批量预置。timeout设置30秒是为了在个别机器掉线时快速失败,不至于让整个Playbook长时间挂起。

4.2 Playbook核心任务拆解

整个Playbook的核心逻辑分三步:推仓库配置、安装新版本、验证服务状态。先用copy模块把repo文件推到目标机器,再用yum模块做安装,最后用systemd模块检查sshd状态。

--- - name: Upgrade OpenSSH offline hosts: openssh-gray gather_facts: true become: true vars: local_repo_file: "openssh-local.repo" local_repo_path: "/etc/yum.repos.d/" tasks: - name: Push local repo config copy: src: "files/{{ local_repo_file }}" dest: "{{ local_repo_path }}{{ local_repo_file }}" owner: root group: root mode: '0644' - name: Clean yum cache command: yum clean all - name: Install OpenSSH packages from local repo yum: name: "{{ item }}" state: latest disablerepo: "*" enablerepo: "openssh-local" loop: - openssh - openssh-server - openssh-clients - name: Check sshd config syntax command: sshd -t register: sshd_config_check failed_when: sshd_config_check.rc != 0 - name: Restart sshd service systemd: name: sshd state: restarted enabled: true - name: Verify sshd is active systemd: name: sshd state: started register: sshd_status - name: Show sshd status debug: msg: "sshd status: {{ sshd_status.status['ActiveState'] }}"

这里有几个设计细节要展开讲:

yum模块里我加了disablerepo和enablerepo,是因为目标机器上可能配置了多个仓库源,如果不做限制,yum会扫描所有仓库,一旦某个外部源不可达会导致超时。强制只启用我们自己的本地源,行为最可控。

ssh -t这个命令是在重启sshd之前做配置语法校验,很多升级后连不上的事故,根源是新旧配置项不兼容导致sshd启动失败。先校验再重启,能提前拦截一大半问题。

4.3 关键Trick:会话保持与重启策略

升级OpenSSH的过程中有个天然悖论:你要用SSH连到机器上改SSH服务,而重启sshd的一瞬间,当前连接理论上会受到影响。实际上大部分情况下,重启sshd并不会立即掐断已建立的连接,但如果新版本的sshd启动失败或者端口监听异常,你的控制端会瞬间失联。

针对这个问题,我习惯给Playbook加一个保护机制:在重启sshd之前,先启动一个临时独立监听端口的sshd实例作为逃生通道,或者直接用at命令安排一个延迟任务自动回滚。这里给出一个相对实用的方案:利用Ansible的async轮询模式,在重启命令执行后短暂等待几秒,重新探测端口,如果探测失败则触发回滚逻辑,把旧版本rpm装回去。

不过这个方案实现起来相对复杂,很多人一开始hold不住。我更推荐另一条更简单的路:先把新版本rpm包安装到机器上,但不立即触发重启;等所有机器都装完后,再单独跑一个收尾Playbook,把sshd服务重启这件事放到业务低峰期统一执行。这样就算重启过程出现问题,影响面也受控,处理一台机器的同时不会干扰到其他机器的安装流程。

5. 执行百台升级的实操记录

5.1 先跑小范围灰度

我自己的习惯是:任何Playbook第一次执行,永远先在2到3台低风险机器上灰度,不要直接上all组。灰度的目的不仅是验证Playbook语法,更是验证你的rpm依赖清单是否齐全、目标机器系统版本差异是否会造成问题。

实际操作时,我用--limit参数限定灰度主机,然后加上-C做一次语法级预检,再真正执行:

ansible-playbook -i hosts upgrade_openssh.yml --limit openssh-gray --syntax-check ansible-playbook -i hosts upgrade_openssh.yml --limit openssh-gray

灰度通过的标准有三个:Playbook所有任务返回ok或changed;sshd服务重启后控制端能重新连上目标机器;ssh -V显示新版本号。三个条件全部满足,才允许进入下一批。

有个容易被忽视的细节:灰度机器上安装完成后,我建议顺手做一次真实的SSH重连测试。Ansible的service模块报告“active”只能说明服务进程起来了,不能保证新版本能正常工作。用下面这条命令手动验证,虽然简单但极其有效:

ssh -o ConnectTimeout=5 user@node-1.example.com 'echo ok && ssh -V'

5.2 批量执行与过程观察

灰度确认无误后,我通常会这样放量:中午先跑一批非核心业务机器,观察半小时无异常,再跑第二批;核心业务机器留到晚上低峰期,并且逐台确认业务侧无连接告警。

批量执行时建议加上forks参数控制并发数。Ansible默认并发5,对百台规模来说太慢;但并发太高也有风险,因为每台机器都要经历“重启sshd”这个瞬间,如果几十台机器同时重启,万一某台机器因为配置问题启动失败,而你同时又在控制端跟其他机器保持连接,局面容易混乱。我实测下来,百台规模用forks=20比较均衡:

ansible-playbook -i hosts upgrade_openssh.yml --limit openssh-common -f 20

执行过程中,让Playbook的输出实时滚动到日志文件里,方便事后排查:

ansible-playbook -i hosts upgrade_openssh.yml --limit openssh-common -f 20 | tee upgrade_$(date +%F).log

5.3 升级后的验证清单

批量执行结束后,不要只看Ansible的返回结果就认为万事大吉。Ansible成功只能说明任务执行完毕,新版本sshd是否在所有机器上都处于健康运行状态,还需要一个独立的验证步骤。

我通常做三层验证:

第一层,批量验证sshd运行状态和版本。用Ansible的ad-hoc命令快速收集所有机器的版本信息:

ansible all -i hosts -m shell -a "sshd -V 2>&1 || ssh -V 2>&1" -f 20

第二层,随机抽几台机器,手动做一次SSH登录并执行几个常规命令,确认业务连接无异常。这一步看起来原始,但对发现“服务活着但实际不可用”这类隐形故障很有效。

第三层,观察一段时间内的系统日志,重点看sshd有没有持续报错的记录,比如协议错误、解析错误等。新版本升级后有一些warning不会导致服务挂掉,但会污染日志,长期堆积会影响问题排查效率,这些warning如果没有大碍也可以在下一次维护窗口顺手处理掉。

6. 高频问题与排查实录

6.1 常见问题速查表

我把这些年升级OpenSSH过程中遇到的典型问题整理成了一个速查表,按频率排序:

问题现象可能原因解决方法
yum安装时报依赖缺失本地仓库rpm包不全,或系统小版本差异导致依赖不一致重新在同版本机器执行downloadonly,补充缺失包
Playbook卡在Restart sshd步骤后连接断开sshd配置不兼容新版本,服务启动失败通过带外管理口登录,执行回滚Playbook,检查sshd -t输出
SSH客户端连接时报host key验证失败升级后host key发生变化,客户端known_hosts记录旧key在客户端清理旧host key记录,重新确认新key
sshd服务active但无法正常登录SELinux上下文或PAM配置与新版本不兼容检查audit日志,恢复SELinux上下文,必要时更新pam相关包
批量执行后部分机器返回failed备份用的yum缓存目录不一致,或个别机器系统版本特殊单独对failed机器跑--limit,逐台分析错误信息
SELinux环境升级后sshd端口监听异常新版本二进制文件的SELinux策略未加载执行restorecon -Rv /usr/sbin/sshd 或更新selinux-policy

6.2 踩过的几个坑和对应解法

第一个坑是依赖清单不全。我第一次做离线升级时,自信满满地只下载了openssh相关的三个包,结果跑到第二台机器就报openssl版本过旧。默认的openssl版本和灌进仓库里的新版本openssh不匹配,这种问题不会在灰度机器上暴露,因为灰度机器恰好openssl版本较新,等到大规模放量时才集中爆发。从那之后我学乖了,任何依赖清单都以yum downloadonly执行结果为准,不再凭感觉挑包。

第二个坑是host key变更引发的连锁反应。OpenSSH升级后,如果新版本生成host key的算法策略变了,或者原有host key文件被覆盖,所有曾连接过这台机器的客户端都会在连接时收到host key变更警告。在批量环境里,这就意味着成百上千个客户端要手动更新known_hosts,运维团队半夜收到一堆“无法连接”的告警。解决办法是升级前把/etc/ssh/ssh_host_*全部备份,升级后原样放回,确保host key不改变。

第三个坑是SELinux上下文问题。某发行版自带SELinux,升级OpenSSH后,新文件可能没有正确的SELinux标签,sshd进程就会在监听的瞬间被SELinux拦截,而systemd报告的服务状态偏偏还是active。这种状态非常迷惑人,A同学当时排查了很久,最后才在/var/log/audit/audit.log里发现avc拒绝记录。从那以后,我在Playbook里专门加了一个恢复SELinux上下文的task,把这个隐患直接干掉了。

第四个坑是批量执行时个别机器系统版本差异导致的repo冲突。有些机器的release版本较老,本地仓库里的某些依赖包版本高于它原本的基准版本,yum解析时可能挑出一个半新不旧的组合,导致升级后某些组件版本错位。现在的做法是Inventory里按系统版本再细分一层,不同版本用不同的本地仓库,哪怕麻烦一点,也比升级后出幺蛾子强。

最后分享一个小技巧:升级完OpenSSH之后,别急着删本地Yum仓库和Playbook。过半个月,你大概率会因为其他安全加固需求再次用到这套离线分发链路。把rpm包、Playbook、repo文件归档到一个固定目录里,写一份简单的README记录操作要点,下次直接复用,省时省力。我自己现在就有一个名为“离线安全升级”的目录,里面存放了所有历史升级用的物料和记录,遇到同类问题第一时间翻它,而不是重新踩一遍坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询