问题背景
公司办公网采用 802.1X 认证,无线和有线全部走 FreeRADIUS + Microsoft AD CS 颁发的 EAP-TLS 客户端证书,三年前由前同事搭建,当时写了个 openssl 一键生成脚本签了 10 年期的自建 CA,所有终端的根证书、FreeRADIUS 服务器证书、客户端证书全部挂在这棵 CA 下。
2026 年 6 月 28 日上午 9 点 47 分开始,全公司 8 栋办公楼先后出现大规模无线断连和有线认证失败,研发部门、客服中心、工厂车间反馈最为集中。Zabbix 上 AP 的认证成功率从 99.6% 直线掉到 23%,无线控制器每秒钟有 400+ 重新认证请求堆积。我们的 2000+ 员工终端、IoT 设备、IP 电话几乎全部受影响,初步估计单是业务停滞损失每小时就在十万级别。事故发生前一周,刚好有同事反馈”Windows 提示网络证书有问题”,但当时没人在意——这就是典型的预警被忽视引发的连锁反应。
故障现象
9 点 47 分开始,IT 群开始密集弹消息:
1 2 3 4 5
| 【研发2部-工位A12】电脑右下角弹"无法连接到网络 WiFi" 【研发2部-工位A13】重连失败,提示"无法验证服务器身份" 【客服中心】IP电话全部掉线,话机显示注册失败 【6楼车间】扫码枪连不上AP,无法作业 【财务部】连有线网提示"无法完成身份验证"
|
- 无线场景:手机和笔记本弹出”无法验证服务器身份 / Unable to verify server identity”,但 SSID 还是能搜到,点击连接后直接失败
- 有线场景:插了网线的工位弹出黄色感叹号,登录界面反复弹出要求输入域账号但怎么输都通不过
- IoT 设备:IP 电话、扫码枪、门禁设备、打印机几乎全部离线
抓包看无线控制器上的 Radius 报文:
1 2 3 4 5 6 7
| [FreeRADIUS log] (5) eap_tls: ERROR: TLS Alert read:fatal:certificate unknown [FreeRADIUS log] (5) eap_tls: ERROR: TLS_accept: Failed in unknown state [FreeRADIUS log] (5) eap: ERROR: Failed continuing EAP TLS (13) session [FreeRADIUS log] (5) [eap] = reject [FreeRADIUS log] (5) [authz] = reject [FreeRADIUS log] (5) Auth: (5) Login incorrect (eap_tls: Failed to verify certificate): [FreeRADIUS log] (5) } # server outer-eappeap-inner-tls { ... }
|
而前一天最后一次成功的认证日志时间戳是 6 月 27 日 23:58:14。问题集中在上午 9 点 47 分之后,所有终端都无法完成 EAP-TLS 握手。
排查过程
第一阶段:现场确认范围
第一反应是 Radius 服务挂了,先用 SSH 登入两台 FreeRADIUS 节点查看服务状态:
1 2 3 4 5 6 7 8 9
| ssh radius01 "systemctl status freeradius | head -20"
ssh radius01 "radtest test test 127.0.0.1 0 testing123"
|
这个 PAP 测试失败很关键——这意味着不是网络问题、不是设备问题,是 FreeRADIUS 本身拒绝认证。如果 Radius 进程崩溃、配置错误,至少本地 radtest 应该成功。
第二阶段:定位具体错误原因
打开 FreeRADIUS 的详细日志模式:
1 2 3
| ssh radius01 "radiusd -X 2>&1 | tee /tmp/radius-debug.log" &
radtest user@corp password 10.1.1.100 0 testing123
|
详细日志关键片段:
1 2 3 4
| (1) eap_tls: ERROR: SSL says error 10 : certificate has expired (1) eap_tls: ERROR: TLS Alert write:fatal:certificate expired (1) eap: ERROR: Failed continuing EAP TLS (13) session (1) [eap] = reject
|
根因锁定:RADIUS 服务器上 EAP-TLS 链路用的证书已经过期。这个证书就是 FreeRADIUS 服务器证书,是当年用自建 CA 签的,签发 10 年有效期,已经在 6 月 28 日 00:00:00 过期。
第三阶段:确认所有关联证书状态
接下来要查清楚:到底过期了哪几张证书,根 CA 有没有一起过期:
1 2 3 4 5 6 7 8 9 10 11
| ssh radius01 "ls -la /etc/freeradius/certs/"
for cert in /etc/freeradius/certs/{ca,server}.pem; do echo "=== $cert ===" openssl x509 -in $cert -noout -dates -subject -issuer done
|
输出:
1 2 3 4 5 6 7 8 9 10 11
| === /etc/freeradius/certs/ca.pem === notBefore=Jun 15 14:22:08 2024 GMT notAfter =Jun 15 14:22:08 2034 GMT # 根 CA 还有 8 年 subject= /CN=Corp Internal CA issuer= /CN=Corp Internal CA # 自签根
=== /etc/freeradius/certs/server.pem === notBefore=Jun 15 14:22:08 2024 GMT notAfter =Jun 15 14:22:08 2026 GMT # 昨天 00:00 过期! subject= /CN=radius.corp.example.com issuer= /CN=Corp Internal CA # 由上面这个 CA 签发
|
一目了然:根 CA 还在有效期,但服务器证书是 2 年前生成时签了 2 年(前面写的 10 年期是 CA 本身),server.pem 在 6 月 28 日 00:00 过期。我之前对脚本记忆有误,所以忽视了 Server 证书本身的 2 年有效期。
更糟糕的是,这个 server.pem 同时被三处使用:
- FreeRADIUS 主配置文件
/etc/freeradius/eap.conf 和 mods-available/eap
- FreeRADIUS 的客户端工具(用于向 AD CS 申请证书的 SCEP 接口)
- AD CS 上挂的 Web Enrollment / OCSP 服务也用了同一张证书做 SSL 绑定
第四阶段:紧急止血
完全处理需要重新签发服务器证书、推送新的 CA/服务器证书到 2000+ 终端,过程需要数小时。在此之前先用 OpenSSL 临时续签一张短期证书:
1 2 3 4 5 6 7 8 9
| cd /etc/freeradius/certs openssl req -new -key server.key -out server.csr \ -subj "/CN=radius.corp.example.com" \ -addext "subjectAltName=DNS:radius.corp.example.com,DNS:radius01.corp.example.com,IP:10.1.1.100"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key \ -CAcreateserial -out server.pem -days 30 \ -sha256 -extfile <(printf "subjectAltName=DNS:radius.corp.example.com,DNS:radius01.corp.example.com,IP:10.1.1.100")
|
然后重启 FreeRADIUS:
1 2 3 4 5
| ssh radius02 "systemctl restart freeradius" ssh radius01 "systemctl restart freeradius"
radtest test test 127.0.0.1 0 testing123
|
但这只是 FreeRADIUS 这一端,终端上的根 CA 信任链没变——终端有之前手工导入的根 CA,CA 还在有效期,EAP-TLS 客户端对服务器证书的校验会校验 issuer 链 + 过期时间。新签的 30 天证书由同一个 CA 签发,CA 链没变,所以理论上已部署过 CA 根证书的终端不需要重新安装 CA。
可是事实是无线终端还是连不上!继续抓 FreeRADIUS 详细日志:
1 2
| (1) eap_tls: ERROR: SSL says error 26 : unsupported certificate purpose (1) eap_tls: ERROR: TLS Alert write:fatal:unsupported certificate
|
新问题!原因是脚本里签证书时没加 extendedKeyUsage=serverAuth 扩展:
1 2 3 4
| openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key \ -CAcreateserial -out server.pem -days 30 -sha256
|
正确的签发方式需要加 EKU 扩展:
1 2 3 4 5 6 7 8
| cat > /tmp/extfile.cnf <<'EOF' extendedKeyUsage = serverAuth, clientAuth subjectAltName = DNS:radius.corp.example.com, DNS:radius01.corp.example.com, IP:10.1.1.100 EOF
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key \ -CAcreateserial -out server.pem -days 30 \ -sha256 -extfile /tmp/extfile.cnf
|
再次重启后 FreeRADIUS 正常签出 Received Access-Accept,已信任 CA 根证书的终端陆续恢复。
第五阶段:长期解决 - 自动化证书轮换
30 天临时证书只是续命。要根治,必须做完整的自动化证书轮换:
1. 用 AD CS 替代自建 OpenSSL 签发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| $inf = @" [Version] Signature = "`$Windows NT`$" [NewRequest] Subject = "CN=radius.corp.example.com" KeySpec = 1 KeyLength = 2048 Exportable = TRUE MachineKeySet = TRUE SMIME = FALSE PrivateKeyArchive = FALSE UserProtected = FALSE UseExistingKeySet = FALSE ProviderName = "Microsoft RSA SChannel Cryptographic Provider" ProviderType = 12 RequestType = PKCS10 KeyUsage = 0xa0 HashAlgorithm = SHA256 [EnhancedKeyUsageExtension] OID = 1.3.6.1.5.5.7.3.1 ; Server Authentication OID = 1.3.6.1.5.5.7.3.2 ; Client Authentication [Extensions] 2.5.29.17 = "{text}" _continue_ = "dns= radius.corp.example.com&" _continue_ = "dns= radius01.corp.example.com&" _continue_ = "ipaddress= 10.1.1.100&" [RequestAttributes] CertificateTemplate = RadiusServerTemplate "@ $inf | Out-File -Encoding ASCII C:\radius01.inf certreq -new C:\radius01.inf C:\radius01.req certreq -submit C:\radius01.req C:\radius01.cer certreq -accept C:\radius01.cer
|
2. 写 cron 自动化续签 + 部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #!/bin/bash
DAYS_BEFORE_EXPIRY=60 CERT_PATH="/etc/freeradius/certs/server.pem" THUMBPRINT=$(openssl x509 -in $CERT_PATH -noout -fingerprint -sha256 | cut -d= -f2) EXPIRY=$(openssl x509 -in $CERT_PATH -noout -enddate | cut -d= -f2) EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -gt $DAYS_BEFORE_EXPIRY ]; then echo "[OK] 证书还有 $DAYS_LEFT 天,无需续签" exit 0 fi
echo "[WARN] 证书还有 $DAYS_LEFT 天,开始续签流程"
cp -a $CERT_PATH ${CERT_PATH}.bak-$(date +%Y%m%d) cp -a /etc/freeradius/certs/server.key /etc/freeradius/certs/server.key.bak-$(date +%Y%m%d)
winexe -U corp\\admin%password //radius01 "cmd /c certreq -enroll -machine RadiusServerTemplate" 2>/dev/null
smbclient //radius01/c$ -U corp\\admin%password -c "cd \ProgramData\Microsoft\Crypto\RSA\MachineKeys; ls" > /dev/null
systemctl restart freeradius
sleep 5 radtest test test 127.0.0.1 0 testing123 && echo "[OK] 续签完成并通过测试"
|
3. 终端侧:使用 SCEP/NDES 自动注册
旧流程是 IT 同事手工给每台电脑装 CA 根证书,效率低且容易漏装。改用 NDES + Intune SCEP:
解决方案
最终落地的方案分三层:
止血阶段(T+0 ~ 30 分钟):在 FreeRADIUS 上用 OpenSSL + 自建 CA 临时签发 30 天带 extendedKeyUsage=serverAuth,clientAuth 和 subjectAltName 的服务器证书,重启服务后约 70% 已部署 CA 根证书的终端自动恢复,剩下 30% 是历史装机从未导入过 CA 的工位机,由现场 IT 同事手工导入。
短期修复(T+1 ~ 2 周):将 FreeRADIUS 证书切到 AD CS 颁发的 5 年期 RadiusServerTemplate 证书(受 5 年 CA 模板限制),由 certreq 自动续签脚本每 30 天检查并续签,提前 60 天主动轮换,避免再出现凌晨过期无人值守的情况。
长期根治(T+1 ~ 2 月):
- CA 侧:把自建 OpenSSL CA 整个迁移到企业 AD CS,使用 5 年期
SubCA 模板并启用 autoenroll,所有 2000+ 终端通过 GPO 自动注册 RootCA 信任
- 客户端侧:IoT 设备、扫码枪、IP 电话改为 SCEP 自动注册客户端证书,免去手工导入
- 监控侧:在 Zabbix 上添加对
/etc/freeradius/certs/*.pem 的扫描,所有证书剩余有效期 < 90 天的都触发告警,证书清单每周发邮件给安全组
- 流程侧:建立”证书生命周期”维基页面,所有证书的签发日期、到期日期、负责人、轮换 SOP 一目了然,离到期 90/60/30/7 天分别触发不同级别告警
根因分析
深挖下来,这次事故有四个相互独立的根因叠加:
- EAP-TLS 服务器证书的有效期被脚本写死成 2 年,而前同事把这个脚本注释为”自建 CA 10 年期”,导致后续接手的人(包括我)误以为整套体系都是 10 年一换
- 没有任何证书过期监控,Zabbix 监控的是 Radius 服务进程存活、AP 认证成功率,但没人意识到”证书”也是一种需要监控的资源
- 历史装机太松散,CA 根证书在客户端没有强制 GPO 推送,部分工位机是当年实习生手工装的,证书已经丢/坏,并且这个状态在 AP 重新认证时才暴露
- 缺乏证书全生命周期管理流程,没有 PKI 文档、没有续签 SOP、没有 SCEP/NDES 这种”证书可以自动过期和续签”的设计,本质上还是手工运维的思路在管理 CA
预防措施
基于这次踩坑,建立了下面这套预防体系:
1. 证书全生命周期管理系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
RECIPIENT="[email protected]" WARN_DAYS=90 CRITICAL_DAYS=30 CERT_DIRS=("/etc/freeradius/certs" "/etc/nginx/ssl" "/etc/strongswan/ipsec.d")
REPORT="" for dir in "${CERT_DIRS[@]}"; do for cert in $dir/*.pem $dir/*.crt; do [ -f "$cert" ] || continue EXPIRY=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2) [ -z "$EXPIRY" ] && continue DAYS_LEFT=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 )) SUBJECT=$(openssl x509 -in "$cert" -noout -subject | cut -d= -f2-) if [ $DAYS_LEFT -lt $CRITICAL_DAYS ]; then REPORT+="[CRITICAL] $cert 剩 ${DAYS_LEFT} 天 - $SUBJECT\n" elif [ $DAYS_LEFT -lt $WARN_DAYS ]; then REPORT+="[WARN] $cert 剩 ${DAYS_LEFT} 天 - $SUBJECT\n" fi done done
if [ -n "$REPORT" ]; then echo -e "证书过期预警:\n$REPORT" | mail -s "【证书过期预警】$(date +%Y-%m-%d)" $RECIPIENT fi
|
2. Zabbix 自定义监控项
1 2 3 4 5
| UserParameter=cert.expiry[*],openssl x509 -in $1 -noout -enddate 2>/dev/null | cut -d= -f2 | xargs -I{} date -d "{}" +%s
|
3. 终端 GPO 强制信任企业 CA
1 2 3 4 5 6 7 8 9
| Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies → Certificate Services Client - Auto-Enrollment ☑ 自动注册证书 ☑ 续签过期的证书 ☑ 更新正在使用的证书
|
4. SCEP/NDES 自动客户端证书
所有域内电脑和受管移动设备通过 Intune SCEP 自动向 AD CS 申请客户端证书,证书有效期 1 年自动续签,永久不需要 IT 同事手工介入。
5. 红蓝对抗演练
每季度模拟”把 Radius server.pem 替换为已过期证书”,演练运维团队的应急响应速度和流程顺畅度,确保事故响应的肌肉记忆。
总结
这次事故最深的教训,是证书这种”非生命体”资源在大多数运维同学的监控盲区里。它不像磁盘、CPU、内存那样有常规的仪表盘告警,往往只在过期那一刻才”砰”地炸出来,但影响范围却是公司级别的。
回头看,EAP-TLS 体系的设计原则应该是”证书全生命周期”——从 CA 设计、模板、签发、续签、客户端分发、信任链维护到过期监控,每一步都要有自动化兜底。任何依赖”人工记得去续签”的设计,最终都会在某个凌晨、某个长假、某个新人接手的时刻翻车。
我们这次虽然用 30 天临时证书快速止血,没有出现业务数据丢失,但这种”凭证类”的故障本应通过前置的告警 + 自动续签 + 监控来彻底规避。建议所有使用 802.1X / EAP-TLS / HTTPS 证书 / IPsec 证书的团队,都把”证书全生命周期管理”作为年度安全审计的必查项,并且把 Zabbix / Prometheus 对证书过期天数的监控当作 P0 级别来对待——这绝不是”锦上添花”,而是”救命的最后一道防线”。