很多跑在 Ubuntu 20.04 上的服务器,OpenSSH 还停留在系统自带的 8.2p1。这个版本是 2020 年初的产物,放在当年不算落后,但放到现在这个几乎每年都爆几个高危漏洞的环境里,多少有点让人不踏实:ssh-agent 远程代码执行(CVE-2023-38408)、ProxyCommand 配置注入(CVE-2023-51385)、还有 8.7 及以下版本都受影响的提权漏洞(CVE-2021-41617),每一个都实实在在地写在公网服务器的攻击面上。
更要命的是,很多人的 Ubuntu 20.04 不是一台“干净”的服务器,而是装过 ROS、折腾过显卡驱动、跑过虚拟机的开发机或实验室主机。这种机器上 apt 依赖经常一团乱,某次apt upgrade顺手把 openssh-server、libssl、libc 一起动了一遍,重启之后发现 SSH 再也连不上了——这种事我见过不止一次。与其把命运交给不自知的依赖变动,不如主动做一次可控的 OpenSSH 源码升级,把版本握在自己手里。
这篇文章记录的是我在 Ubuntu 20.04 上把 OpenSSH 从 8.2p1 源码升级到 9.9p1 的完整过程。内容包括为什么要升、升级前怎么备份、configure 参数怎么选、远程操作时如何避免把自己锁在外面,以及升级后最容易翻车的几个坑和对应的自救方案。不管你是刚接触 Linux 的新手,还是被安全通告追着跑的运维老手,这套流程都能直接照着用。
1. 为什么 Ubuntu 20.04 自带的 OpenSSH 8.2p1 值得大动干戈
1.1 安全公告和仓库补丁之间的时间差
先别急着开喷。Ubuntu 20.04 是有长期支持的,Canonical 确实会通过 apt 向 8.2p1 推送安全补丁,apt update && apt upgrade之后你拿到的仍然是一个被打了补丁的 8.2p1。如果你只在乎“能联网、能登录”,仓库版其实够用。
问题出在两个地方。第一,补丁推送的节奏和策略你控制不了。官方源修漏洞有它的优先级和排期,有些高危 CVE 从公开到进仓库要等不少时间,中间这段窗口期你的 SSH 服务就暴露在风险里。第二,如果你用的不是标准 Ubuntu 官方源,而是内网离线镜像、第三方定制镜像、或者基于 20.04 的衍生发行版,补丁滞后的问题会更严重,这时候“等仓库修”远不如“源码升级到最新版”来得踏实。
另外一个常被忽略的动机是新特性。OpenSSH 8.2 之后引入了不少实用能力,比如基于 FIDO2/U2F 硬件的sk-ssh-ed25519密钥、更现代的密钥交换算法、更严格的默认加密策略。仓库版 8.2p1 对这些支持是不完整的,你想用新特性就得自己升级。所以我在多个场景下都选择了源码编译,而不是干等仓库。
1.2 升级路线怎么选:源码编译、PPA 还是 backports
决定升级后,路线主要有三条,我分别试过,直接说结论。
| 升级方案 | 优点 | 缺点 |
|---|---|---|
| apt 仓库安全补丁 | 最稳,和 dpkg 状态一致 | 版本永远是 8.2p1,新功能没有 |
| 第三方 PPA 源 | 安装快,一条命令搞定 | 第三方包可信度存疑,依赖冲突时很难排查 |
| backports 仓库 | 比 PPA 正式一点 | Ubuntu 20.04 的 openssh backports 并不常驻,基本指望不上 |
| 源码编译 | 版本完全可控,可定制编译参数 | 和 dpkg 包管理状态脱节,后续要自己维护升级 |
我的建议很直接:生产环境能扛住就继续用官方源的安全补丁,别乱动;但如果你明确需要新版本、新特性,或者被迫使用滞后镜像,那就走源码编译。make install之后 dpkg 里关于 openssh-server 的记录确实会失真,这是源码升级固有的代价,提前想清楚,后面就不会慌。
2. 动工之前的巡检、备份与依赖准备
2.1 先摸清版本和系统状态
任何升级操作,第一步永远是搞清楚自己站在哪块地上。用下面这几条命令把家底盘一遍:
lsb_release -a uname -m ssh -V dpkg -l | grep -E 'openssh|libssl' ss -lntp | grep ':22'重点关注几个信息:系统是不是干净的 20.04、CPU 架构是不是 x86_64、当前 OpenSSH 版本、以及 22 端口监听的到底是哪个 sshd 二进制。很多坑都是在这步提前发现的,比如我之前遇到过一台机器上同时装了/usr/sbin/sshd和/usr/local/sbin/sshd两份 sshd,systemd 启动的是系统版,但你自己测试时用的又是另一个,两边版本不一致,升级后服务监听的还是老进程,排查起来非常迷惑。
还要顺手检查一下/etc/ssh下有没有主机密钥文件:
sudo ls -l /etc/ssh/ssh_host_*主机密钥是 SSH 服务器的“身份证”,正常情况下应该存在且权限正确:私钥 600,公钥 644。如果这步发现密钥缺失,先别继续,处理好再升。
2.2 备份三样东西:配置、二进制、deb 包
源码编译升级最怕的不是编译失败,而是升级到一半发现自己回不去了。所以我在动手前一定会做三次备份。
第一个是配置文件,/etc/ssh整个目录拷走,外加 PAM 配置:
sudo mkdir -p /root/openssh-backup/config sudo cp -a /etc/ssh /root/openssh-backup/config/ sudo cp /etc/pam.d/sshd /root/openssh-backup/config/第二个是当前正在用的二进制,避免make install覆盖后连旧版都找不回来:
sudo mkdir -p /root/openssh-backup/bin /root/openssh-backup/sbin sudo cp /usr/sbin/sshd /root/openssh-backup/sbin/ sudo cp /usr/bin/ssh /usr/bin/scp /usr/bin/sftp /usr/bin/ssh-keygen /usr/bin/ssh-agent /usr/bin/ssh-copy-id /root/openssh-backup/bin/第三个是通过 apt 把当前版本对应的 deb 包下载到本地,这是最快的回滚手段,后面会专门讲怎么用:
cd /root/openssh-backup sudo apt-get download openssh-server openssh-client openssh-sftp-server别嫌麻烦。这三步加起来不到两分钟,但它保证哪怕你把服务搞到彻底起不来,也有明确的后路。
2.3 编译依赖与可选功能组件
OpenSSH 源码编译需要的依赖不多,但少了任何一个都会导致编译出来的东西不好用。我的标准依赖安装命令是:
sudo apt update sudo apt install -y build-essential zlib1g-dev libssl-dev libpam0g-dev libsystemd-dev重点解释一下为什么这些一个都不能少。build-essential提供 gcc 和 make,属于基本盘;zlib1g-dev是压缩库头文件,OpenSSH 压缩支持依赖它;libssl-dev提供 OpenSSL 开发文件,密钥交换、加密算法全都要靠它;libpam0g-dev是为了编译时启用 PAM 支持,没有它,就算你在sshd_config里写了UsePAM yes,sshd 启动时也会报 PAM 初始化失败,导致密码登录不可用;libsystemd-dev则是让 sshd 能和 systemd 的Type=notify配合,缺少它的话 systemd 单元可能一直等到超时。
如果以后想用 FIDO2 安全密钥登录,可以额外装libfido2-dev再编译,这一步可选,不装也不影响常规使用。
3. 源码编译升级 OpenSSH 9.9p1 的分步实操
3.1 下载源码并校验完整性
去官网确认最新稳定版本号后,把源码包下载到/usr/local/src:
cd /usr/local/src sudo wget -c https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.9p1.tar.gz sudo wget -c https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.9p1.tar.gz.asc官网同时提供了.asc签名文件,理论上可以用 GPG 验签。但我实操中更常用的还是比对 SHA256 校验和,因为更直观:
sha256sum openssh-9.9p1.tar.gz把输出的哈希值和官网页面上的 SHA256 逐个字母对一遍。这一步别省,你接下来要把这个二进制放到 22 端口上给所有人连,源码被动手脚的后果太严重了。
3.2 configure 参数怎么写给后续运维省事
很多人编译 OpenSSH 时直接./configure一把梭,装到/usr/local下。这样不是不行,但会给 systemd 单元适配、PATH 查找、AppArmor 策略带来一堆额外麻烦。我的做法是让新版本直接顶替系统路径:
cd /usr/local/src/openssh-9.9p1 ./configure \ --prefix=/usr \ --sysconfdir=/etc/ssh \ --with-ssl-dir=/usr \ --with-zlib=/usr \ --with-pam \ --with-privsep-path=/var/run/sshd \ --with-privsep-user=sshd这里的每个参数都有讲究。--prefix=/usr是最关键的一个,它保证二进制安装到/usr/bin、/usr/sbin,和 Ubuntu 自带版的路径完全一致,systemd 单元里写的ExecStart=/usr/sbin/sshd -D不用改,PATH 里也直接能找到。--sysconfdir=/etc/ssh让新版本继续读你熟悉的配置目录。--with-pam必须加,Ubuntu 的密码认证、sudo 联动都依赖 PAM。--with-privsep-path指定特权分离目录,Ubuntu 上就是/var/run/sshd(也就是/run/sshd)。
3.3 编译安装与 systemd 单元适配
configure 顺利通过后,编译安装一气呵成:
make -j$(nproc) sudo make install-j$(nproc)让编译用满所有 CPU 核心,二十秒左右就能编完。make install执行完之后,先做一次版本确认:
ssh -V /usr/sbin/sshd -V如果输出是OpenSSH_9.9p1之类的字样,说明二进制已经替换成功。接着检查 systemd 单元里实际调用的路径是否和新二进制一致:
systemctl cat sshUbuntu 20.04 的ssh.service里ExecStart写的是/usr/sbin/sshd -D $SSHD_OPTS,因为我们用了--prefix=/usr,所以这条路径正好指向新编译的 sshd,无需修改。如果你当初真的装了/usr/local路径,这里就必须用systemctl edit ssh加一条 override,把 ExecStart 指到新 sshd,否则重启服务后跑的还是旧进程。
3.4 sshd -t 校验与重启技巧
任何一次配置或二进制替换之后,重启服务之前,必须跑语法检查:
sudo /usr/sbin/sshd -t有输出说明配置有问题,没输出就是通过。这条命令是 SSH 升级的生命线,我见过太多人跳过它直接 restart,结果配置文件里一个无关紧要的旧指令让 sshd 起不来,把自己锁在机器外面。
检查通过后再重启服务:
sudo systemctl restart ssh sudo systemctl status ssh --no-pager这里有一个远程操作时非常重要的建议:如果服务器离你很远,请务必在tmux或screen里执行重启命令。这样就算网络瞬断,命令也会在 session 里继续跑完,重连之后还能看到完整输出。后面我专门有一节讲这个事为什么值得认真对待。
4. 远程升级最容易翻车的三个坑与自救方法
4.1 sshd 起不来:动态库和 privilege separation 目录
源码编译升级后第一个高频翻车点,是systemctl restart ssh之后服务直接 failed。用journalctl -u ssh -n 50看日志,经常能看到类似这样的报错:
/usr/sbin/sshd: error while loading shared libraries: libcrypto.so.1.1: cannot open shared object file: No such file or directory原因很清晰:编译时链接的 OpenSSL 库和运行时系统里实际存在的库对不上。Ubuntu 20.04 本来默认是 OpenSSL 1.1.1,对应libcrypto.so.1.1;但如果你的机器之前装过其他软件栈引入了 OpenSSL 3.x,系统里只有libcrypto.so.3,新 sshd 启动时就会找不到旧库。
排查命令是:
ldd /usr/sbin/sshd | grep crypto如果显示缺libcrypto.so.1.1,先尝试安装:
sudo apt install -y libssl1.1如果还是找不着,那就得搞清楚你编译时用的头文件到底是哪个 OpenSSL 版本,必要时卸载混乱的 OpenSSL 3 开发包,重新 configure 再编一次。
另一个“起不来”的隐蔽原因是/run/sshd目录丢了。权限分离目录缺失时,sshd 会直接拒绝启动,日志里写着Missing privilege separation directory: /run/sshd。Ubuntu 上这个目录一般在系统启动时自动创建,但如果你之前清理过/run下的临时文件,或者做过奇怪的加固脚本,它可能就不在了。解决办法很简单:
sudo mkdir -p /run/sshd sudo chmod 0755 /run/sshd4.2 新版本把旧配置拒之门外
第二个常见坑是sshd -t直接报错,指向/etc/ssh/sshd_config里某一行。OpenSSH 9.x 对老配置的兼容性总体不错,但有些旧指令确实被改名或收紧了。
最典型的是ChallengeResponseAuthentication这个老指令,从 8.7 开始就变成了KbdInteractiveAuthentication的废弃别名,继续写在配置里虽然能通过检查,但新版日志会有警告;某些更老的指令则是直接删除,比如 DSA 算法相关的HostKeyAlgorithms ssh-dss,早在 7.0 就移除了,任何 8.2 时代的配置里还留着它都会导致启动失败。
另一个让很多人意外的变化是默认算法策略收紧。9.x 默认不再接受ssh-rsa(SHA-1)签名,如果你手上还有老的自动化脚本或较旧版本的客户端在用 RSA 密钥连接,升级后可能突然连不上。处理办法有两种:一是升级客户端到支持 SHA-2 的版本;二是在 sshd_config 里临时放开:
PubkeyAcceptedAlgorithms +ssh-rsa注意:这个配置只是过渡方案,能让老客户端继续连,但 SHA-1 签名在安全层面已经不值得信任,长期用等于把升级意义打了折扣。改完配置一定要再跑sudo /usr/sbin/sshd -t,然后重启服务。
4.3 手滑误杀会话:延迟重启和 tmux 兜底
远程升级最刺激的一幕,是你在 sshd 上执行systemctl restart ssh,然后看着终端卡住不动,像死机了一样。其实没那么容易死:Ubuntu 20.04 的ssh.service默认KillMode=process,重启只杀掉主监听进程,已经建立的会话子进程会保留,当前连接理论上不会断。但有前提条件——如果别人的加固脚本、自动化工具把 KillMode 改成了 control-group,或者你手快执行了systemctl stop ssh没接着 start,那当前会话说没就没了。
不要赌这个。我给自己定了个规矩:远程改 ssh 相关东西,一律在 tmux 里操作,并配合延迟重启。
tmux new -s upgrade-ssh # 在 tmux 里执行编译、安装、校验 sudo bash -c 'sleep 3; systemctl restart ssh'延迟几秒重启的意义在于:命令执行后你还有一点时间观察终端状态,就算连接真的闪断,tmux 里的命令也已经跑完了,重连后tmux attach -t upgrade-ssh能看完整日志。万一重启后服务异常,只要当前会话还在,立刻执行系统里备好的回滚脚本就能救回来。
5. 升级完成后的验证清单与快速回滚预案
5.1 一线功能体检
服务重启不代表升级成功,我每次都要按下面的清单做一遍验证:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 版本号 | ssh -V | OpenSSH_9.9p1 |
| 配置语法 | /usr/sbin/sshd -t | 无输出 |
| 服务状态 | systemctl is-active ssh | active |
| 端口监听 | ss -lntp | grep ':22' | sshd 监听 0.0.0.0:22 |
| 主机密钥权限 | ls -l /etc/ssh/ssh_host_* | 私钥 600,公钥 644 |
| 本地登录 | ssh localhost | 能登录 |
| 文件传输 | sftp localhost | 能列出目录 |
| 安全日志 | journalctl -u ssh -n 20 | 无异常报错 |
另外值得关注的是 AppArmor。Ubuntu 上 sshd 有默认 AppArmor 配置,如果升级后出现能启动但登录行为诡异的情况,看一眼:
dmesg | grep -i sshd | grep -i denied有输出说明新的二进制触发了 AppArmor 限制,常见的诱因是你把二进制装到了非标准路径,或者自定义过 profile。处理方式是调整对应的 AppArmor 配置,而不是关掉 AppArmor 了事。
5.2 五分钟回滚预案
升级完成后我会把备份目录封存留档,不急着删。万一新版本在新功能带来了更严重的不兼容,五分钟左右就能退回旧版:
sudo systemctl stop ssh sudo cp /root/openssh-backup/sbin/sshd /usr/sbin/sshd sudo cp /root/openssh-backup/bin/ssh /usr/bin/ssh sudo cp /root/openssh-backup/bin/scp /usr/bin/scp sudo cp /root/openssh-backup/bin/sftp /usr/bin/sftp sudo cp -a /root/openssh-backup/config/. /etc/ssh/ sudo systemctl start ssh如果你更希望把 dpkg 的包状态也恢复干净,用之前apt-get download保存的 deb 包直接安装:
cd /root/openssh-backup sudo dpkg -i openssh-server_*.deb openssh-client_*.deb openssh-sftp-server_*.deb注意,回滚后要再次执行sshd -t并确认ssh -V变回了旧版本,同时测试新开一个会话能正常登录。只要你没删备份目录,这个回滚流程永远有效。
最后再分享一个个人习惯:每次升级 OpenSSH 之前,我都会把sshd_config里计划要改的部分先在文本编辑器里改好,然后专门用一次正常的systemctl reload ssh(而不是 restart)让配置热生效。重启是最后一步、也是风险最大的一步,把其他所有前置操作都做完、验证完,最后那一下重启才值得按下。别小看这个顺序,它能帮你把“升级 OpenSSH”从一次惊险的跳伞,变成一趟有完整降落伞检查的单人飞行。