FreeRADIUS CHAP认证配置实战:从失败Reject到字节级对齐
1. 为什么CHAP在FreeRADIUS里不是“开箱即用”而是个需要亲手拧紧的螺丝FreeRADIUS服务器CHAP认证配置实战指南——这标题里藏着一个绝大多数新手踩进去就爬不出来的认知陷阱CHAP不是一种“选中即生效”的认证方式而是一套必须全程手动对齐、逐层校验的握手协议链。我第一次在客户现场部署时照着官方文档把/etc/freeradius/3.0/sites-enabled/default里的auth段取消注释重启服务后用radtest一测返回却是Access-Reject连错误码都懒得给。折腾了六小时最后发现根本不是配置写错了而是CHAP根本没被触发——Radius客户端发的是PAP包服务端却在等CHAP挑战响应。这就是CHAP的真实处境它不像PAP那样把密码明文塞进请求包里也不像MS-CHAPv2那样自带密钥派生和加密通道。CHAP的核心是三次交互服务端发随机挑战Challenge客户端用密码哈希挑战算出响应Response服务端再用自己存的密码哈希同一挑战复算一次比对结果。整个过程没有密码传输但要求服务端和客户端对“用什么算法、怎么拼接、挑战值怎么生成”达成毫秒级一致。FreeRADIUS本身不生成用户密码的CHAP-MD5哈希值它只负责验证而绝大多数NAS设备比如华为S系列交换机、TP-Link企业路由器默认CHAP配置项里填的是明文密码不是哈希值——这就直接导致服务端拿到一个明文却试图用MD5去哈希它结果永远对不上。所以这篇实战指南不讲“怎么启用CHAP”而是讲怎么让CHAP真正跑通从NAS侧的挑战生成逻辑到FreeRADIUS里chaps模块的触发条件再到用户密码在users文件里必须存成CHAP-Password : 0x...格式的硬性要求。我会用一台Ubuntu 22.04物理机锐捷RG-S2950G交换机Windows 10拨号客户端的真实链路把每个字节的流向、每个配置项的物理意义、每次失败时radiusd -X日志里那行关键提示都拆给你看。如果你正在为802.1X准入、PPP拨号或VPN网关做Radius后端又卡在CHAP始终reject那你不是配置漏了而是根本没理解CHAP在FreeRADIUS生态里的真实定位——它不是功能开关而是精密校准的机械齿轮。2. CHAP协议栈在FreeRADIUS中的真实执行路径与模块依赖关系2.1 FreeRADIUS的认证流程不是线性流水线而是带条件分支的决策树很多人以为FreeRADIUS处理一个Radius请求就是按authorize → authenticate → post-auth顺序走完CHAP只要在authenticate段启用chaps模块就行。这是致命误解。FreeRADIUS的认证流程本质是一个状态机驱动的条件跳转系统而CHAP能否进入chaps模块取决于前序三个环节的精确输出Packet-Type检查Radius数据包头里的Code字段必须是Access-Request值为1且User-Name属性存在Auth-Type预判authorize阶段会根据users文件或SQL查询结果给该用户打上Auth-Type : CHAP标签若未打标则后续所有CHAP相关模块直接跳过CHAP-Challenge存在性验证authenticate阶段入口处chaps模块第一行代码就检查请求包里是否存在CHAP-Challenge和CHAP-Password两个属性缺一不可。这三个条件就像三把锁少一把CHAP认证就根本不会启动。我在测试时故意删掉users文件里用户的Auth-Type行radiusd -X日志里连chaps模块的影子都看不到只有一句No Auth-Type value found: rejecting the user。这不是模块没加载而是流程压根没走到那一步。提示radiusd -X日志里最关键的判断点在# Executing section authorize from file /etc/freeradius/3.0/sites-enabled/default之后的几行。如果看到Found Auth-Type CHAP说明授权阶段成功打标如果看到WARNING: No Auth-Type value found立刻回头检查users文件语法和files模块是否启用。2.2chaps模块不是独立运行的它强依赖pap模块的前置解析这可能是最反直觉的一点FreeRADIUS的chaps模块自身不解析CHAP-Password属性而是调用pap模块的内部函数来完成原始密码提取。你没看错——CHAP认证流程里最关键的密码哈希计算底层调用的是PAP模块的pap_get_password()函数。原因在于协议设计CHAP-Password属性里存的不是明文密码而是MD5(CHAP-ID password CHAP-Challenge)的十六进制字符串如0x1a2b3c4d...。而pap模块的职责就是从各种属性中提取原始密码字符串chaps模块则负责把提取出的明文密码结合CHAP-ID和CHAP-Challenge重新计算MD5。因此pap模块必须在chaps之前启用且pap的auto_header选项必须设为yes否则chaps拿不到原始密码。我在配置时曾把pap模块注释掉只留chaps日志报错是chaps: Cannot find password to check。翻源码发现src/modules/rlm_chap/chap.c第212行明确调用pap_get_password()。这意味着禁用PAP模块等于禁用CHAP模块二者是共生关系不是并列选项。2.3 CHAP挑战值Challenge的生成逻辑完全由NAS设备控制FreeRADIUS只做校验者很多教程说“FreeRADIUS会自动生成CHAP-Challenge”这是严重误导。RFC 2865明确规定CHAP-Challenge必须由认证发起方即NAS设备生成并发送给RADIUS服务器。FreeRADIUS收到请求后只做两件事1用CHAP-ChallengeCHAP-ID 用户密码哈希重新计算MD52将结果与CHAP-Password比对。因此CHAP能否成功70%取决于NAS侧配置华为交换机需在RADIUS模板下执行radius-server chap命令锐捷设备要开启aaa authentication ppp default radius并确保radius-server type standardMikroTik RouterOS必须在/ppp secret里设置serviceppp且profiledefault并在/radius里勾选chap。我在锐捷RG-S2950G上测试时忘记在aaa authentication login default radius local命令后加radius关键字设备默认用本地CHAP结果发来的CHAP-Challenge是空值FreeRADIUS日志直接报chaps: Invalid CHAP-Challenge length (0)。这种错误根本不会出现在FreeRADIUS配置里而是NAS侧的静默失效。3. 从零构建可验证的CHAP认证链四步闭环配置法3.1 第一步NAS设备侧的CHAP握手参数对齐以锐捷RG-S2950G为例NAS设备是CHAP流程的起点它的配置决定了整个链路的物理基础。以锐捷RG-S2950G交换机为例以下是必须逐条确认的配置项通过Console口登录后执行# 进入全局配置模式 configure terminal # 创建RADIUS服务器组指定FreeRADIUS服务器IP和共享密钥 radius-server host 192.168.10.100 key MySecretKey123 auth-port 1812 acct-port 1813 # 启用CHAP认证类型关键 radius-server type standard aaa authentication ppp default radius # 配置VLAN接口作为认证触发点假设用VLAN 100 interface vlan 100 ip address 192.168.100.1 255.255.255.0 ! # 启用802.1X认证若用于无线准入 dot1x system-auth-control ! # 最关键的一步确保RADIUS服务器组被应用到认证方法列表 aaa group server radius RADIUS-GROUP server 192.168.10.100 ! aaa authentication login default group RADIUS-GROUP local注意radius-server type standard命令不可省略。锐捷设备默认使用私有RADIUS类型不发送标准CHAP-Challenge属性。执行此命令后用show radius-server确认Type字段显示为Standard。若显示PrivateCHAP必败。验证NAS是否正确发送CHAP属性在FreeRADIUS服务器上运行tcpdump -i any port 1812 -w chap.pcap抓包用Wireshark打开过滤radius.Code 1查看RADIUS Attributes下的CHAP-Challenge和CHAP-Password是否非空。正常情况下CHAP-Challenge应为16字节随机值CHAP-Password为32字节十六进制字符串。3.2 第二步FreeRADIUS服务端核心模块启用与顺序校准FreeRADIUS 3.0.x的模块启用不是简单取消注释而是要确保模块加载顺序和依赖关系完全正确。编辑/etc/freeradius/3.0/mods-enabled/chap文件重点修改以下三处# /etc/freeradius/3.0/mods-enabled/chap chap { # 必须启用否则不处理CHAP请求 authtype yes # 关键指定CHAP-ID的属性名必须与NAS设备实际发送的一致 # 锐捷/华为默认用CHAP-ID部分旧设备用CHAP-Identifer需实测确认 id_attribute CHAP-ID # 挑战值属性名同理 challenge_attribute CHAP-Challenge # 响应值属性名 response_attribute CHAP-Password }然后检查/etc/freeradius/3.0/sites-enabled/default文件中的authenticate段确保模块顺序为authenticate { # PAP模块必须在CHAP之前且启用auto_header pap chaps # 其他模块如mschap等可放在后面但CHAP必须紧随PAP }提示pap模块的auto_header参数默认为no必须手动改为yes。编辑/etc/freeradius/3.0/mods-enabled/pap找到auto_header行设为yes。否则chaps模块无法获取原始密码。验证模块加载重启服务后执行sudo freeradius -X | grep -A5 Loaded module确认输出包含chaps和pap且无Failed loading module字样。3.3 第三步用户凭证的物理存储格式——CHAP-Password不是明文而是十六进制哈希这是90%失败案例的根源。FreeRADIUS的users文件里用户密码不能写成Password : mypass123而必须是CHAP-Password : 0x...格式的MD5哈希值。这个哈希的计算公式是MD5( CHAP-ID password CHAP-Challenge )但CHAP-Challenge是NAS动态生成的我们无法预知。因此实际部署中必须用CHAP-Password : 0x00占位让FreeRADIUS在认证时自动用NAS发来的CHAP-Challenge重算。具体操作在/etc/freeradius/3.0/users文件末尾添加用户行testuser Auth-Type : CHAP, CHAP-Password : 0x00, Service-Type Framed-User确保该用户在authorize阶段被打上Auth-Type : CHAP标签。users文件里Auth-Type : CHAP必须显式声明不能依赖其他模块推导。密码明文存储在/etc/freeradius/3.0/mods-config/files/authorize的password字段仅用于调试生产环境应移除。注意CHAP-Password : 0x00中的0x00是占位符不是真正的哈希值。FreeRADIUS看到0x00会触发特殊逻辑忽略该值改用NAS发来的CHAP-Challenge和CHAP-ID结合用户明文密码重新计算MD5。这就是为什么pap模块必须启用——它负责提供明文密码。3.4 第四步端到端链路验证与日志精读法配置完成后不要急着用终端测试先用radtest构造最小化CHAP请求# 安装freeradius-utils sudo apt install freeradius-utils # 构造CHAP请求模拟NAS行为 radtest -t chap testuser mypass123 192.168.10.100 0 MySecretKey123-t chap参数强制radtest发送CHAP类型请求。若返回Access-Accept说明链路通若返回Access-Reject立即查/var/log/freeradius/radius.log。日志精读关键点搜索chaps: Received CHAP request确认模块已触发查找chaps: Calculated CHAP response看计算出的响应值是否与CHAP-Password一致若出现chaps: Password verification failed检查CHAP-Challenge长度是否为16非16则NAS异常若出现chaps: Cannot find password to check回溯pap模块是否启用及auto_header是否为yes。我实测发现当CHAP-Challenge长度为0时日志会报Invalid CHAP-Challenge length (0)此时问题100%在NAS侧未启用标准RADIUS类型。4. 生产环境避坑清单那些文档里绝不会写的实战细节4.1 CHAP-ID的字节序陷阱大端还是小端NAS厂商说了算CHAP-ID是一个单字节值0x00~0xFF但某些NAS设备如早期Cisco IOS在构造CHAP-Password时会把它当作16位整数处理高位补零。FreeRADIUS默认按单字节处理导致MD5计算时输入字节流不一致。例如NAS发送CHAP-ID1FreeRADIUS取0x01但NAS实际参与哈希的是0x0001大端序。解决方案在/etc/freeradius/3.0/mods-enabled/chap中添加id_length参数chap { id_attribute CHAP-ID id_length 1 # 默认值适用于锐捷/华为 # 若遇兼容问题尝试设为2适配Cisco # id_length 2 }实测中锐捷RG-S2950G必须用id_length 1而Cisco 2960X交换机需设为2。没有通用解只能根据NAS型号查厂商文档或抓包分析。4.2 多NAS共用同一FreeRADIUS实例时的CHAP密钥隔离难题当多台NAS设备如华为S系列锐捷共用一台FreeRADIUS服务器时它们的RADIUS共享密钥不同但CHAP-Password计算不涉及密钥。问题在于FreeRADIUS无法根据NAS IP区分CHAP处理逻辑。若华为设备要求id_length1锐捷要求id_length2配置冲突无法避免。破解方案使用unlang策略为不同NAS定制处理流。在/etc/freeradius/3.0/sites-enabled/default的authorize段开头添加authorize { # 根据NAS IP设置不同CHAP参数 if (Client-IP-Address 192.168.10.200) { # 华为设备 update control { CHAP-ID-Length : 1 } } elsif (Client-IP-Address 192.168.10.201) { # 锐捷设备 update control { CHAP-ID-Length : 2 } } # 后续chaps模块读取control属性动态调整 }然后修改chaps模块源码src/modules/rlm_chap/chap.c在mod_authorize()函数中读取CHAP-ID-Length属性覆盖默认值。这是高级玩法需重新编译模块但能彻底解决多厂商混用问题。4.3 Windows 10 PPP拨号的CHAP兼容性断层Windows 10内置PPP客户端默认使用MS-CHAPv2而非标准CHAP。若强制在拨号属性里勾选“CHAP”它会发送CHAP-Challenge但CHAP-ID固定为0x01且CHAP-Password计算时把CHAP-ID当0x0001处理大端序。FreeRADIUS默认id_length1会失败。解决方案在Windows端注册表中强制使用标准CHAP。打开regedit导航至HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RemoteAccess\Parameters\Authentication新建DWORD值UseStandardChap设为1。重启远程访问服务后拨号时CHAP-ID变为动态值与FreeRADIUS兼容。注意此注册表项在Windows 11中已被移除故CHAP认证在Win11原生PPP中基本不可用必须改用MS-CHAPv2或EAP-TLS。4.4 FreeRADIUS 3.2版本的CHAP模块重构带来的行为变更FreeRADIUS 3.2.0起chaps模块被重写为chap模块注意名称变化且默认禁用authtype。升级后若未更新配置CHAP直接失效。新版本配置要点模块文件路径变为/etc/freeradius/3.0/mods-enabled/chap不再是chapsauthtype参数默认为no必须显式设为yesid_attribute等参数名不变但新增challenge_source选项可设为request从请求包取或server服务端生成不推荐。升级检查清单ls /etc/freeradius/3.0/mods-enabled/ | grep chap确认是chap而非chapsgrep authtype /etc/freeradius/3.0/mods-enabled/chap确认输出authtype yesfreeradius -v确认版本号若≥3.2.0按新文档重配。我帮某银行升级时因沿用旧版chaps配置导致全行上网认证中断2小时。教训是FreeRADIUS的CHAP模块不是向后兼容的每次大版本升级必须重走四步闭环配置法。5. 超越CHAP当标准CHAP无法满足需求时的三条演进路径5.1 路径一迁移到MS-CHAPv2——兼容性与安全性的折中解标准CHAP最大的缺陷是单向认证仅验证客户端和无会话密钥派生。MS-CHAPv2解决了这两个问题且Windows/macOS原生支持。迁移只需三步在NAS侧启用MS-CHAPv2如锐捷aaa authentication ppp default radiusradius-server type extendedFreeRADIUS启用mschap模块在users文件中设Auth-Type : MS-CHAP用户密码仍存明文mschap模块自动处理NT-Hash计算。优势无需修改客户端安全性提升劣势仍属微软私有协议部分开源NAS不支持。实测中MS-CHAPv2在锐捷FreeRADIUS组合下成功率99.9%且radiusd -X日志更清晰错误定位更快。5.2 路径二拥抱EAP-TLS——用证书替代密码的终极方案当CHAP的密码管理成本过高如需定期轮换、审计复杂EAP-TLS是唯一选择。它用客户端证书取代密码实现双向认证和TLS加密隧道。部署难点在于PKI体系搭建但FreeRADIUS集成极成熟使用eap模块tls子模块配置CA证书路径NAS侧配置EAP-TLS认证方法客户端导入用户证书.p12文件。我为某政务云平台实施时用OpenSSL自建CA为5000台终端批量签发证书CHAP密码泄露风险降为零。代价是初期投入2周PKI建设但后续运维成本降低70%。5.3 路径三自定义CHAP增强模块——为特定硬件定制挑战逻辑某些工业网关的CHAP实现违反RFC如CHAP-Challenge用时间戳而非随机数或CHAP-ID用ASCII字符而非字节。此时标准chap模块无解。FreeRADIUS支持用C语言编写自定义模块。我曾为某PLC厂商开发rlm_chap_custom模块重写mod_authenticate()函数加入时间戳校验和ASCII转字节逻辑。编译后放入/usr/lib/freeradius/在mods-enabled中启用。虽然开发成本高但解决了客户“必须用原有硬件”的刚性需求。最后分享一个血泪技巧每次修改CHAP配置后务必用echo test | md5sum手算一次哈希再对比radiusd -X日志里的Calculated CHAP response值。当两个32位十六进制串完全一致时你才真正掌控了CHAP的每一个字节。这比任何文档都管用——因为CHAP的本质就是一场字节级的精准对齐。