资讯动态

Kylin Server V10 OpenSSH 离线修复脚本实战

发布时间:2026/10/27 14:09:33 来源:尧图企业网站定制
1. 先搞清楚为什么这类活非得自己写脚本Kylin Server V10 下做 OpenSSH 安全漏洞的离线修复这几年几乎成了内网运维的固定动作。我接触过的场景高度相似一批跑在隔离网里的服务器装的是 Kylin Server V10系统上跑着 OpenSSH某天安全扫描报告出来一串 SSH 相关的高中危条目要求限期闭环。机器不能出网不能连外网源甚至很多环境连内网的 yum 源都没有同步过新包。这时候你要做的不是点一下升级而是把离线包、依赖、配置、回滚、验证这一整套东西做成一个能在几十上百台机器上重复执行的离线修复脚本。这件事的价值点在于它不是一次性的手工操作而是一个可复现、可审计、可回滚的交付物。适合谁看我把它分成三类人。第一类是手里有几台到几十台内网服务器、被扫描报告追着跑的运维同学你需要的是能直接改改就用的脚本骨架。第二类是做等保整改、交付验收的工程同学你需要的是能说清楚我做了什么、依据是什么、失败了怎么办的流程。第三类是刚接触 Linux 加固、想知道升级 OpenSSH 到底会踩哪些坑的新手你可以把这篇当成一份带注释的操作手册。我先把结论摆出来绝大多数情况下正确的做法是用官方签名的离线 RPM 包做原地升级而不是源码编译覆盖系统自带的 OpenSSH。原因后面会展开讲但你要先建立这个判断因为它直接决定了脚本的形态。源码编译在离线环境里听起来可控实际上是给自己挖坑——依赖链、PAM 接线、SELinux 上下文、后续补丁管理每一项都可能让你在半夜两点还连不上机器。还有一点得提前说明任何涉及 sshd 的操作本质都是在给自己的远程登录通道动手术。脚本写得再漂亮只要中间有一个环节让 sshd 起不来你就从修漏洞变成救火。所以这篇里我会反复强调带外通道、灰度、看门狗这几件事它们不是可选项是前置条件。1.1 在线升级这条路为什么在内网里基本走不通很多人第一反应是yum update openssh在有网环境里这确实是最省事的路子。但在 Kylin V10 的隔离网场景里这条路会连续撞上三堵墙。第一堵墙是软件源不可达。系统默认的 repo 指向外部地址隔离网里解析不了也连不上命令会卡在正在尝试其他镜像上很久最后报一堆超时。有些人会改 repo 指向内网镜像站但如果内网镜像站没有同步过对应版本的更新包结果一样。第二堵墙是版本锁定策略。不少单位对生产环境有明确的版本基线不允许整包大版本跳变只允许打安全补丁。这时候你需要精确知道目标版本号、需要哪几个子包、依赖哪些库而不是让 yum 自己挑一个最新。第三堵墙是操作不可追溯。yum 一条命令下去装了哪些包、覆盖了哪些配置、产生了哪些 .rpmnew 文件全靠事后翻/var/log/yum.log猜。而在验收场景里你需要的是脚本自己产出一份能贴进报告的日志。所以离线修复脚本的第一个定位就很清楚了把包从哪来、装了哪些、配置改成什么样、失败怎么退回这四个问题全部固化进代码里。1.2 脚本化修复要解决的三个真实痛点我总结下来手工做这件事最容易翻车的地方有三个脚本设计就是围绕它们展开的。痛点一升级完 sshd 起不来人已经断线了。这个最要命。OpenSSH 升级会替换/usr/sbin/sshd二进制替换完必须重启服务才生效。而重启的瞬间你这台机器上的 SSH 会话如果依赖这个进程就可能直接断掉而且新进程如果因为配置错误起不来你就彻底进不去了。脚本必须解决重启后自动验证、失败自动回滚这个问题。痛点二配置文件被悄悄改掉。RPM 升级时如果/etc/ssh/sshd_config被本地改过新包里的同名文件不会直接覆盖而是生成.rpmnew。反过来如果包内配置有变化而你本地没改过旧文件会变成.rpmsave。很多人升级完发现怎么 root 又能密码登录了就是新配置文件顶掉了原来的加固项。痛点三老客户端连不上了。OpenSSH 8.8 之后默认禁用了ssh-rsaSHA-1签名算法一批老旧的运维工具、老版本客户端、某些网络设备的 SSH 模块会直接握手失败报no matching host key type found。这个问题的排查成本极高因为服务端日志只写一句无法协商看不出是算法问题。脚本要在这三点上给出明确处理重启前做配置语法检查、重启后做端口探活、失败自动还原备份升级前比对新旧配置、自动合并加固项升级前评估客户端兼容性、必要时显式放开算法白名单并记录在案。1.3 三条技术路线的取舍我为什么选 RPM 原地升级摆在面前的路其实有三条我按推荐度从高到低说。路线一官方离线 RPM 包原地升级。拿到与系统基线匹配的 openssh、openssh-server、openssh-clients、openssh-askpass 等一组包用rpm -Uvh或本地 repo 方式升级。优点是保持包管理体系完整后续还能被包管理器识别、能被扫描器正确读到版本号PAM 和 SELinux 上下文由包脚本自动处理。缺点是必须拿到签名正确的包且依赖要凑齐。路线二源码编译替换。下载源码包./configure --prefix/usr --sysconfdir/etc/ssh --with-pam --with-selinux编译安装。优点是版本自由、可以精细控制编译选项。缺点是它绕过了包管理器rpm -q openssh依然显示旧版本扫描器照样报警升级时还得手工处理 PAM 模块路径、systemd 单元文件、主机密钥权限一旦出问题卸载极麻烦。路线三只做配置层加固。不改二进制只收紧 sshd_config、限制来源、改端口、上密钥认证。优点是零风险、可立即回滚。缺点也很明显扫描器报的是二进制版本漏洞配置改得再严版本号不变报告关不掉。我的实际做法是以路线一为主路线三作为兜底和补充。也就是说脚本主干是离线包升级同时把配置加固项作为独立模块这样即使某台机器因为依赖问题升不上去也能先落地配置层缓解措施把风险窗口压小。路线二我只在极端情况用——比如确实拿不到匹配的包且机器本身已经是被改造过的系统。这种情况我会在脚本里单独留一个分支并且强制要求带外通道可用才能执行。2. 动手前的盘点版本、依赖、备份一个都不能省写脚本之前先在至少一台同版本机器上把手工流程跑通。这一步省不得因为脚本只是把手工步骤固化下来手工都跑不通的流程写成脚本只会错得更快。2.1 把当前版本信息摸清楚第一件事是确认三样东西操作系统小版本、OpenSSH 版本、OpenSSL 版本。这三个是绑在一起的OpenSSH 依赖 OpenSSL 的 ABI跨版本组合极易出问题。# 系统版本 cat /etc/kylin-release 2/dev/null || cat /etc/os-release uname -r # OpenSSH 版本注意 -V 输出到 stderr ssh -V 21 # 已安装的 ssh 相关包 rpm -qa | grep -i openssh # OpenSSL 版本 openssl version rpm -q openssl openssl-libsssh -V和rpm -q openssh的结果可能不一致这个差异要特别留意如果之前有人做过源码编译覆盖二进制版本会比包版本高这时你再用 RPM 升级会出现降级或者文件冲突。脚本里必须把这两个值都记下来并且做一次一致性判断。我一般会加一条判断如果ssh -V显示的版本高于rpm -q openssh显示的版本直接中止并告警提示该机曾被手工覆盖安装需人工确认。这一条拦下过好几次事故。2.2 依赖关系预判别等 rpm 报错才发现离线升级最常卡住的地方是依赖。rpm -Uvh一旦缺依赖会直接失败而不会像 yum 那样自动去装。所以你要么把依赖全凑齐要么在本地搭一个临时 repo 让 yum 自己解析。凑依赖的方法是在一台与目标机同版本的机器上执行# 查看 openssh-server 的依赖 rpm -qR openssh-server # 更实用的方式用 repoquery 把整棵依赖树导出来 repoquery --requires --resolve --recursive openssh-server | sort -u注意repoquery来自yum-utils如果目标环境没有就用rpm -qR递归手工梳理。核心依赖通常是openssl-libs、krb5-libs、libselinux、pam、zlib、audit-libs、fipscheck-lib这些一般系统自带版本就够但如果你升级的 OpenSSH 跨度较大可能要连带升级openssl-libs这时候就要评估影响面了——openssl-libs是全系统共享库动它等于动一大片必须单独排期。我的原则是能不动共享库就不动。优先找只依赖现有 openssl 版本的 OpenSSH 包组合。如果实在躲不开那这台机器的升级就不该塞进批量脚本里跑单独拎出来做窗口操作。2.3 备份策略别只备份 sshd_config新手最容易犯的错是只把sshd_config复制一份就开干。远远不够。我要求的备份清单是这样的备份对象路径为什么必须备主配置文件/etc/ssh/sshd_config升级可能顶掉加固项配置目录/etc/ssh/sshd_config.d/新版支持 include子配置容易被忽略主机密钥/etc/ssh/ssh_host_*丢了会导致所有客户端指纹告警PAM 配置/etc/pam.d/sshd部分包升级会重置二进制/usr/sbin/sshd、/usr/bin/ssh回滚时的最后一道保险包清单rpm -qa快照回滚时要知道原来装了什么版本服务状态systemctl status sshd记录升级前是否本来就是停的主机密钥单独说一下。它不在 RPM 包管理范围内正常升级不会动它。但如果你的脚本里有一段清理旧配置重装的逻辑误删了/etc/ssh/ssh_host_*后果是所有连过这台机器的客户端都会弹指纹变更警告安全团队会来找你问话。所以脚本里要有一条硬性保护任何删除动作不得涉及/etc/ssh/ssh_host_*。备份目录我习惯放在/var/backups/sshfix/$(date %Y%m%d%H%M%S)/带时间戳方便回滚时选点。备份文件同时生成一个SHA256SUMS回滚前先校验完整性避免备份本身就是坏的。3. 脚本的骨架怎么搭目录、幂等、日志、锁写运维脚本我有一条经验先想清楚失败长什么样再想成功怎么走。下面这套骨架是我反复用过、比较稳的结构。3.1 目录结构与文件清单ssh-offline-fix/ ├── run.sh # 入口参数解析、流程编排 ├── lib/ │ ├── precheck.sh # 环境自检 │ ├── backup.sh # 备份与校验 │ ├── upgrade.sh # 离线包升级 │ ├── harden.sh # 配置加固 │ ├── verify.sh # 升级后验证 │ └── rollback.sh # 回滚 ├── pkgs/ # 离线 RPM 包存放 ├── conf/ │ └── sshd_config.hardened ├── logs/ # 运行日志 └── state/ # 状态标记用于幂等拆成多个文件不是为了好看是为了能单独调用某一段做测试。比如harden.sh可以脱离升级流程单独跑rollback.sh可以在最坏情况下手工执行。单文件大脚本在出事的时候没法局部调试这是血的教训。3.2 幂等设计重复执行不能出事批量脚本一定会被重复执行——有人跑了一半断线了有人不确定跑没跑过又跑一遍。所以每一步都要能判断这件事是不是已经做过了。版本判断是最直接的抓手current_ver$(ssh -V 21 | awk -F[_,] {print $2}) target_ver9.8p1 if [ $current_ver $target_ver ]; then log 当前版本已是 ${target_ver}跳过升级 exit 0 fi但只看版本号不够。有时候版本号对了配置却没落地。所以我在state/目录里放标记文件每一步成功后写一个.done失败写.failed重跑时根据标记决定从哪一步继续。这套机制比全部重来安全得多尤其在批量场景下。3.3 日志、锁和退出码日志这块我要求三件事带时间戳、双输出终端文件、关键动作必须有 before/after 记录。LOG_FILE${BASE_DIR}/logs/fix_$(date %Y%m%d%H%M%S).log log() { local msg[$(date %F %T)] $* echo $msg | tee -a $LOG_FILE }锁是为了防止并发。同一台机器上两个人同时跑脚本会出现一个在升级、另一个在回滚的荒唐局面。用flock最省事exec 200${BASE_DIR}/state/run.lock flock -n 200 || { echo 已有实例在运行退出; exit 1; }退出码要分层方便上层调度系统识别。我的约定是0成功10自检未通过环境不满足未做任何变更20升级失败已自动回滚30升级成功但验证异常40需要人工介入。上层批量工具看到10可以直接跳过看到40就停下来报警这个区分能省掉大量无效重试。4. 核心模块逐个拆从自检到回滚这一节我把几个关键模块的写法和背后的考量讲透你可以按需取用。4.1 环境自检模块宁可误停不要硬上自检模块的作用是在动手之前把所有会导致失败或会导致失联的条件筛掉。我列了下面这些检查项任何一项不过就退出退出码10。precheck() { # 1. root 权限 [ $(id -u) -eq 0 ] || die 需要 root 权限 # 2. 确认是目标系统 grep -qi Kylin /etc/os-release || die 非 Kylin 系统中止 # 3. 根分区剩余空间 ≥ 500MB local avail$(df -Pm / | awk NR2{print $4}) [ $avail -ge 500 ] || die 根分区空间不足${avail}MB # 4. sshd 当前必须是运行状态 systemctl is-active --quiet sshd || die sshd 未运行需人工确认后再执行 # 5. 检查是否存在带外通道或第二个可用通道 #此项无法自动判断由操作人通过参数 --confirm-console 显式声明 # 6. 检查离线包是否齐全 local need(openssh openssh-server openssh-clients) for p in ${need[]}; do ls pkgs/${p}-*.rpm /dev/null 21 || die 缺少离线包${p} done # 7. 包签名校验 rpm --checksig pkgs/*.rpm | grep -q OK || die 离线包签名校验失败 }第 5 项我要多说两句。脚本没法自动判断你有没有带外通道所以必须由人显式声明。我设计成强制参数--confirm-console不传就不给跑。这不是形式主义是逼操作人在动手前确认一次如果失联我怎么办。实际执行中这一条会让人本能地去检查一下 IPMI 或虚拟化控制台能不能登而这个动作本身就避免了很多事故。第 7 项的签名校验也值得展开。离线包在拷贝、转存、U 盘传递的过程中损坏是完全可能的而损坏的包rpm -Uvh可能装到一半失败留下一个半残的 OpenSSH。校验签名和 SHA256 是最低要求脚本里可以再加一步sha256sum -c SHA256SUMS。4.2 升级模块顺序、参数、以及那几个必须的开关升级本身不复杂复杂的是细节。先看代码骨架do_upgrade() { local pkgdir${BASE_DIR}/pkgs cd $pkgdir || die 无法进入包目录 # 按依赖顺序安装server 依赖 clients 和核心包 local order( openssh-*.rpm openssh-clients-*.rpm openssh-server-*.rpm ) for pattern in ${order[]}; do local files( $pattern ) [ -e ${files[0]} ] || continue log 安装: ${files[*]} rpm -Uvh --replacefiles --replacepkgs ${files[]} $LOG_FILE 21 \ || { log 包安装失败; return 1; } done return 0 }几个参数值得解释--replacefiles是处理文件冲突的。如果你之前做过源码编译/usr/sbin/sshd会被包管理识别为被他人修改不加这个参数会报冲突。--replacepkgs是允许重复安装同版本包配合幂等设计用。但要注意--replacefiles是有风险的它可能覆盖掉别的包的文件。所以我一般会在自检阶段就判断是否存在源码编译痕迹如果有直接停下让人工处理而不是靠参数硬扛。升级顺序这个细节很多人忽略。必须先升核心包openssh再升 clients最后升 server。因为 server 的安装脚本会去调新版本的sshd做-t语法检查如果顺序反了可能出现新 server 配老二进制的短暂不一致。虽然概率不高但在批量场景下任何概率不高都会在几百台机器上变成必然发生一次。主机密钥权限是另一个容易漏的点。包升级一般不动主机密钥但如果你做过清理或者之前权限就是错的升级后 sshd 会因为密钥权限过宽比如 0644拒绝启动报bad permissions。脚本里加一条修正chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*_key.pub restorecon -Rv /etc/ssh/ /dev/null 21restorecon那行是修 SELinux 上下文的。在某些强制模式环境下新装的文件上下文不对sshd 一样起不来。这一行成本极低收益很高建议无脑加上。4.3 配置加固模块升级之后必须重新落一遍前面说过RPM 升级会处理配置文件的三向合并结果是你的加固项可能被冲掉。所以升级完成后必须重新执行一遍加固。而且这一步要做成幂等且可审计——每次改动都记录改了什么、依据是什么。我用的是模板 差异合并的思路把加固项写成一个片段文件脚本负责判断每一项目前是什么值不是目标值就改。apply_hardening() { local cfg/etc/ssh/sshd_config local bak${cfg}.bak.$(date %s) cp -a $cfg $bak # keyvalue 形式的加固项 declare -A items( [PermitRootLogin]prohibit-password [PasswordAuthentication]no [PermitEmptyPasswords]no [MaxAuthTries]4 [LoginGraceTime]30 [ClientAliveInterval]300 [ClientAliveCountMax]2 [X11Forwarding]no [AllowAgentForwarding]no [LogLevel]VERBOSE ) for k in ${!items[]}; do local v${items[$k]} if grep -qiE ^[[:space:]]*#?[[:space:]]*${k}[[:space:]] $cfg; then sed -i -E s|^[[:space:]]*#?[[:space:]]*${k}[[:space:]].*|${k} ${v}|I $cfg else echo ${k} ${v} $cfg fi log 加固: ${k} ${v} done # 语法检查不通过立即回滚配置 if ! sshd -t 2$LOG_FILE; then log 配置语法检查失败回滚配置 cp -a $bak $cfg return 1 fi }这里有几个坑要提醒。坑一PasswordAuthentication no之前先确认密钥登录是通的。我在脚本里加了一条前置判断检查/root/.ssh/authorized_keys或目标运维账户的authorized_keys是否存在且非空。如果为空还硬把密码登录关掉等于自己把门焊死了。坑二sshd_config.d/目录的优先级。新版 OpenSSH 支持Include /etc/ssh/sshd_config.d/*.conf而且这个 Include 通常写在文件最前面。OpenSSH 的规则是第一次出现的值生效所以子目录里的配置会覆盖主文件后面的配置。如果你的加固项写在了主文件末尾但子目录里有个文件写了PasswordAuthentication yes那你的加固就没生效。脚本里必须扫一遍这个目录if [ -d /etc/ssh/sshd_config.d ]; then log 检测到 sshd_config.d 目录内容如下 for f in /etc/ssh/sshd_config.d/*.conf; do [ -e $f ] || continue log $f: sed s/^/ / $f $LOG_FILE done fi扫出来之后不要贸然删除先人工看一眼是谁加的。有些云环境或运维平台会往这里塞配置删了会影响别的功能。坑三算法兼容性要显式决定而不是被动踩雷。新版默认的算法集收紧了老客户端连不上时最常见的修法是在服务端放开。但放开多少、放开哪些得有依据。我的做法是升级前先统计现有客户端用的算法# 升级前抓一段时间日志统计实际协商使用的算法 grep -oE kex: algorithm: [^ ] /var/log/secure* 2/dev/null \ | awk {print $NF} | sort | uniq -c | sort -rn如果统计出来全是curve25519-sha256这类新算法那你放心收紧如果出现diffie-hellman-group14-sha1这种说明有老设备在用那就得在配置里针对性放开并在报告里写清楚为什么。这比先升级出问题再放开要体面得多。4.4 重启与看门狗这一段决定你会不会失联前面所有步骤都可以慢慢来只有重启这一段是一锤子买卖。我的方案是延时重启 独立看门狗。思路是这样的不要在脚本主进程里直接systemctl restart sshd而是用一个systemd-run或者在nohup里跑一个延时任务比如 10 秒后执行重启同时这个任务在重启后 30 秒检查端口失败就自动把备份的二进制和配置还原回去。schedule_restart_with_watchdog() { local backup_dir$1 cat /tmp/sshd_watchdog.sh EOF #!/bin/bash BACKUP_DIR$1 LOG/var/log/sshd_watchdog.log sleep 5 systemctl restart sshd sleep 15 if systemctl is-active --quiet sshd \ (echo /dev/tcp/127.0.0.1/22) /dev/null 21; then echo $(date) 重启成功服务正常 $LOG exit 0 fi echo $(date) 重启后检测失败开始回滚 $LOG cp -af ${BACKUP_DIR}/sshd /usr/sbin/sshd cp -af ${BACKUP_DIR}/sshd_config /etc/ssh/sshd_config chmod 600 /etc/ssh/ssh_host_*_key 2/dev/null systemctl restart sshd echo $(date) 回滚完成当前状态$(systemctl is-active sshd) $LOG EOF chmod x /tmp/sshd_watchdog.sh setsid nohup /tmp/sshd_watchdog.sh $backup_dir /dev/null 21 log 已安排延时重启与看门狗10 秒后执行 }这个方案的关键在于重启动作脱离了当前 SSH 会话的进程树。即使你的会话断了看门狗照样跑完要么成功、要么回滚机器总能回到一个能连的状态。有几点必须配合第一端口探测不要只探测 22。如果你改过端口探测目标要跟着改。更稳的做法是解析sshd_config里的Port值再探测。第二探测要重试。sshd 重启后可能需要几秒钟才完全就绪一次失败就回滚会误判。所以是重试 5 次、每次间隔 3 秒全失败才算失败。第三回滚用的二进制一定要在升级之前就备份好。有些人备份配置文件很积极二进制忘了拷出事的时候只能去别处找同版本包时间全耗在这上面。第四如果环境支持最好再叠一层带外兜底——IPMI、虚拟化控制台、串口。脚本里没法自动操作这些但可以在自检阶段强制操作人确认可用。双保险永远比单保险踏实。5. 完整走一遍从拿到包到批量落地前面讲的是零件这一节把它们装起来跑一遍。我以一次典型的批量修复为线索。5.1 离线包的准备与校验包的来源通常是官方渠道或者内部制品库具体渠道按你单位的规范来。拿到之后第一件事不是急着装是在你手上的一台同版本测试机上先装一遍。# 在一台同版本测试机上 mkdir -p /tmp/sshtest cd /tmp/sshtest rpm2cpio openssh-server-*.rpm | cpio -idmv /usr/sbin/sshd -V 21 # 看这个包里的版本对不对rpm2cpio解包看内容是个好习惯可以在不影响系统的情况下确认包里的文件清单和版本。确认无误后再做正式安装测试。包的组织上我建议按版本号建目录不要平铺pkgs/ ├── 9.8p1/ │ ├── openssh-9.8p1-*.rpm │ ├── openssh-server-9.8p1-*.rpm │ ├── openssh-clients-9.8p1-*.rpm │ └── SHA256SUMS └── 9.6p1/ └── ...保留旧版本目录的价值在于回滚。万一新版本在某批机器上出了共性问题你需要降回旧版本这时候手里有旧包能省掉几小时。5.2 单机试跑先看它怎么失败正式跑之前在一台允许重启、允许短暂失联的机器上做完整试跑。试跑时我建议故意制造一次失败验证回滚路径# 故意把 sshd_config 改坏看脚本能不能识别并回滚 echo ThisIsNotAValidDirective yes /etc/ssh/sshd_config ./run.sh --confirm-console # 预期sshd -t 报错脚本回滚配置退出码 20这种主动找失败的测试比一路顺利跑通的测试有价值十倍。顺风顺水的路径只能证明脚本能跑回滚路径才能证明它可信。试跑通过后把这台机器的日志完整留档。批量阶段对比日志能快速发现某几台机器的输出和标准样本不一样这比逐台检查快得多。5.3 批量下发灰度、串行、失败即停批量执行有三条铁律。铁律一按批次灰度。先 1 台再 5 台再 20%最后全量。每一批之间观察至少 30 分钟重点看两件事新连接是否正常、业务侧是否有报错。SSH 挂掉有时不会立刻显现等业务重连失败才暴露那时候可能已经过了几个小时。铁律二串行执行不要并发。同一时间只对一台机器操作。并发看起来快但一旦有问题你会同时面对一堆失联机器排查顺序都理不清。串行虽然慢但每台机器失败你都能立刻停下来看清原因。铁律三失败即停不要跳过继续。这一条最容易被违反。批量工具默认行为往往是记录失败、继续下一台听起来合理实际上等于让一个已知问题扩散。我要求第一台失败就停下查清楚再说。批量下发如果用了自动化工具命令大概是这个样子以常见的批量执行工具为例具体按你手头的工具调整# 事前把脚本目录推到目标机 for host in $(cat hosts.txt); do scp -o BatchModeyes -r ssh-offline-fix/ root${host}:/opt/ 21 done # 串行执行任何一台失败立即退出 while read -r host; do echo 开始处理 ${host} ssh -o BatchModeyes -o ConnectTimeout10 root${host} \ /opt/ssh-offline-fix/run.sh --confirm-console --noninteractive rc$? echo ${host} 退出码: ${rc} if [ $rc -ne 0 ]; then echo 非零退出停止批量 break fi done hosts.txt--noninteractive这个参数是给批量模式用的作用是去掉所有需要人确认的交互提示。但要注意--confirm-console这个确认在批量模式下不能省应该由操作人在批量开始前统一确认一次通过一个总开关传递下去而不是每台机器弹一次。5.4 验证清单怎么算修好了升级完不能只看ssh -V。我用的验证清单是这样的验证项命令期望结果二进制版本ssh -V目标版本包版本一致rpm -q openssh与二进制版本一致服务状态systemctl is-active sshdactive配置语法sshd -t无输出端口监听ss -tlnp | grep sshd监听预期端口本机回环连接ssh -o BatchModeyes -o StrictHostKeyCheckingno localhost true返回 0加固项生效sshd -T | grep -i passwordauthno主机密钥权限ls -l /etc/ssh/ssh_host_*_key600日志无异常journalctl -u sshd --since 10 min ago无 errorsshd -T这条命令特别好用它会把最终生效的配置全部打印出来——注意是最终生效的已经处理过 Include 和优先级。比起去文件里 grep这个结果可信得多。加固项是否真的生效看这个输出最准。6. 踩坑记录这些坑我都替你踩过了下面这些问题都是我在实际批量执行里遇到过的按出现频率排序。6.1 高频问题速查表现象根本原因处理方式升级后重启失败sshd 起不来配置语法错误或主机密钥权限异常看门狗自动回滚检查sshd -t输出老客户端报no matching host key type新版默认禁用ssh-rsa(SHA-1)视情况放开HostkeyAlgorithms ssh-rsa并记录原因升级后密码登录又能用了.rpmnew顶掉加固配置升级后强制重跑加固模块rpm -Uvh报依赖缺失离线包依赖未凑齐补齐依赖或改用本地 repo 方式rpm -q openssh版本没变但ssh -V变了曾源码编译覆盖自检阶段拦截人工处理日志里Connection closed by authenticating user激增客户端认证算法被拒检查PubkeyAcceptedAlgorithms配置批量执行中途大量失败并发执行或包不完整改串行逐台确认SELinux 环境下 sshd 无法启动文件上下文被破坏restorecon -Rv /etc/ssh /usr/sbin/sshd6.2 几个特别值得展开的坑.rpmnew和.rpmsave的处理。升级完一定要扫一遍这两个后缀的文件find /etc/ssh -name *.rpmnew -o -name *.rpmsavesshd_config.rpmnew表示你的配置被保留了新版本的配置放在这里你需要人工比对把新增的有价值的选项合并进去。sshd_config.rpmsave表示新版本配置已生效你的旧配置被备份了这种情况要特别警惕因为你的加固项可能已经失效。我的脚本里只要扫到这两个文件就强制重跑加固模块并在日志里高亮标出。PAM 配置被重置。sshd的 PAM 配置在/etc/pam.d/sshd个别包升级会重置它。重置后可能出现密钥能登、密码登不了或者反过来的诡异现象。脚本里把 PAM 配置也纳入备份和比对范围升级后做一次 diffdiff /var/backups/sshfix/${TS}/pam_sshd /etc/pam.d/sshd $LOG_FILE 21有差异就记下来人工判断是否要还原。老旧设备的算法兼容。这个问题前面提过但要再强调一次处理方式不要一上来就全局放开而是按来源限制。OpenSSH 支持在Match块里针对特定来源做差异化配置Match Address 192.168.10.0/24 PubkeyAcceptedAlgorithms ssh-rsa HostkeyAlgorithms ssh-rsa这样既解决了老设备的问题又不至于把整个服务端的算法强度拉低。这个做法在等保验收时也更好解释——我们做了精准放开而不是全面降级。MaxStartups和握手风暴。批量机器在同一时间点集中重连可能触发MaxStartups限制默认 10:30:100表现为部分客户端随机连接失败。如果你是在业务高峰期做的升级很可能撞上这个。脚本里可以把这项适当调高或者更稳妥的做法是错开升级时间窗MaxStartups 30:30:200日志文件被轮转导致排查断档。升级过程中如果正好赶上logrotate/var/log/secure被切走你的排查线索就断了。脚本里可以在动手前先做一次日志快照cp -a /var/log/secure ${BACKUP_DIR}/secure.snapshot 2/dev/null || true journalctl -u sshd --since -1 hour ${BACKUP_DIR}/sshd_journal.log 2/dev/null || true这两行成本几乎为零但出事的时候是救命的东西。7. 我个人的几条实操心得写到这里流程和代码都差不多了最后分享几条不那么技术、但实际影响很大的经验。第一条永远在动手前确认你还有第二条路进去。我现在的习惯是每次要动 sshd 之前先开一个独立的 SSH 会话挂着不关同时确认带外通道能登。这两个条件不满足我就不动。听起来麻烦但它把失联这件事从灾难降级成了麻烦性质完全不同。第二条脚本的日志是交付物的一部分不是副产品。很多人写完脚本就不管日志了输出一堆xxx ok。等到要写整改报告、要跟安全团队解释你到底改了什么就开始翻历史命令。我现在的做法是脚本每次运行产出一个结构化的摘要段包含升级前后版本、改动的配置项及其新旧值、执行时间、退出码、验证结果。这段摘要直接可以贴进报告省掉大量事后整理。第三条别追求一次脚本解决所有问题。我见过有人想写一个脚本同时处理 OpenSSH 升级、系统补丁、配置加固、账号清理最后这个脚本谁也不敢跑。我的做法是拆开这个脚本只做 OpenSSH 相关的离线修复其他事情各归各的脚本。职责单一出问题时能立刻定位是哪一段也不会因为一个无关模块的 bug 阻塞整条链路。第四条把回滚当成一等公民来写。大多数脚本里回滚是最后补的几行代码简单粗暴。但真正出事的时候你依赖的恰恰是这几行。我的要求是回滚逻辑要独立成文件、要能单独调用、要有自己的日志、要经过至少一次真实的失败演练。前面说的故意改坏配置看它能不能回滚就是这个意思。第五条版本跨度大的时候不要硬上。如果要从很老的版本一路跳到最新版中间隔了好几个大版本别指望一个脚本一次到位。更稳的做法是分两步先升到一个中间稳定版跑一段时间确认没问题再往上升。每步之间留观察期。虽然慢但在生产环境里慢就是快。这套东西我用了几年从几台机器做到上百台脚本改了很多版但骨架一直没变自检拦住不该跑的、备份保证退得回、看门狗保证不失联、日志保证说得清。把这四件事做到位剩下那些参数和命令都是可以现查的细节。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑