OpenSSH 7.4p 这个版本号,干过 CentOS 7 运维的人基本都眼熟——系统装完打眼一看ssh -V,出来的就是OpenSSH_7.4p1, OpenSSL 1.0.2k-fips。这个组合在当年很稳,但架不住时间久了,安全基线扫描一跑,一堆中高危全指着它。更麻烦的是 7.4p 里还留着ssh-rsa(SHA-1 签名)、diffie-hellman-group1-sha1这些早就被认为不安全的算法,放在内网还好说,只要机器对外暴露过端口,日志里天天有人拿着字典撞你的 22 端口。
所以把 OpenSSH 从 7.4p 拉到 9.0p,本质上不是"升级",而是一次"换血"。它不是yum update openssh一条命令就完事的操作,因为 CentOS 7 官方源里最高就给到 7.4p1,想要 9.0 只能自己编译或者找第三方 RPM 包。而编译安装这事,踩坑的地方远比敲命令的地方多:OpenSSL 版本不够、PAM 没编进去导致密码登录全挂、Subsystem sftp路径还指着老文件、systemd 的Type=notify让服务起不来、SELinux 上下文不对把 sshd 直接拦死……
这篇内容就是把我自己在一批 CentOS 7 / 类 CentOS 7 机器上把 OpenSSH 7.4p1 升到 9.0p1 的整个过程、参数选择理由、连不上的排查套路,以及最终的回滚方案完整摊开讲一遍。适合手上有一批老系统、又必须过安全合规检查的运维同学,也适合想搞明白"源码编译替换系统组件"这套玩法的人。整篇按"评估—准备—编译—配置—验证—批量"的顺序推进,每一步我都会说清楚为什么这么做,而不是只丢一条命令。
1. 升级前的整体评估与方案选型
1.1 为什么 7.4p 必须动:漏洞和算法两线夹击
先说清楚驱动力。7.4p1 是 2016 年底发布的版本,到 9.0p1 之间隔了差不多五年半,中间修掉的安全问题数量相当可观。其中最出名的一类是认证绕过和整数溢出,比如CVE-2018-15473那个用户名枚举问题,攻击者可以通过响应时间差异判断某个账号到底存不存在,给后续爆破省了一大半力气。再比如CVE-2020-15778,scp的参数处理可以被注入命令,只要服务端允许 scp,配合某些场景就能执行非预期指令。
但比单个 CVE 更麻烦的是算法层面。7.4p1 的默认算法集里,ssh-rsa用的还是 SHA-1 签名,SHA-1 的碰撞攻击在学术上早就实锤了;diffie-hellman-group1-sha1这种 1024 位群的密钥交换,算力稍微宽裕一点就能被拿下。等保测评、行业安全基线、甲方自己的扫描报告,基本上都会把这几条列成高危项。你可以手工在sshd_config里把算法白名单收紧来规避,但那相当于在旧地基上不断打补丁,越补越乱,最终还是得换版本。
还有一个容易被忽略的点:新版本带来的不只是安全,还有可用性。9.0p1 默认启用了sntrup761x25519-sha512@openssh.com这个抗量子计算的混合密钥交换,虽然现在谈量子威胁有点早,但它在握手性能上并不吃亏;另外scp在 9.0 里默认改走 SFTP 协议,传输大目录时对断点、权限的处理比老的 rcp 协议干净得多。
注意:升级理由要写清楚,最好在变更单里明确列出"修复高危漏洞"和"禁用弱算法"两条,不要只写"升级到最新版"。前者是合规语言,后者是技术语言,两边都站得住。
1.2 三条升级路线的取舍:yum 源、第三方 RPM、源码编译
真正动手前,路线选择比技术细节更重要。我整理过三条路,各自的适用场景差别很大。
第一条路是换 yum 源。有些第三方源确实提供了较新版本的 openssh RPM,安装体验最好,yum install一条命令,依赖自动解决,回滚也简单。但问题在于:这类源的可信度参差不齐,包里的编译参数你无法确认,而且一旦这个源哪天不维护了,后续再升级就断了。对于生产环境,尤其是要过合规审计的环境,用来源不明的二进制包替换核心登录组件,风险比收益大。
第二条路是自己打 RPM 包。从源码编译出二进制,再用 rpmbuild 或 fpm 打成 RPM,最后rpm -Uvh安装。这条路兼顾了可控性和可管理性:编译参数是你自己定的,包是你自己签的,升级和回滚都走标准包管理流程,rpm -qa能查到版本,后面批量分发也方便。缺点是要多花时间写 spec 文件,第一次搭起来有点折腾。
第三条路是纯源码编译覆盖安装。直接在机器上./configure && make && make install,把二进制覆盖到/usr/sbin/sshd、/usr/bin/ssh这些位置。最快、最直接,适合机器数量少、或者没有内网 yum 仓库的场景。但回滚只能靠提前备份的二进制,没有包管理记录。
我自己的选择是:小规模(十台以内)用源码编译,中大规模先打一个 RPM 再用 Ansible 分发。原因很实际——十台以内手工搞一遍也就半天,为它单独搭 RPM 构建环境不划算;但机器一多,手工编译时任何一台的参数敲错都可能导致差异,用统一的 RPM 才能保证环境一致性。
1.3 动手前的环境摸底清单
不管走哪条路,先摸底。我吃过一次亏:在一台机器上编译到一半才发现 gcc 版本太老,configure直接报错退出,之前装的依赖全白装。所以下面这张表我基本每次都跑一遍。
| 检查项 | 命令 | 期望结果 | 不满足时的处理 |
|---|---|---|---|
| 当前版本 | ssh -V | OpenSSH_7.4p1 | 记录,作为回滚基线 |
| 系统版本 | cat /etc/redhat-release | CentOS 7.x | 确认包管理方式 |
| OpenSSL 版本 | openssl version | 1.0.2k 或更高 | 低于 1.1.1 时需单独编译 |
| gcc 版本 | gcc --version | 4.8.5 以上 | 升级或换构建机 |
| PAM 开发库 | rpm -q pam-devel | 已安装 | yum install -y pam-devel |
| zlib 开发库 | rpm -q zlib-devel | 已安装 | yum install -y zlib-devel |
| SSH 端口 | ss -lntp | grep sshd | 22 或自定义端口 | 记录,别改错端口 |
| SELinux 状态 | getenforce | Enforcing / Permissive | Enforcing 时准备好 restorecon |
| 控制台通道 | 云厂商 VNC / IDRAC / IPMI | 可用 | 没这个别开始升级 |
最后一行是我最看重的。升级 OpenSSH 有一个铁律:必须有一条不依赖 SSH 的登录通道。云主机用厂商控制台的 VNC,物理机用带外管理,虚拟机用宿主机的 console。原因很简单——你在升级 sshd 的过程中,任何一步出错都会导致 SSH 登录不上,如果这时候你只有 SSH 这一条路,那这台机器就彻底失联了,只能走工单让机房的人重启进救援模式。
2. 动手前的准备:备份与依赖
2.1 回滚预案要落实到命令级别
很多人做变更时的"备份"就是cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak,这远远不够。源码编译安装会覆盖至少四个二进制文件和一批头文件、库文件,回滚必须能把这些都还原回去。
我习惯在/root下建一个固定目录,把所有需要还原的东西按原路径结构存好:
mkdir -p /root/ssh_rollback_$(date +%Y%m%d) cd /root/ssh_rollback_$(date +%Y%m%d) # 配置目录整体备份(保留权限和时间戳) cp -a /etc/ssh ./etc_ssh # 二进制备份 cp -a /usr/sbin/sshd ./sshd.bin cp -a /usr/bin/ssh ./ssh.bin cp -a /usr/bin/scp ./scp.bin cp -a /usr/bin/ssh-keygen ./ssh-keygen.bin # 记录包信息,方便必要时用 yum reinstall 恢复 rpm -qa | grep -i openssh > openssh_packages.txt rpm -ql openssh-server > openssh_server_files.txt # 记录当前算法协商结果,作为对比基线 ssh -Q kex > kex_before.txt ssh -Q key > key_before.txt ssh -Q cipher > cipher_before.txtssh -Q这几个子命令特别有用,它能把当前版本支持的算法族全列出来。升级前后各存一份,对比一下就知道哪些算法没了,后面配兼容性白名单时心里有数。
回滚时的操作就是把备份的文件拷回去:
cp -a /root/ssh_rollback_YYYYMMDD/sshd.bin /usr/sbin/sshd cp -a /root/ssh_rollback_YYYYMMDD/etc_ssh/* /etc/ssh/ restorecon -Rv /etc/ssh systemctl restart sshd注意:备份二进制时一定要用
cp -a保留原来的 owner 和权限。/usr/sbin/sshd是 root:root 755,如果拷回来变成别的权限,sshd 会因为安全校验直接拒绝启动。
2.2 依赖包和工具链准备
CentOS 7 的默认最小化安装缺不少东西,编译 OpenSSH 之前先把下面这些装上:
yum install -y gcc gcc-c++ make perl \ zlib-devel pam-devel \ openssl-devel \ wget tar \ rpm-build # 如果要打 RPM 包其中zlib-devel和pam-devel是两个必装项,而且必须在configure之前装好。原因在于 OpenSSH 的configure脚本是"探测式"的——它发现不到pam相关头文件,就会静默地把 PAM 支持关掉,编译过程不会报任何错,一路绿灯到make install完成。等你重启 sshd 准备试密码登录时,才会看到Permission denied,日志里写着PAM: none或者干脆不提 PAM。这时候你只能重装 pam-devel 然后重新编译,白折腾一轮。
openssl-devel是系统自带的那套,版本是 1.0.2k。这里有个关键决策点:要不要单独编译 OpenSSL 1.1.1?
我的建议是分开看:
- 如果你只升级到 9.0p1,官方给出的最低要求是 OpenSSL 1.0.1 以上,系统自带的 1.0.2k 在
configure阶段是能通过的,可以先用系统版本编译。 - 但如果你后续还要往 9.4、9.6 这些版本走,它们对 OpenSSL 的要求提到了 1.1.1 以上,那时候系统自带的 1.0.2k 就不够用了。所以如果时间允许,我建议一次性把 OpenSSL 1.1.1 也编译好,放到
/usr/local/ssl,后面升级 OpenSSH 就是纯增量操作。
单独编译 OpenSSL 时注意一点:只装到/usr/local/ssl,绝对不要覆盖系统自带的/usr/lib64/libssl.so.10和/usr/lib64/libcrypto.so.10。系统里 yum、curl、rpm 这些工具全依赖老版本,一旦被覆盖,整个包管理系统可能直接罢工,那台机器就只能重装了。
tar -zxf openssl-1.1.1w.tar.gz -C /usr/local/src cd /usr/local/src/openssl-1.1.1w ./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib make -j$(nproc) make install # 让新库能被找到,但不动系统老库 echo "/usr/local/ssl/lib" > /etc/ld.so.conf.d/openssl-111.conf ldconfig--openssldir=/usr/local/ssl这个参数容易被漏掉。不写的话,默认会指向编译目录,将来openssl命令找配置文件openssl.cnf时会找不到,导致某些证书操作报错。
2.3 关于 host key 的一个冷知识
/etc/ssh/ssh_host_*_key这些主机密钥,升级过程中不要动。有些人为了"干净"会把它们删掉让新版本重新生成,这会导致一个直接后果:所有客户端的known_hosts里的指纹全部失效,下次连接报一大段WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!,几台机器还好说,几百台客户端逐个ssh-keygen -R就够你受的。
只有一种情况需要重新生成:你的老主机密钥里包含 DSA 类型。OpenSSH 9.0 已经完全移除对 DSA 的支持,如果/etc/ssh/下存在ssh_host_dsa_key,新的 sshd 读取时会报错并拒绝启动。处理方式很简单:
ls -l /etc/ssh/ssh_host_*_key # 如果有 ssh_host_dsa_key,先移走 mv /etc/ssh/ssh_host_dsa_key /root/ssh_rollback_backup/ mv /etc/ssh/ssh_host_dsa_key.pub /root/ssh_rollback_backup/移走之后不用重新生成,因为ssh_host_rsa_key、ssh_host_ecdsa_key、ssh_host_ed25519_key这三对还在,9.0 会正常使用它们。
3. 源码编译安装 9.0p1 全流程
3.1 源码包的下载与校验
官方发布包在 OpenSSH 官网和各大开源镜像站都能找到。包名是openssh-9.0p1.tar.gz,注意那个p1后缀,它表示这是 9.0 系列的第一个补丁版本,别把p1漏掉只下载openssh-9.0.tar.gz,那个包在官方列表里是不存在的。
下载之后强烈建议校验,尤其是从镜像站拉的时候:
cd /usr/local/src wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.0p1.tar.gz wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.0p1.tar.gz.asc # 计算 SHA256,和官网公布的对比 sha256sum openssh-9.0p1.tar.gz校验这一步很多人跳,我觉得在升级核心登录组件这事上不值得省。如果内网有自建的文件服务器,把源码包放上去,所有机器从同一份文件拉,能避免不同机器下载到不同版本或者被中间篡改。
解压:
tar -zxf openssh-9.0p1.tar.gz cd openssh-9.0p13.2 configure 参数逐项拆解
configure是整个编译过程里最需要动脑的一步。参数写错,后面要么功能缺失,要么路径错乱。我用的这套参数是在多个环境验证过的:
./configure \ --prefix=/usr \ --sysconfdir=/etc/ssh \ --with-pam \ --with-zlib \ --with-ssl-dir=/usr/local/ssl \ --with-privsep-path=/var/lib/sshd \ --with-md5-passwords \ --with-ssl-engine \ --with-selinux逐条说明为什么这么选:
--prefix=/usr是最关键的一个。CentOS 7 的sshd.service里写死了ExecStart=/usr/sbin/sshd -D $OPTIONS,ssh、scp、sftp这些客户端命令也都在/usr/bin/下。如果prefix用默认的/usr/local,那么编译出来的 sshd 会装到/usr/local/sbin/sshd,而 systemd 还是去/usr/sbin/sshd找,结果就是systemctl start sshd报status=203/EXEC,找不到可执行文件。把 prefix 设成/usr,编译产物正好覆盖掉老版本,systemd 不用改一行配置。
--sysconfdir=/etc/ssh让配置文件路径和系统默认保持一致。默认的 sysconfdir 是$prefix/etc,也就是/usr/etc/ssh,这个路径既不符合 CentOS 习惯,也会让现有的/etc/ssh/sshd_config被忽略。设成/etc/ssh之后,新 sshd 直接读你原来的配置,改动量最小。
--with-pam必须加。CentOS 的密码认证、账号锁定、登录审计全走 PAM 模块。不加这个参数编译出来的 sshd 不支持 PAM,PasswordAuthentication yes形同虚设,所有用密码登录的用户都会被拒。前面说过,这个问题在编译阶段完全静默,只有实际登录时才暴露。
--with-zlib打开压缩支持。默认情况下 configure 会自动探测 zlib,但显式写上更保险,尤其在zlib-devel装得比较晚的情况下。
--with-ssl-dir=/usr/local/ssl指定 OpenSSL 路径。如果你决定用系统自带的 1.0.2k,把这个参数去掉。用独立编译的 1.1.1w 就按这个写。注意这里填的是前缀目录,不是 lib 目录。
--with-privsep-path=/var/lib/sshd是指定权限分离用的空目录。OpenSSH 在 7.x 之后默认用/var/empty,但某些系统上/var/empty的权限被改过(比如被别的软件弄成了 755),sshd 启动时会因为"权限不满足安全要求"而失败。改成/var/lib/sshd更可控:
mkdir -p /var/lib/sshd chmod 711 /var/lib/sshd chown root:root /var/lib/sshd权限 711 是硬性要求,sshd 会检查这个目录的属主和权限,太宽松直接拒绝启动。
--with-md5-passwords是为了兼容老密码哈希。有些历史遗留账号的密码用的是 MD5-crypt 格式,不加这个参数的话这些账号会登录失败。
--with-selinux在 SELinux 处于 Enforcing 的系统上建议加上。它让 sshd 在运行期能正确处理 SELinux 上下文,减少权限拒绝。
configure跑完后,终端最后几行会有一个汇总。这一步一定要看完,不要直接拉到最后敲 make。重点检查三个字段:PAM 是不是yes,OpenSSL 版本是不是你期望的那个,PrivSep 路径对不对。我见过configure因为找不到 OpenSSL 头文件而静默降级使用内置实现的情况,最后登录时各种加密套件不匹配。
3.3 编译、安装与二进制替换
configure通过之后就顺了:
make -j$(nproc)编译大概几分钟,中间如果有 warning 一般可以忽略,但如果出现 error,基本都是依赖没装全。常见的两个:
fatal error: openssl/opensslv.h: No such file—— 缺openssl-devel,或者--with-ssl-dir路径写错。fatal error: security/pam_appl.h—— 缺pam-devel。
编译完之后,我强烈建议先跑一次完整的自检再安装:
make tests这套测试会本地起一个临时 sshd,验证密钥交换、认证、文件传输这些核心链路。跑一遍大概几十秒,能提前发现问题。特别是在跨大版本升级时,它能帮你确认新编译的二进制本身是健全的。
确认没问题再安装:
make install安装过程会自动做几件事,其中一件是生成新的 host key——但前提是/etc/ssh/下不存在同名文件。因为我们已经有了老密钥,这一步实际上会被跳过,密钥保持不散,客户端的known_hosts不受影响。
安装完之后立刻验证二进制版本:
/usr/sbin/sshd -V # 期望输出:OpenSSH_9.0p1, OpenSSL 1.1.1w ... which ssh scp ssh-keygen sftp ssh -V如果sshd -V报unknown option,说明/usr/sbin/sshd还是老版本,make install没覆盖成功。检查一下是不是prefix写错了,或者安装时有权限报错被忽略了。
3.4 补齐 sftp-server 路径和权限
这一步是 9.0 升级里最容易被漏掉的坑。/etc/ssh/sshd_config里通常有这么一行:
Subsystem sftp /usr/libexec/openssh/sftp-server这是 CentOS 打 RPM 包时的路径,sftp-server被放在/usr/libexec/openssh/下。而源码编译安装的sftp-server默认装到/usr/libexec/sftp-server,两个路径对不上。结果就是 SSH 登录正常,但一执行sftp或者用支持 SFTP 的客户端(比如 WinSCP、FileZilla)就报subsystem request failed on channel 0。
两个解决办法,选一个就行:
# 方法一:改配置指向新路径 sed -i 's#Subsystem\s\+sftp\s\+/usr/libexec/openssh/sftp-server#Subsystem sftp /usr/libexec/sftp-server#' /etc/ssh/sshd_config # 方法二:建个软链接,老路径继续有效 ln -sf /usr/libexec/sftp-server /usr/libexec/openssh/sftp-server我更倾向方法一,因为 9.0 的scp默认走 SFTP 协议,sftp-server的路径正确与否直接影响 scp 能否工作。改配置的时候顺手确认一下新装的sftp-server确实在:
ls -l /usr/libexec/sftp-server顺手还要检查/var/lib/sshd权限、/etc/ssh下密钥文件权限:
chmod 600 /etc/ssh/ssh_host_*_key chmod 644 /etc/ssh/ssh_host_*_key.pub chown root:root /etc/ssh/ssh_host_*4. sshd_config 配置改造与算法兼容
4.1 9.0p1 到底弃用了哪些算法
配置改造前先把账算清楚。从 7.4p1 到 9.0p1,被默认关掉或者彻底移除的算法主要是这几类:
| 类别 | 被弃用/移除的算法 | 生效版本 | 影响 |
|---|---|---|---|
| 主机密钥 | ssh-dss | 9.0 完全移除 | 老 DSA 主机密钥无法加载 |
| 签名算法 | ssh-rsa(SHA-1) | 8.8 默认禁用 | 老客户端公钥认证失败 |
| 密钥交换 | diffie-hellman-group1-sha1 | 7.6 起默认禁用 | 老设备协商失败 |
| 密钥交换 | diffie-hellman-group14-sha1 | 8.x 起降级 | 部分老客户端不兼容 |
| 加密 | 3des-cbc、blowfish-cbc、cast128-cbc | 7.x 起默认禁用 | 老客户端无法连接 |
| MAC | hmac-ripemd160、umac-64@openssh.com | 7.x 起默认禁用 | 老客户端协商失败 |
这里最需要关注的是ssh-rsa。很多企业的自动化脚本、老款网络设备、甚至一些老版本的 Java 客户端(比如 JSch),用的还是ssh-rsa做公钥签名。升级到 9.0p1 之后,服务端默认不接受这种签名,客户端连接时会看到:
Unable to negotiate with 10.0.0.1 port 22: no matching host key type found. Their offer: ssh-rsa或者:
Permission denied (publickey).注意第二种情况特别隐蔽——协商阶段过了,但认证阶段被拒,日志里看不出明显的算法错误。区分方法是在客户端加-vvv看详细日志,搜索send_pubkey_test附近的内容,如果出现using ssh-rsa然后紧跟着Authentications that can continue: publickey,基本就是这个问题。
4.2 一个稳妥的服务端配置模板
我的做法是:默认算法全部收紧,只为确实需要的老客户端开最小白名单。直接把ssh-rsa全局打开是不可取的,那等于把安全升级的意义抵消了一半。
# 备份原配置 cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d) # 显式声明算法白名单 cat >> /etc/ssh/sshd_config <<'EOF' # ===== 9.0p1 算法加固配置 ===== # 密钥交换:只要现代算法 KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,sntrup761x25519-sha512@openssh.com # 主机密钥算法:去掉 ssh-rsa HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256 # 加密套件 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr # MAC MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com # 公钥认证算法 PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256 EOF如果确实有老客户端必须用ssh-rsa,用+前缀追加而不是替换:
HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa+表示"在默认集合基础上追加",-表示"从默认集合里移除",直接写算法列表则是"完全替换"。搞清这三个语义能省很多事——直接写列表容易漏掉某个必要算法,导致自己都连不上。
另外提醒一下,PubkeyAcceptedAlgorithms在 8.5 之前的名字叫PubkeyAcceptedKeyTypes。9.0p1 里老名字还能作为别名使用,但会有 warning。如果你在配置里看到老名字,顺手换成新的。
4.3 客户端侧的老设备兼容处理
服务端收紧之后,客户端侧也要跟着调整。对于 Linux 客户端,最省事的办法是在~/.ssh/config里针对特定主机做例外:
Host old-switch-10.0.0.88 HostKeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa KexAlgorithms +diffie-hellman-group14-sha1 Ciphers +aes128-cbc这样做的好处是:只有连这台老设备时才放宽,连其他主机的安全策略不受影响。比在全局配置里放开要干净得多。
Windows 上如果用 PuTTY,它的新版已经支持rsa-sha2-256和rsa-sha2-512,一般不用特殊处理;如果是很老的版本,需要升级客户端。用 WinSCP 的话,在站点设置的高级选项里可以手工指定算法优先级。
至于scp,9.0p1 默认已经走 SFTP 协议了。如果对端是很老的 sshd(6.x 及以下),SFTP 子系统可能不可用,这时要用-O参数强制回退到老协议:
scp -O local_file.tar.gz user@old-server:/tmp/这个小改动坑了不少人——升级完客户端之后,原本正常的 scp 脚本突然全部失败,报subsystem request failed,排查半天才发现是协议变了。
4.4 sshd 配置校验与平滑重启
改完配置千万别直接systemctl restart。先用-t做语法检查:
/usr/sbin/sshd -t返回空表示配置没问题。如果报错,它会明确指出哪一行、什么问题,比如:
Bad SSH2 cipher spec '3des-cbc,aes128-cbc'—— 指定的加密算法里有 9.0 已移除的,需要删掉。Unsupported option UsePrivilegeSeparation—— 这个关键字在 8.0 就被移除了,配置里有残留就要删掉。Missing privilege separation directory: /var/lib/sshd—— 目录不存在,按前面说的方法创建。
还可以用sshd -T输出最终生效的完整配置(合并了默认值和文件配置),确认算法白名单确实按预期生效:
/usr/sbin/sshd -T | grep -E "kexalgorithms|hostkeyalgorithms|ciphers|macs"确认无误后重启服务,但一定不要在当前 SSH 会话里重启完就关窗口:
systemctl restart sshd systemctl status sshd紧接着,在另一个终端窗口里开新连接测试:
ssh -p 22 root@localhost "echo connection_ok"新连接成功了,再关掉老会话。这个顺序看起来啰嗦,但它是唯一能保证你不会把自己锁在门外的操作习惯。
5. 验证、回滚与常见问题排查
5.1 一份可执行的验证清单
重启完成后,我会按固定顺序跑一遍验证。这套清单是踩坑之后总结出来的,覆盖面比较全。
# 1. 版本确认 /usr/sbin/sshd -V ssh -V # 2. 服务状态 systemctl is-active sshd systemctl is-enabled sshd # 3. 端口监听 ss -lntp | grep sshd # 4. 本机回环连接(公钥) ssh -o BatchMode=yes root@127.0.0.1 "uname -r" # 5. 密码认证(用一个普通账号测) ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no testuser@127.0.0.1 "id" # 6. SFTP 子系统 echo "hello" > /tmp/t.txt sftp -o BatchMode=yes root@127.0.0.1:/tmp/t.txt /tmp/t2.txt # 7. scp(新协议) scp -o BatchMode=yes /tmp/t.txt root@127.0.0.1:/tmp/t3.txt # 8. 算法协商实际结果 ssh -vvv root@127.0.0.1 exit 2>&1 | grep -E "kex:|host key algorithm:|cipher:" # 9. 日志检查 journalctl -u sshd --since "10 minutes ago" | grep -iE "error|fail|denied"第 8 条特别值得做,它能告诉你实际协商用的是哪套算法。看到rsa-sha2-512或者ssh-ed25519就对了,如果还是ssh-rsa,说明配置没生效,需要检查是不是有别的配置文件覆盖(比如/etc/ssh/sshd_config.d/下的片段文件)。
提示:
sshd -T是排查配置问题的利器,它把最终生效的配置全量输出,比反复读 sshd_config 猜测默认值快得多。养成改配置后先-t查语法、再-T看结果的习惯。
5.2 常见问题速查表
下面这些是我在多个环境里真实遇到过的,按现象归类,方便对照排查。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
systemctl start sshd卡住后超时 | 服务文件是Type=notify,新编译的 sshd 不支持 sd_notify | 改/usr/lib/systemd/system/sshd.service为Type=simple,daemon-reload |
status=203/EXEC | ExecStart路径下没有 sshd | 确认--prefix=/usr,或修正 unit 里的路径 |
| sshd 启动就退出,日志提 DSA | host key 里有ssh_host_dsa_key | 移走该密钥文件 |
| 密码登录全部失败 | 编译时漏了--with-pam | 装 pam-devel 后重新 configure、make、install |
Subsystem request failed | sftp-server 路径不一致 | 改Subsystem行或建软链接 |
老客户端提示no matching host key type | 客户端只支持ssh-rsa | 服务端加HostKeyAlgorithms +ssh-rsa(受控使用) |
Permission denied (publickey)但密钥没问题 | PubkeyAcceptedAlgorithms不含客户端签名算法 | 用ssh -vvv确认签名算法,按需加白名单 |
| SELinux 环境下 sshd 被拒绝 | 新二进制没有正确的安全上下文 | restorecon -Rv /etc/ssh /usr/sbin/sshd,必要时用semanage fcontext |
登录后scp报错 | 9.0 的 scp 走 SFTP,对端不支持 | 加-O参数回退 |
sshd 提示Missing privilege separation directory | /var/lib/sshd不存在或权限不对 | 创建目录并设chmod 711 |
| 服务重启后连接被立即断开 | 配置里残留UsePrivilegeSeparation等已删关键字 | 删掉这些行,重新sshd -t |
5.3 systemd 服务文件那个坑
单独把Type=notify这个问题拎出来说,因为它在源码编译升级里出现概率很高。
CentOS 7 的/usr/lib/systemd/system/sshd.service里写的是Type=notify,意思是 sshd 启动后会主动给 systemd 发一个就绪通知。RHEL 官方的 openssh 包打过补丁,支持这个通知;但上游源码编译出来的 sshd 默认不带这个能力,于是 systemd 一直等不到通知,等到超时就把服务标记为失败。表现就是:systemctl restart sshd卡在那不动,几十秒后返回失败,但此时 sshd 进程其实已经在跑了。
处理方式有两种:
# 方式一:改成 simple,保留 -D 前台运行 sed -i 's/^Type=notify/Type=simple/' /usr/lib/systemd/system/sshd.service systemctl daemon-reload systemctl restart sshd# 方式二:确认 -D 参数存在,配合 Type=simple 或 Type=exec grep ExecStart /usr/lib/systemd/system/sshd.service # ExecStart=/usr/sbin/sshd -D $OPTIONS改完记得daemon-reload,否则改动不生效,你还是会看到超时。这个坑我前后踩过两次,第二次才意识到是 unit 文件的问题,之前一直以为是 sshd 本身启动失败。
6. 生产环境批量升级与加固经验
6.1 用 Ansible 做可控的批量分发
机器多了之后,手工升级的问题不在于慢,而在于"每台可能不一样"。你在 A 机上多敲了一个空格,B 机上少改了一行配置,半年后排查问题时根本对不上。所以批量场景下,我会把整个升级流程固化成脚本,用 Ansible 统一下发。
核心思路是:编译只做一次,产物统一分发。在一台和线上同架构的机器上编好,把二进制打包,推送过去覆盖。这样能保证所有机器的二进制完全一致。
# upgrade_openssh.yml 片段 - hosts: ssh_targets serial: 5 # 分批,每次 5 台,控制爆炸半径 gather_facts: yes tasks: - name: 备份现有 sshd shell: cp -a /usr/sbin/sshd /root/sshd.bak.$(date +%s) args: creates: /root/sshd.bak.flag - name: 分发新二进制 copy: src: "{{ item.src }}" dest: "{{ item.dest }}" owner: root group: root mode: "{{ item.mode }}" backup: yes loop: - { src: "files/sshd", dest: "/usr/sbin/sshd", mode: "0755" } - { src: "files/ssh", dest: "/usr/bin/ssh", mode: "0755" } - { src: "files/scp", dest: "/usr/bin/scp", mode: "0755" } - { src: "files/sftp-server",dest: "/usr/libexec/sftp-server", mode: "0755" } - name: 分发配置文件 template: src: templates/sshd_config.j2 dest: /etc/ssh/sshd_config backup: yes notify: restart sshd - name: 配置语法校验 command: /usr/sbin/sshd -t changed_when: false handlers: - name: restart sshd systemd: name: sshd state: restarted daemon_reload: yesserial: 5这一行是我加的最重要的一行。它让 Ansible 每次只处理 5 台,处理完一批再下一批。如果某批出了问题,后面的机器不会被波及,你有充足的时间去排查。全量并发推下去,一旦配置文件有误,整个集群同时失联,那场面相当难看。
另外ssh和scp客户端二进制要不要一起换,这个取决于场景。如果服务端和客户端是同一批机器互相连接,建议一起换;如果客户端是异构环境,只换服务端更安全。
6.2 打成 RPM 包的好处
前面提到中大规模建议走 RPM。这里说一下打包的关键点。用fpm打包最省事:
fpm -s dir -t rpm \ -n openssh \ -v 9.0p1 \ --iteration 1.el7 \ --epoch 1 \ -a x86_64 \ --rpm-autoreqprov false \ --depends pam --depends zlib \ --prefix /usr \ -C /usr/local/src/openssh-9.0p1 \ sbin/sshd bin/ssh bin/scp bin/sftp libexec/sftp-server--epoch 1很关键。CentOS 官方的 openssh 包 epoch 就是 1,如果你打的包不带 epoch,版本号比较时会被判为"比官方包旧",yum update就会拒绝升级或者被 yum 源里的老版本"反向覆盖"。加上 epoch 之后,9.0p1 才会被认为比 7.4p1 新。
--rpm-autoreqprov false关掉自动依赖探测。因为编译出来的二进制依赖libcrypto.so.1.1这类非系统库,自动探测会往里写一堆找不到的依赖,导致安装时Failed dependencies。手工声明--depends更可控。
打包的目的不只是安装方便,更重要的是版本可追溯。半年后你在一台机器上看到rpm -qa | grep openssh输出openssh-9.0p1-1.el7.x86_64,立刻就知道这台机器升过级;而源码安装的机器上,这个命令查不到任何东西,得靠ssh -V去猜。在几十上百台机器的环境里,这种可追溯性价值很高。
6.3 升级之外的加固动作
版本升上去只是第一步,顺手把几项配置一起改了,收益更大。
关闭 root 直接登录。如果业务允许,把PermitRootLogin设成no,改成用普通账号登录后sudo。这个改动会直接影响一批自动化脚本,动之前先确认脚本里用的是哪个账号。
限制登录用户。用AllowUsers或者AllowGroups做白名单,只允许特定账号登录。这样即使有人爆破出某个系统账号的密码,只要它不在白名单里,照样连不上。
开启登录审计。9.0p1 支持sshd -T输出完整配置,配合journalctl -u sshd做日志收集,可以比较方便地把登录事件汇总到日志平台。关键字段是Accepted publickey for、Failed password for、Connection closed by。
配置文件权限。/etc/ssh/sshd_config建议设成 600 root:root。有些加固基线会检查这一项。
定期做算法扫描。用nmap --script ssh2-enum-algos -p 22 <ip>可以从外部确认服务端实际开放的算法集合,比看配置文件更权威,因为它是真实协商结果。
这些都是和升级配套的动作。单独做某项效果有限,但组合起来,整个主机的远程访问面会收得很紧。
7. 一些我踩过的坑和实际体会
讲到这里,技术和流程基本讲完了。剩下几条是文档里通常不会写、但实际干活时很有用的经验,我单独拎出来说说。
关于 OpenSSL 的选择,别急着一次到位。我第一次升级时想着"反正要动,把 OpenSSL 也一起换成 1.1.1 吧",结果在一台装了自编译软件的机器上,新库的ld.so.conf顺序影响了别的程序,导致某个业务进程启动异常。后来我的做法改成:先只升 OpenSSH,用系统的 OpenSSL 1.0.2k 编译,验证通过后再评估是否升级 OpenSSL。一次变更只动一个变量,出问题时排查范围小得多。
sshd -t必须成为肌肉记忆。我有一次因为赶时间,改完配置直接systemctl restart,结果配置里有个拼写错误的算法名,sshd 启动失败,而当时我只有这一个会话,一重启连接就断了。那次是靠云控制台救回来的。从那之后,sshd -t && sshd -T | grep ...变成我改配置后的固定动作,一步都不省。
老会话不要急着关。如果你是通过 SSH 连上去做升级的,那在你成功用新连接登录之前,老会话绝对不能关。因为 sshd 重启时,已建立的会话不会立即断开(sshd 的进程模型是这样设计的),你可以利用这个特性:重启服务,然后用新窗口测试,成功了再关老窗口,失败了直接用老窗口回滚。这是最实用的安全网。
备份要检查可恢复性。备份的动作做完了,不代表备份能用。我建议在升级前,把备份的sshd.bin复制到一个临时位置,加上可执行权限跑一下./sshd.bin -V,确认它确实能启动。有些情况下文件权限没保留好,回滚时才发现二进制跑不起来,那就晚了。
记录基线数据。ssh -Q kex、ssh -Q key、ssh -Q cipher、ssh -Q mac这四条命令在升级前后各跑一次,存成文本文件对比。将来排查客户端兼容问题的时候,这份差异表能帮你快速定位是哪个算法消失了。
最后一条,也是最重要的:升级窗口和通知。OpenSSH 是登录入口,任何闪失都会导致整台机器不可达。所以升级一定要安排在维护窗口内,提前通知到相关方,并确保有带外管理通道可用。技术再熟,也架不住一次意外。
至于后续还能怎么扩展,我自己的路线是:把手动流程脚本化,脚本 Ansible 化,Ansible 流程接入内网的发布审批,最后形成"安全基线扫描 → 自动生成变更单 → 分批升级 → 自动验证 → 报告归档"的闭环。这套东西搭起来要花点时间,但对于管理规模超过几十台的团队来说,省下来的排查时间远超投入。