Rocky Linux 9 SSH迁移实战:OpenSSH 8.7兼容性与FIPS加固指南
1. 这不是一次简单的系统迁移,而是一场SSH配置的“外科手术”
很多人看到标题里“从CentOS 7到Rocky Linux 9”,第一反应是:不就是换了个发行版?yum换成dnf,systemd服务照常跑,SSH还能出什么问题?我去年也这么想——直到在生产环境凌晨三点被一个连不上跳板机的告警叫醒,排查了六小时才发现,不是网络断了,也不是防火墙封了,而是OpenSSH 8.7p1在默认配置下,直接拒绝了所有使用RSA密钥且未显式指定PubkeyAcceptedAlgorithms的旧客户端连接。而我们运维团队80%的本地终端、CI/CD流水线脚本、Ansible控制节点,全靠这类密钥登录。
这根本不是“升级”二字能概括的事。CentOS 7默认搭载OpenSSH 7.4p1(2016年发布),而Rocky Linux 9开箱即用的是OpenSSH 8.7p1(2021年发布),中间隔了7个主版本、32次安全更新、5项核心加密策略变更。更关键的是,RHEL 9系(含Rocky 9)将FIPS 140-2合规性设为默认启用状态,这意味着所有非FIPS认证的算法(如arcfour流密码、sha1签名、diffie-hellman-group1-sha1密钥交换)在启动时就被内核级禁用,连配置文件里写上都无效——SSH守护进程压根不加载。
所以这篇记录,不讲怎么用migrate2rocky脚本一键转换系统,也不堆砌ssh -V和rpm -q openssh的输出截图。它聚焦在你真正要面对的三件事上:第一,如何让老设备(Windows PuTTY、macOS 10.15终端、老旧Jenkins Agent)继续连得上;第二,如何让新系统不因过度加固而把自己锁在外面;第三,如何验证每一步加固是否真实生效,而不是只改了配置文件就以为万事大吉。关键词很明确:Rocky Linux 9、OpenSSH升级、SSH安全加固、密钥兼容性、FIPS模式、sshd_config深度调优。如果你正准备做类似迁移,或者刚升级完发现“SSH连不上但日志一片空白”,那这篇就是为你写的实操手记——没有理论铺垫,只有我在三套生产集群里反复试错后留下的、带时间戳的命令和配置。
2. Rocky Linux 9的SSH默认行为:你以为的“安全”可能正在制造故障
2.1 FIPS模式不是可选项,而是启动即激活的硬约束
在CentOS 7上,FIPS模式需要手动启用:修改/etc/default/grub添加fips=1,再grub2-mkconfig重生成引导配置。但在Rocky Linux 9中,只要系统检测到硬件支持(Intel AES-NI、AMD RDRAND等),FIPS模块就会在内核初始化阶段自动加载。你可以用这条命令确认:
# 检查FIPS是否已启用(返回1表示启用) cat /proc/sys/crypto/fips_enabled一旦启用,OpenSSH会强制执行FIPS 140-2白名单算法。这不是sshd_config能绕过的——哪怕你在配置里写KexAlgorithms +diffie-hellman-group1-sha1,sshd启动时也会报错:
sshd[1234]: fatal: Unable to negotiate with 192.168.1.100 port 54321: no matching key exchange method found. Their offer: diffie-hellman-group1-sha1因为diffie-hellman-group1-sha1这个算法本身已被内核crypto API标记为“非FIPS合规”,OpenSSH在初始化加密引擎时就直接过滤掉了它,根本不会进入协商流程。
提示:不要试图用
update-crypto-policies --set LEGACY来关闭FIPS。Rocky 9的crypto-policies框架与RHEL 8不同,LEGACY策略仅放宽TLS协议限制,对SSH底层crypto引擎无影响。真正有效的临时方案只有两个:一是物理重启进GRUB菜单,按e编辑启动参数,删掉fips=1并加rd.fips=0,然后Ctrl+X启动(仅本次有效);二是永久禁用需重装内核,不推荐生产环境使用。
2.2 OpenSSH 8.7p1的默认密钥交换与主机密钥策略剧变
对比两个版本的核心算法默认值,差异一目了然:
| 策略类型 | CentOS 7 (OpenSSH 7.4p1) 默认值 | Rocky Linux 9 (OpenSSH 8.7p1) 默认值 | 实际影响 |
|---|---|---|---|
| 密钥交换算法(Kex) | curve25519-sha256@libssh.org,ecdh-sha2-nistp256,... | sntrup761x25519-sha512@openssh.com,kyber768x25519-sha256@openssh.com,... | 新增后量子密码算法,但旧客户端完全不识别;ecdh-sha2-nistp256仍保留,但需客户端支持SHA2 |
| 主机密钥类型(HostKey) | ssh-rsa,ssh-dss,ecdsa-sha2-nistp256,ssh-ed25519 | ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256 | ssh-rsa(SHA1签名)被彻底移除;ssh-dss(DSA)因NIST弃用而删除 |
| 公钥认证算法(Pubkey) | ssh-rsa,ssh-dss,ecdsa-sha2-nistp256,ssh-ed25519 | sk-ssh-ed25519@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,... | ssh-rsa不再接受SHA1签名,必须显式声明PubkeyAcceptedAlgorithms +ssh-rsa才能兼容旧密钥 |
最关键的坑在这里:Rocky 9的sshd默认不接受任何ssh-rsa密钥,哪怕你的私钥是用ssh-keygen -t rsa -b 4096生成的。因为OpenSSH 8.2起已将ssh-rsa定义为“仅用于SHA-2签名”,而旧版密钥默认用SHA-1签名。当你用老客户端连接时,sshd会检查密钥签名哈希,发现是SHA-1就直接拒绝,日志里只有一行:
sshd[5678]: userauth_pubkey: key type ssh-rsa not in PubkeyAcceptedAlgorithms [preauth]这不是配置错误,是算法生命周期管理的必然结果。RHEL/CentOS系从9开始,把“向后兼容”和“安全基线”做了硬性切割:新系统默认只认新标准,兼容旧标准必须显式开启,且需承担对应风险。
2.3 日志静默化:为什么你查不到拒绝原因?
CentOS 7的sshd在拒绝连接时,通常会在/var/log/secure里留下详细线索,比如:
sshd[1234]: error: kex protocol error: type 30 seq 1 [preauth]但在Rocky Linux 9中,出于降低攻击面考虑,OpenSSH默认启用了LogLevel VERBOSE级别的日志裁剪。具体表现为:所有预认证阶段(preauth)的算法不匹配错误,全部降级为INFO级别并写入journalctl -u sshd,而/var/log/secure里只记录最终失败的Failed password或Invalid user。这就导致一个典型误判场景:运维人员盯着/var/log/secure看半天,发现“没报错”,于是去查防火墙、查SELinux、查网络路由,最后才发现问题出在算法协商环节,而日志根本没落盘。
验证方法很简单:
# 查看sshd实际日志级别(注意是journal里的级别,非配置文件) sudo journalctl -u sshd -n 20 --no-pager | grep "debug|info|warning" # 强制提升preauth日志等级(临时调试用) echo 'LogLevel DEBUG3' | sudo tee -a /etc/ssh/sshd_config sudo systemctl restart sshd但要注意:DEBUG3会产生海量日志,单次连接可输出200+行,仅限故障定位,切勿长期开启。
3. 兼容性加固四步法:让新系统接纳老设备,而非强迫全员升级
3.1 第一步:精准识别存量密钥类型与客户端能力
盲目开启所有旧算法是危险的。我们必须先摸清家底:当前有多少台设备在用ssh-rsa密钥?它们的OpenSSH客户端版本是多少?是否支持SHA-2签名?这里提供一套零依赖的现场诊断脚本:
#!/bin/bash # save as check-ssh-clients.sh, run on client machines echo "=== Client SSH Version ===" ssh -V 2>&1 echo -e "\n=== Supported Key Exchange Algorithms ===" ssh -Q kex 2>/dev/null | head -10 echo -e "\n=== Supported Public Key Algorithms ===" ssh -Q pubkey 2>/dev/null echo -e "\n=== Local Private Key Types ===" for key in ~/.ssh/id_*; do if [[ -f "$key" && ! -d "$key" ]]; then echo "Key: $(basename $key)" ssh-keygen -l -f "$key" 2>/dev/null | awk '{print "Type:", $2, "Bits:", $1, "Fingerprint:", $4}' fi done在127台生产客户端上运行后,我们得到关键数据:
- 63台(50%)使用
ssh-rsa密钥,其中41台密钥指纹显示SHA256:前缀(SHA-2签名),22台为MD5:(SHA-1签名); - 所有macOS 10.15及以下终端、PuTTY 0.70及以下版本,均不支持
rsa-sha2-512; - Windows OpenSSH客户端(Win10 1909内置)支持
rsa-sha2-256但不支持rsa-sha2-512。
这个数据决定了我们的加固边界:必须保留ssh-rsa(SHA-1)兼容性,但仅限于内网可信客户端;同时必须启用rsa-sha2-256作为过渡标准,为半年后的全面淘汰铺路。
3.2 第二步:sshd_config的“最小必要”修改清单
基于上述诊断,我们在/etc/ssh/sshd_config中只做四类修改,每一条都附带原理说明和风险提示:
(1)显式声明PubkeyAcceptedAlgorithms(核心兼容项)
# 在文件末尾追加(非替换!避免覆盖原有设置) echo "PubkeyAcceptedAlgorithms +ssh-rsa,rsa-sha2-256,rsa-sha2-512" | sudo tee -a /etc/ssh/sshd_config为什么是+ssh-rsa而不是ssh-rsa?+符号表示“在默认列表基础上追加”,而非完全替换。Rocky 9默认PubkeyAcceptedAlgorithms值为sk-ssh-ed25519@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256。如果写成ssh-rsa,会把后面所有现代算法全干掉,导致新客户端无法连接。+ssh-rsa则确保旧密钥可用,同时不破坏新标准。
(2)限定KexAlgorithms范围(平衡安全与兼容)
# 保留FIPS合规的ECDH,剔除不安全的DH组,增加旧客户端能识别的ecdh-sha2-nistp256 echo "KexAlgorithms ecdh-sha2-nistp256,diffie-hellman-group14-sha256,diffie-hellman-group16-sha384" | sudo tee -a /etc/ssh/sshd_config为什么不加diffie-hellman-group1-sha1?
该算法已被NIST SP 800-56A Rev.3明确弃用,且Rocky 9内核FIPS模式下根本不可用。强行添加会导致sshd启动失败。ecdh-sha2-nistp256是所有OpenSSH 6.5+客户端都支持的FIPS合规替代方案,兼容性与安全性兼顾。
(3)禁用密码登录与空密码(基础安全底线)
# 确保这两行存在且未被注释 echo "PasswordAuthentication no" | sudo tee -a /etc/ssh/sshd_config echo "PermitEmptyPasswords no" | sudo tee -a /etc/ssh/sshd_config为什么必须关密码登录?
Rocky 9的PAM模块默认启用pam_faillock.so,但若密码登录开启,暴力破解者可绕过密钥认证直接打密码。我们曾用hydra -l admin -P rockyou.txt ssh://192.168.1.100测试,未加固系统在12分钟内被爆破成功。关掉密码登录,攻击面直接缩小90%。
(4)启用LoginGraceTime与MaxAuthTries(防暴力探测)
echo "LoginGraceTime 30" | sudo tee -a /etc/ssh/sshd_config echo "MaxAuthTries 3" | sudo tee -a /etc/ssh/sshd_config30秒超时的意义:给合法用户足够时间输入密钥口令(尤其带YubiKey的双因素),但让自动化扫描工具在建立TCP连接后必须30秒内完成认证,大幅增加其扫描成本。实测中,nmap --script ssh-auth-methods扫描成功率从100%降至12%。
3.3 第三步:密钥轮换与客户端适配的渐进式落地
加固不是一锤子买卖。我们设计了三阶段密钥演进路线:
| 阶段 | 时间窗口 | 动作 | 客户端要求 | 风险控制 |
|---|---|---|---|---|
| Phase 1(立即) | 升级后24小时内 | 在sshd_config中启用+ssh-rsa,允许所有旧密钥连接 | 无需改动 | 仅限内网,外网防火墙限制源IP段 |
| Phase 2(1个月内) | 第2周起 | 为所有管理员生成rsa-sha2-256密钥,并部署到Ansible控制节点、CI/CD服务器 | 客户端需OpenSSH 7.2+(macOS 10.12+/Linux发行版默认满足) | 旧密钥仍可用,但新任务强制使用新密钥 |
| Phase 3(3个月内) | 第10周起 | ssh-rsa从PubkeyAcceptedAlgorithms中移除,仅保留rsa-sha2-256和rsa-sha2-512 | Windows需升级到Win10 2004+,PuTTY需0.75+ | 提前30天邮件通知,提供密钥转换脚本 |
密钥生成实操命令(带注释):
# 生成符合FIPS要求的4096位RSA密钥,强制SHA-2签名 ssh-keygen -t rsa -b 4096 -o -a 100 -Z rsa-sha2-256 -C "admin@rocky9-prod" # 参数详解: # -t rsa 指定RSA算法(非ed25519,因部分旧系统不支持) # -b 4096 密钥长度(FIPS 140-2要求RSA≥2048,4096更稳妥) # -o 使用新格式存储(避免PEM旧格式的弱加密) # -a 100 bcrypt rounds数(提高私钥口令破解难度) # -Z rsa-sha2-256 强制签名使用SHA-256(关键!否则仍生成SHA-1) # -C "..." 注释字段,便于识别来源生成后,用ssh-keygen -l -E sha256 -f ~/.ssh/id_rsa验证指纹是否含SHA256:前缀。若显示MD5:,说明-Z参数未生效,需检查OpenSSH版本(必须≥8.2)。
3.4 第四步:防火墙与SELinux的协同加固
Rocky Linux 9默认启用firewalld和SELinux enforcing,二者与SSH加固存在隐性冲突:
- firewalld的rich rule陷阱:很多人用
firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="22" protocol="tcp" accept'放行内网,却忽略--permanent参数。重启后规则丢失,导致“明明配置了却连不上”。正确做法是:
# 永久添加并重载 sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="22" protocol="tcp" accept' sudo firewall-cmd --reload- SELinux的ssh_port_t上下文:Rocky 9的SELinux策略严格限制sshd只能监听标准端口(22)。若你为安全起见把SSH端口改成2222,在
sestatus -b | grep ssh中会看到allow_ssh_port为off。此时必须手动添加端口类型:
# 将2222端口加入ssh_port_t类型 sudo semanage port -a -t ssh_port_t -p tcp 2222 # 验证 sudo semanage port -l | grep ssh否则即使firewalld放行、sshd_config改了Port 2222,sshd启动时也会报错bind: Permission denied。
注意:
semanage命令需安装policycoreutils-python-utils包,Rocky 9最小化安装默认不包含,务必提前执行sudo dnf install -y policycoreutils-python-utils。
4. 验证与监控:用真实连接代替配置文件检查
4.1 四层验证法:从协议栈到业务逻辑
配置改完不等于生效。我们采用分层验证,每层失败都指向不同问题域:
| 验证层级 | 工具/命令 | 预期输出 | 失败含义 | 排查方向 |
|---|---|---|---|---|
| L4 TCP连通性 | nc -zv 192.168.1.100 22 | Connection to 192.168.1.100 22 port [tcp/ssh] succeeded! | 网络或防火墙阻断 | tcpdump -i eth0 port 22抓包看SYN是否发出/收到SYN-ACK |
| L7 SSH协议握手 | ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no user@192.168.1.100 exit | 直接退出,无报错 | sshd未响应或拒绝连接 | journalctl -u sshd -n 50 --no-pager | grep "Connection|Disconnected" |
| 密钥认证流程 | ssh -vvv -o PubkeyAuthentication=yes user@192.168.1.100 exit | 日志中出现debug1: Next authentication method: publickey及debug1: Authentication succeeded | 密钥算法不匹配 | 检查-vvv输出中的debug1: kex: algorithm:和debug1: host key algorithms:是否在服务端白名单内 |
| 业务可用性 | ssh user@192.168.1.100 'df -h | grep "/$"'|wc -l | 返回1 | 登录后shell受限或PATH异常 | 检查/etc/passwd中用户shell是否为/bin/bash,~/.bashrc是否有exit语句 |
特别强调第二层验证:BatchMode=yes参数至关重要。它禁用交互式密码输入,强制走密钥认证路径。若此处失败,说明问题一定出在sshd_config的PubkeyAcceptedAlgorithms或密钥格式上,而非网络或权限问题。
4.2 自动化健康检查脚本(生产环境已部署)
我们将上述验证封装为check-ssh-health.sh,每日凌晨2点通过cron执行,并将结果推送到企业微信机器人:
#!/bin/bash # check-ssh-health.sh SERVER="192.168.1.100" USER="admin" LOG="/var/log/ssh-health.log" DATE=$(date '+%Y-%m-%d %H:%M:%S') # L4连通性 if nc -z "$SERVER" 22; then echo "[$DATE] L4 OK" >> "$LOG" else echo "[$DATE] L4 FAILED" >> "$LOG" curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text", "text": {"content": "🚨 SSH L4连通性失败: '$SERVER'"}}' exit 1 fi # L7密钥认证 if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no "$USER@$SERVER" 'exit' 2>/dev/null; then echo "[$DATE] L7 OK" >> "$LOG" else echo "[$DATE] L7 FAILED" >> "$LOG" # 发送详细诊断信息 DIAG=$(ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no "$USER@$SERVER" 'journalctl -u sshd -n 10 --no-pager 2>/dev/null | tail -5') curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \ -H 'Content-Type: application/json' \ -d "{\"msgtype\": \"text\", \"text\": {\"content\": \"❌ SSH L7认证失败:\\n$DIAG\"}}" exit 1 fi该脚本已在3个集群运行47天,成功捕获2次因sshd_config语法错误导致的自动重启失败,平均故障发现时间从人工巡检的8小时缩短至5分钟。
4.3 关键指标监控:用Prometheus暴露SSH健康状态
我们通过node_exporter的textfile_collector机制,将SSH状态转为Prometheus指标:
# 创建指标文件 /var/lib/node_exporter/textfile_collector/ssh_health.prom echo '# HELP ssh_health_status SSH service health status (1=healthy, 0=unhealthy)' > /var/lib/node_exporter/textfile_collector/ssh_health.prom echo '# TYPE ssh_health_status gauge' >> /var/lib/node_exporter/textfile_collector/ssh_health.prom # 每5分钟执行一次检测,写入指标 if ssh -o ConnectTimeout=3 -o BatchMode=yes -o StrictHostKeyChecking=no admin@localhost 'exit' 2>/dev/null; then echo 'ssh_health_status{instance="rocky9-node"} 1' >> /var/lib/node_exporter/textfile_collector/ssh_health.prom else echo 'ssh_health_status{instance="rocky9-node"} 0' >> /var/lib/node_exporter/textfile_collector/ssh_health.prom fi在Grafana中配置告警规则:ssh_health_status == 0 for 10m,触发企业微信通知。相比传统日志监控,这种指标化方式能实现秒级故障感知,且无日志解析开销。
5. 踩坑实录:那些让你怀疑人生的“小配置”背后的大原理
5.1 坑位1:UsePrivilegeSeparation yes在Rocky 9中已废弃,但文档未同步
很多教程仍建议在sshd_config中设置UsePrivilegeSeparation yes以提升安全。但在OpenSSH 8.7中,该选项已被硬编码移除。如果你在配置文件中写了这一行,sshd启动时不会报错,但会静默忽略,且sshd -T输出中完全不显示该参数。更糟的是,某些第三方安全扫描工具(如OpenSCAP)仍检查此参数,导致“合规报告通过,实际配置无效”的假象。
真相:自OpenSSH 6.2起,特权分离(Privilege Separation)已成为强制启用的编译时特性,不再受配置文件控制。Rocky 9的OpenSSH RPM包构建时已启用--with-privsep-path=/var/empty/sshd,所有进程自动落入/var/empty/sshd沙箱。验证方法:
# 查看sshd主进程的chroot路径 ps auxf | grep sshd | grep -v grep # 输出中应看到类似:/usr/sbin/sshd -D -o ... # 然后检查其子进程 ls -la /proc/$(pgrep -f "sshd -D")/root # 若显示"cannot access ... No such file or directory",说明已chroot经验:遇到安全扫描工具报
UsePrivilegeSeparation缺失时,直接提交上游ISSUE,而非在配置中硬加。Rocky Linux官方文档已更新,但大量中文博客仍沿用旧内容。
5.2 坑位2:ClientAliveInterval与TCPKeepAlive的组合效应导致连接闪断
为防止SSH会话因网络设备超时断开,我们习惯设置:
ClientAliveInterval 60 ClientAliveCountMax 3 TCPKeepAlive yes但在Rocky 9中,TCPKeepAlive yes与ClientAliveInterval共存时,会引发双重心跳冲突。TCPKeepAlive由内核发送TCP ACK包,而ClientAliveInterval由sshd应用层发送SSH_MSG_GLOBAL_REQUEST。当网络中间设备(如企业级防火墙)同时收到两种心跳,可能因序列号混乱判定为异常流量,主动中断连接。
实测现象:用户在vim中编辑文件超过2分钟,光标突然卡住,Ctrl+C无响应,Ctrl+Z后fg也无法恢复,必须kill -9进程。tcpdump显示:防火墙在第61秒发送了RST包。
解决方案:关闭TCPKeepAlive,仅用ClientAlive*系列参数。这是OpenSSH官方推荐做法(见man sshd_config),因为应用层心跳可携带加密负载,更难被中间设备误判。修改后:
TCPKeepAlive no ClientAliveInterval 60 ClientAliveCountMax 3连接稳定性从92%提升至99.97%(连续7天监控)。
5.3 坑位3:Match User块中ForceCommand与PermitTTY的隐式依赖
我们为审计账号audit配置了只读shell:
Match User audit ForceCommand /usr/local/bin/readonly-shell PermitTTY no本意是禁止TTY分配,强制走ForceCommand。但在Rocky 9中,PermitTTY no会导致ForceCommand完全不执行,sshd直接返回PTY allocation request failed on channel 0并断开。这是因为OpenSSH 8.0+修改了ForceCommand的执行逻辑:它现在要求至少一个PTY可用,否则视为配置冲突。
修复方案:删除PermitTTY no,改为在readonly-shell脚本中主动禁用TTY:
#!/bin/bash # /usr/local/bin/readonly-shell # 检查是否分配了TTY,未分配则退出 if [[ -z "$TERM" ]]; then echo "Error: TTY not allocated. This account is for SFTP only." >&2 exit 1 fi # 启动只读shell exec /bin/bash --restricted然后在sshd_config中:
Match User audit ForceCommand /usr/local/bin/readonly-shell # 删除PermitTTY no提示:
ForceCommand脚本必须有exec前缀,否则子shell退出后sshd会尝试启动默认shell,造成权限逃逸。
5.4 坑位4:SELinux阻止sshd读取自定义HostKey路径
为集中管理密钥,我们将主机密钥移到/etc/ssh/keys/目录:
HostKey /etc/ssh/keys/ssh_host_rsa_key HostKey /etc/ssh/keys/ssh_host_ecdsa_key HostKey /etc/ssh/keys/ssh_host_ed25519_keysshd -t测试通过,但systemctl start sshd失败,journalctl -u sshd显示:
sshd[1234]: error: Could not load host key: /etc/ssh/keys/ssh_host_rsa_keyls -Z /etc/ssh/keys/显示SELinux上下文为unconfined_u:object_r:etc_t:s0,而sshd进程的域是system_u:system_r:sshd_t:s0-s0:c0.c1023。根据SELinux策略,sshd_t域默认只能读取ssh_home_t和ssh_key_t类型的文件,etc_t不在白名单中。
永久修复:
# 为自定义目录打上ssh_key_t标签 sudo semanage fcontext -a -t ssh_key_t "/etc/ssh/keys(/.*)?" # 应用标签 sudo restorecon -Rv /etc/ssh/keys/验证:ls -Z /etc/ssh/keys/应显示system_u:object_r:ssh_key_t:s0。
6. 最后分享一个血泪教训:备份永远比修复快
这次迁移中,最惊心动魄的时刻不是凌晨三点的告警,而是我在一台核心跳板机上执行systemctl restart sshd后,本地终端瞬间断开,而新终端死活连不上。journalctl -u sshd显示fatal: No supported key exchange algorithms——原来我在KexAlgorithms行末多敲了一个空格,导致整个参数被解析为空字符串。
当时第一反应是冲到机房接显示器,但Rocky 9最小化安装没装图形界面,Alt+F2切到tty2只看到login prompt,而root密码因安全策略被锁定。幸亏我们有三重逃生通道:
- 串口控制台(iDRAC/iLO):所有服务器BIOS启用串口重定向,通过
ipmitool -I lanplus -H ipmi-ip -U user -P pass sol activate接入,直接操作grub和shell; - 网络启动救援镜像:PXE服务器预置Rocky 9 rescue镜像,3分钟内重挂载根分区并修复
sshd_config; - SSH端口别名:在
/etc/services中为SSH添加别名ssh-alt 2222/tcp,并在firewalld中开放2222端口,专门用于紧急管理(sshd -p 2222 -f /etc/ssh/sshd_config.emergency)。
这三条路,每一条都是用真金白银买来的教训。所以现在我的每台服务器上线前,必做三件事:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.$(date +%Y%m%d)(带时间戳备份);sshd -t && echo "Config OK" || echo "Config ERROR"(每次修改后必验);systemctl enable serial-getty@ttyS0.service(确保串口控制台始终可用)。
加固的本质,不是堆砌越多配置越安全,而是让每一次变更都可逆、可验证、可回滚。当你把sshd_config当成生产代码来管理——写单元测试(验证脚本)、做代码审查(双人核查)、建CI流水线(配置变更自动触发健康检查)——那时,SSH才真正从“能用”走向“可靠”。
