1. 为什么非升不可,以及升级方案怎么选
1.1 存量CentOS机器的安全压力
这期“常用环境部署”系列,聊聊CentOS上升级OpenSSH 10.2p1。先说背景。我手上一批存量服务器,跑的是CentOS 7.9,系统自带OpenSSH 7.4。安全扫描一拉,高危漏洞一大串,CVE列表里好几个都直接影响sshd组件。老版本默认开启的算法也跟不上现在的等保和密评要求,ssh-rsa、CBC系列加密算法、diffie-hellman-group-exchange-sha1这些在合规检查里基本都是扣分项。很多团队碰到这种情况第一反应是升级整个系统,但现实里业务系统、中间件、数据库版本都绑定在那,说迁就迁不现实。折中方案就是把OpenSSH作为独立组件升级,风险面小、回滚容易,这也是行业里比较常见的做法。
需要说明的是,OpenSSH是服务器远程管理的命门,一旦升挂了,服务器就进不去,所以这篇文章会花大量篇幅讲准备工作和回滚方案,这些往往比编译命令本身更重要。我这次操作的机器都是内网环境,有一台还是隔离网段,不能访问外网,所以整个过程对离线场景也有参考价值。无论你是刚入行的运维新人,还是已经在生产环境摸爬滚打多年的系统工程师,这篇文章从环境检查、依赖安装、编译参数、二进制替换、systemd配置到问题排查都覆盖到了,照着做基本能复现。
1.2 源码编译和大版本升级的取舍
OpenSSH升级通常有几条路:改yum源、用第三方repo、直接源码编译。先说yum源方式,CentOS官方源里OpenSSH版本常年不动,第三方源要么只支持特定系统版本,要么和系统自带的openssl版本绑得太紧,在生产环境里用起来很容易出现依赖冲突。我用过几个第三方源,装完以后发现把系统里的openssl也一并替换了,吓得赶紧回滚,这种联动升级在存量生产环境里风险太大。
源码编译是更可控的思路:只更新sshd、ssh、scp、sftp这几个核心二进制,不碰rpm包管理,也不影响系统里其他组件对openssl的调用。OpenSSH 10.x这个版本本身也清理了大量旧代码,默认弃用了一批老算法,对合规审计非常友好,这也是我把目标定在10.2p1而不是继续用7.4打补丁的原因。版本号按你拿到的源码包为准,哪怕是10.2p1和后续的小版本,流程基本一致。
踩坑提示:升级前务必确认系统里有gcc、make、openssl-devel、zlib-devel、pam-devel这些基础包。我有一次在最小化安装的机器上升级,编译到一半报错找不到openssl/evp.h,只能花时间补包,如果当时是内网离线环境就更尴尬。还有一个隐患:CentOS 7自带的gcc 4.8.5太老,编译新版OpenSSH容易出现兼容问题,建议先升级gcc或者用devtoolset版本,后面我会细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 升级前的环境检查、备份与保底通道
2.1 先摸清系统底细
动手前先把自家环境搞清楚,我不建议上来就下载源码包开干。先用这几条命令把系统信息收集齐:
bash复制cat /etc/redhat-release
uname -m
ssh -V
openssl version
rpm -ql openssh-server | head -20
systemctl status sshd --no-pager | head -10
拿到这些信息后,确认下载的源码包和当前系统架构匹配,x86_64的就别下成aarch64的。还要确认服务是通过systemd管理的,CentOS 7之后基本都用systemctl start sshd,但有些自定义安装的机器可能用的是init脚本,处理方式不同。另外,记录一下当前sshd的监听端口和认证方式,升级后这些配置要尽量保持一致,减少业务侧感知。
我遇到过一台机器,sshd_config里被前人改得乱七八糟,除了监听22还监听了一个自定义端口,rsyslog的日志分割也没配好。升级前先把这些旧账理清楚,免得升级后排查问题时新旧问题混在一起,分不清是哪一步出的错。配置文件如果有大量自定义修改,建议先用sshd -T把生效配置导出来存一份,后面可以对账。
2.2 安装编译依赖
编译OpenSSH依赖的包不多,但一个都不能少。CentOS 7上执行:
bash复制yum install -y gcc make zlib-devel openssl-devel pam-devel
gcc版本别太老。CentOS 7自带的gcc 4.8.5编译OpenSSH 10.x确实有点吃力,我在gcc 4.8.5上编译新版openssh,configure能过,但make到一半遇到编译错误的情况也有。如果出现这种情况,要么升级gcc,要么用低版本的OpenSSH先顶着。不过我这台机器之前已经升过gcc到8.3.0,编译很顺利。建议编译前先gcc --version看一下,如果版本低于5,优先把gcc升上去,否则后面报错了再回头处理,反而耽误时间。
内网离线环境的话,提前在联网机器上用yum install --downloadonly --downloaddir=/root/deps gcc make zlib-devel openssl-devel pam-devel把rpm包拉下来,再拷进内网用rpm -ivh /root/deps/*.rpm安装。这一步一定要提前做,不要等到编译报错了才去找包,内网环境临时搞依赖非常被动。
2.3 备份,备份,再备份
备份是升级OpenSSH最不能省的步骤。我习惯把下面这些路径全部归档到一个带时间的目录:
- /etc/ssh 整个配置目录,特别是ssh_host_*_key私钥和sshd_config
- /usr/sbin/sshd 系统sshd主程序
- /usr/bin/ssh、/usr/bin/scp、/usr/bin/sftp 客户端工具
- /etc/pam.d/sshd PAM认证文件
bash复制cp -a /etc/ssh /etc/ssh.bak.$(date +%F_%H%M)
mkdir -p /root/ssh_bin_bak
cp -a /usr/sbin/sshd /root/ssh_bin_bak/
cp -a /usr/bin/ssh /usr/bin/scp /usr/bin/sftp /root/ssh_bin_bak/
cp -a /etc/pam.d/sshd /etc/pam.d/sshd.bak
备份之后最重要的验证动作:把备份目录里的文件能正常执行。比如直接运行备份的 /root/ssh_bin_bak/sshd -t 看配置是否正常。只有备份可用,后面才能放心替换。还有一点,ssh_host_*_key是主机身份凭证,如果丢了,所有客户端known_hosts里的指纹都会告警,所以这组文件一定要原样保留,不要重新生成,除非你打算让所有客户端都更新指纹。
2.4 开一条不会断的保底通道
这是我最想强调的一点。远程升级OpenSSH,最怕的就是sshd一停就再也起不来,然后你就只能在机房或者通过带外管理去救。避免这个问题的办法是提前开好备用入口:
- 在tmux或screen会话里执行升级操作,防止网络抖动导致脚本中断
- 如果机器有带外管理(iDRAC/iLO/IPMI),提前确认能登录
- 条件允许的话,先开一个telnet临时端口做保底(生产环境谨慎,仅内网低风险场景)
- 本地另外开一个root会话常驻,不要只开一个窗口就开干
一个最简单的做法:先tmux new-session -s sshupgrade,所有命令在tmux里跑。万一SSH连接断了,重新连上后tmux attach还能看到完整现场,这能救命的。我这次操作就全程在tmux里跑,中间有一次网络闪断,要不是tmux,编译到一半的流程就断了,重新登录后还得从头查进度,非常被动。
3. 编译安装OpenSSH 10.2p1实操全流程
3.1 下载源码包与校验
从OpenSSH官网或OpenBSD镜像站下载openssh-10.2p1.tar.gz。下载后建议核对一下SHA256校验值,确保包完整、来源可信:
bash复制sha256sum openssh-10.2p1.tar.gz
这一步在安全整改场景下尤其重要,源码包投毒事故不是没发生过。下载路径放/usr/local/src,解压:
bash复制tar -zxvf openssh-10.2p1.tar.gz -C /usr/local/src
cd /usr/local/src/openssh-10.2p1
如果你是离线环境,源码包从联网机器下载后通过内网传进去,同样要核对校验值。我在内网机器上遇到过传输过程文件损坏的情况,configure阶段报各种奇怪错误,最后发现是源码包本身坏了,浪费了一个多小时。
3.2 configure参数的选择逻辑
OpenSSH的configure参数不算多,但选错后患不少。我用的标准参数是:
bash复制./configure \
--prefix=/usr/local/openssh \
--sysconfdir=/etc/ssh \
--with-pam \
--with-zlib \
--with-ssl-dir=/usr/lib64 \
--with-md5-passwords \
--with-privsep-path=/var/empty/sshd \
--with-privsep-user=sshd
逐个解释一下:
- --prefix=/usr/local/openssh:编译产物安装目录。我用这个独立目录防止污染系统/usr/local,同时方便后续版本回退。注意这里只是装到新目录,最终我们还会把二进制复制到 /usr/sbin 下替换系统原版。
- --sysconfdir=/etc/ssh:配置文件路径直接指定到系统原来的/etc/ssh,这样host key、sshd_config这些老配置能直接复用,升级后客户端的主机指纹不变,不会引发known_hosts告警。
- --with-pam:启用PAM认证。CentOS默认的ssh登录流程和密码策略都依赖PAM,如果不启用,后面可能会遇到密码用户无法登录的问题。
- --with-ssl-dir=/usr/lib64:指定openssl头文件与库文件路径。64位系统一定要写对,否则可能在链接阶段找不到libcrypto。
- --with-privsep-path=/var/empty/sshd:sshd权限分离机制使用的空目录。需要提前创建好并且权限正确。
- --with-md5-passwords:兼容老系统用户密码的MD5存储方式,有的老用户密码还在这么存,不加这个选项登录时可能一直报验证失败。
如果gcc版本太旧导致configure报错,可以尝试加--disable-gssapi之类的选项绕开不必要的特性,但我不建议在生产上为了省事盲目禁用特性。GSSAPI在某些Kerberos环境下还是有用的,一刀切禁用后面要启用就得重新编译。
3.3 make编译和正式安装
配置通过后,执行编译:
bash复制make -j$(nproc)
make -j后面的数字是并行编译的进程数,nproc自动获取CPU核心数,能明显加快编译速度。如果内存小,建议把数字调低,比如make -j4,免得编译时内存不够被OOM killer干掉。我这次在4核8G的机器上编译,用了3分钟左右就完成了。编译期间如果报错,别急着改参数重来,先把报错信息贴到搜索引擎里查一下,很多问题都有通用解法。
编译正常结束后:
bash复制make install
此时OpenSSH所有文件已经装到/usr/local/openssh,可以先验证一下版本:
bash复制/usr/local/openssh/sbin/sshd -V
/usr/local/openssh/bin/ssh -V
然后生成host key。如果复用系统原有/etc/ssh下的key,可以跳过;但稳妥起见,我一般会执行:
bash复制ssh-keygen -A
-A参数表示生成所有默认算法的主机密钥。需要注意的是,如果密钥文件已存在,ssh-keygen -A不会覆盖,所以重复执行是安全的。执行后到/etc/ssh目录下看看,应该能看到ssh_host_rsa_key、ssh_host_ecdsa_key、ssh_host_ed25519_key等文件。
3.4 替换系统二进制
新版本已经编译好了,最核心的一步就是把新二进制替换到系统标准位置。因为系统service文件和SELinux上下文默认都盯着/usr/sbin/sshd这个路径,直接覆盖比修改SELinux策略路径轻松得多。我在之前的机器上试过把sshd放到/usr/local/bin再改SELinux,麻烦不说,后续维护还容易忘,直接覆盖标准路径是最省心的。
bash复制cp /usr/local/openssh/sbin/sshd /usr/sbin/sshd
cp /usr/local/openssh/bin/ssh /usr/bin/ssh
cp /usr/local/openssh/bin/scp /usr/bin/scp
cp /usr/local/openssh/bin/sftp /usr/bin/sftp
替换后先用-t参数测试配置文件:
bash复制/usr/sbin/sshd -t
如果提示OK,再接着处理服务。没提示OK别往下走,先排查。这一步能拦截掉绝大多数配置不兼容问题,别嫌麻烦跳过。我见过有人替换完直接重启sshd,结果因为配置文件里有个旧参数导致起不来,远程全断,只能跑机房,这就是没做语法检查的教训。
3.5 配置sshd_config的升级要点
OpenSSH 10.2p1对老配置的兼容性总体不错,但有几个配置项需要特别检查。
第一,新版默认不再允许空密码登录,PermitEmptyPasswords no可以显式加上。第二,X11Forwarding如果没用到,建议直接关成no,减少攻击面。第三,UseDNS建议设成no,不然每次连接都可能因为DNS反查拖慢速度。第四,MaxAuthTries建议设置成3或更小,防止暴力猜密码。第五,如果开了GSSAPIAuthentication,又用不到Kerberos,建议关掉,这个选项在某些环境下会显著拖慢登录速度。
还有,如果生产环境有老客户端(比如老旧xshell、putty),升级后可能出现无法连接的情况。原因是新版OpenSSH默认禁用了ssh-rsa签名算法和一部分老密钥交换算法。临时恢复兼容可以这样:
bash复制KexAlgorithms +diffie-hellman-group14-sha1
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedAlgorithms +ssh-rsa
但这里我要泼盆冷水:临时兼容是应急手段,长期还是推动客户端升级更靠谱。旧算法保留越多,等保检查越难过。我这次升级前先摸了一下客户端版本,确认没有特别老的客户端才敢直接关老算法,如果你的环境里有历史遗留的老工具,先把过渡方案想好再动。
3.6 让systemd用上新sshd
CentOS的sshd服务由systemd管理,默认ExecStart指向/usr/sbin/sshd。因为我们替换了这个文件,理论上重启服务就生效了。但为了保险,我会显式确认服务单元文件内容:
bash复制systemctl cat sshd
如果里面的ExecStart不是/usr/sbin/sshd -D,而是其他路径,需要用systemctl edit sshd加上覆盖配置,或者直接写新的unit。下面是CentOS 7上标准的sshd.service核心内容:
ini复制[Service]
ExecStart=/usr/sbin/sshd -D
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=42s
确认无误后:
bash复制systemctl daemon-reload
systemctl restart sshd
systemctl status sshd
然后看监听端口:
bash复制ss -tlnp | grep 22
服务起来了,先别关当前连接,开一个新终端连接测试,确认新连接正常后再回到原窗口操作,这是远程升级的基本素养。我这次就是先开着旧窗口,新窗口测试没问题后,才开始做后续验证。
3.7 防火墙和SELinux别忽略
如果没有改端口,22端口防火墙一般没问题。但如果升级后sshd起不来,先看SELinux。CentOS 7默认SELinux enforcing,新编译的二进制如果放在/usr/local下并直接运行,可能被SELinux拦掉。好在我们是覆盖/usr/sbin/sshd,文件上下文还是sshd_exec_t,所以问题不大。但如果你把sshd放到别的路径,就需要restorecon或在SELinux里放行:
bash复制restorecon -Rv /usr/local/openssh
如果你顺便改了SSH端口(比如从22改成2222),记得放行防火墙:
bash复制firewall-cmd --add-port=2222/tcp --permanent
firewall-cmd --reload
同时还要同步处理SELinux端口标签:
bash复制semanage port -a -t ssh_port_t -p tcp 2222
这一步漏了的话,防火墙放行了也可能连不上。我帮别人排查过一台机器,firewall放行了新端口,但SELinux没加标签,连接直接超时,查了半天才发现是这个问题。
4. 升级后的验证与安全加固
4.1 版本确认与连接测试
升级完第一件事是整体验证。新开一个SSH窗口,确认能正常登录,然后执行:
bash复制ssh -V
sshd -V
如果输出OpenSSH_10.2p1,说明核心二进制已经替换成功。再用以下命令确认运行时配置:
bash复制/usr/sbin/sshd -T | grep -E 'protocol|port|permitrootlogin|passwordauthentication'
-T参数会输出实际生效配置,而不是文件里的原始内容。这一步能帮你发现配置里有没有隐藏的坑,比如你改了PermitRootLogin但没生效。我这次就发现有一台机器上,sshd_config里写的PermitRootLogin no,但生效配置里还是yes,原因是Match块没注意,实际走了另一个分支。
还有一个验证点:跑一轮安全扫描或至少用nmap扫一下端口:
bash复制nmap -p 22 -sV 127.0.0.1
输出里的banner信息如果显示OpenSSH 10.2p1,说明对外服务版本已经更新。这一步很直观,也方便在整改报告里截图留证。
4.2 密码登录权限检查
升级后最好顺便看一下几个关键项:
- PermitRootLogin 是否设为no(如果业务没硬性要求root直接登录)
- PasswordAuthentication 是否设为no(建议直接用密钥)
- Protocol 2,新版本来就是默认2
如果决定关闭root密码登录,先确认密钥能正常登录后再改。顺序千万别反,我见过有人先关了密码登录,结果密钥没配好,root直接进不去了。关密码登录前,先把客户端的公钥放到服务器的authorized_keys里,测试密钥登录没问题后再改配置。
关闭root密码登录的配置示例:
bash复制PermitRootLogin prohibit-password
PasswordAuthentication no
这两行的意思是:root只允许通过密钥登录,密码登录全部关闭。如果业务上root必须能密码登录(比如某些自动化脚本依赖),那至少把PasswordAuthentication保持yes,但要配合fail2ban做防护。
4.3 /var/empty/sshd目录权限检查
升级完成后还有一个细节容易漏:新版sshd对权限分离目录很敏感。/var/empty/sshd目录的属主和权限不对,可能导致sshd启动后无法正常工作,甚至能连接但无法分配会话。检查一下:
bash复制ls -ld /var/empty/sshd
chown -R root:root /var/empty/sshd
chmod 755 /var/empty/sshd
权限不对会导致sshd的权限分离机制报错。我遇到过一次,sshd进程起来后,客户端连接一直卡在认证阶段,journal日志里报privilege separation directory权限错误,改完目录权限后恢复正常。
5. 常见问题与排查技巧实录
5.1 sshd启动失败,怎么一步步查
最常见的是systemctl start sshd之后直接失败。先看日志:
bash复制journalctl -u sshd -n 50
如果是SELinux拒绝了,日志里会有denied字样。如果是配置错误,sshd -t会直接报错。还有可能就是新二进制依赖的库找不到,用ldd检查:
bash复制ldd /usr/sbin/sshd | grep 'not found'
如果依赖的libcrypto版本不对(比如系统openssl太老),新二进制根本起不来。这种情况要么升级openssl,要么编译时指定正确的openssl路径。我建议编译前就先ldd确认,别等替换完再查。我自己就吃过一次亏,编译时没注意openssl版本,替换后sshd起不来,一看缺libcrypto.so.1.1,而系统里只有libcrypto.so.1.0.2,只能重新编译,白白耽误了半小时。
5.2 升级后能登录但密码错误
有个经典场景:升级后密码用户登录一直提示Permission denied,但实际上密码没错。这多半是PAM的问题。新版sshd默认UsePAM yes,如果/etc/pam.d/sshd缺失或损坏,就会一直验证失败。恢复方法:
bash复制cp /etc/pam.d/sshd.bak /etc/pam.d/sshd
如果没有备份,从同版本系统拷贝或重新安装openssh-server包来恢复PAM文件。这个坑在从老版本直接编译覆盖时尤其容易出现,因为编译安装不会自动生成PAM配置文件。我这次升级前特意备份了/etc/pam.d/sshd,就是怕出现这个情况。
还有一种情况是/etc/ssh/sshd_config里写了UsePAM no,但你的用户密码是通过LDAP或NIS管理的,那也会导致密码验证失败。排查时先看journalctl里sshd的详细日志,里面会明确记录验证失败的具体原因。
5.3 远程升级失败,直接断开怎么办
如果真的出现升级后连接断开、新sshd起不来,先别慌。如果你有带外管理,直接通过KVM登录;如果没有,只能跑机房。进了服务器之后,把备份的sshd恢复回去:
bash复制cp /root/ssh_bin_bak/sshd /usr/sbin/sshd
systemctl start sshd
这就要靠前面第2节的备份了。我一直强调备份,就是怕这种时刻。恢复后还要检查ssh_host_key等文件是否完好,如果host key丢了,客户端连接时会有指纹告警,重新用ssh-keygen -A生成,并通知各客户端更新known_hosts。
5.4 客户端连不上:no matching key exchange method
升级后老客户端连不上,报no matching key exchange method或no matching cipher found,本质是新版本默认算法和客户端支持的算法没有交集。解决思路有两个:
第一,升级客户端。这个最推荐,xshell、putty、SecureCRT都出了新版本,支持新算法。第二,临时在sshd_config里放开部分老算法。比如:
bash复制KexAlgorithms +diffie-hellman-group14-sha1
Ciphers +aes128-cbc,aes256-cbc
改完reload sshd:
bash复制systemctl reload sshd
这两个老算法原则上只是为了给老客户端过渡,业务侧完成客户端升级后,应该马上从配置里移除。我这次操作时,有个老系统自带的OpenSSH 5.9客户端连不上新服务器,就是临时加了算法才连上,后来客户端升级后就把这些配置去掉了。
5.5 问题速查表
| 现象 | 常见原因 | 快速解法 |
|---|---|---|
| sshd启动失败 | SELinux拦截、依赖库缺失、配置错误 | 看journalctl,ldd检查,sshd -t排查 |
| 密码正确但登录失败 | PAM配置缺失或损坏 | 恢复/etc/pam.d/sshd |
| 老客户端连不上 | 新旧算法无交集 | 升级客户端,或临时放开老算法 |
| 连接极慢 | UseDNS未关闭 | sshd_config里设UseDNS no |
| scp/sftp不可用 | 新二进制未覆盖 | 从/usr/local/openssh/bin复制 |
| 私钥权限报错 | key权限过大 | chmod 600 ~/.ssh/authorized_keys |
| 连接后没有shell | /var/empty/sshd权限错 | 修复目录属主和权限 |
这个表格是我在多次升级过程中总结出来的,基本覆盖了90%的常见问题。如果遇到表格外的现象,先看journalctl -u sshd输出,再结合sshd -T对比生效配置,大部分都能定位。
5.6 给内网离线环境的一点建议
如果你所在环境是内网隔离,没法直接下载源码包,提前在联网机器上把openssh-10.2p1.tar.gz和依赖rpm包都拉下来,用优盘或内网传输工具拷进去。依赖包可以这样下载:
bash复制yum install --downloadonly --downloaddir=/root/deps gcc make zlib-devel openssl-devel pam-devel
然后把源码包和deps目录一起带到内网,先rpm -ivh /root/deps/*.rpm,再继续源码编译流程。离线环境下更要严格校验SHA256和依赖完整性,因为一旦缺包,内网里想临时下载很麻烦。还有一点,离线机器上如果之前装过其他版本openssl-devel,版本冲突概率比较高,建议rpm -qa | grep ssl先把现有版本查清楚,再决定是否强制替换。
最后说点实在的
这批服务器升级OpenSSH到10.2p1之后,我特意跑了一轮安全扫描,旧版扫出来的高危项基本清零,等保那边的报告也顺利过关。整个操作窗口控制在十五分钟左右,业务零感知。我的体会是,像OpenSSH这种牵一发动全身的基础组件,升级前把备份、保底通道、回滚步骤想清楚,比编译参数本身更重要。最后再给一个建议:升级完别急着删备份,至少留一个月。等几个安全扫描周期都过了,确认没有需要回滚的场景,再清理也不迟。如果后续你还打算批量在其他机器上做同样操作,可以把今天这套步骤写成脚本,配合ansible批量分发,效率会高很多。
