Freeradius 3.2.10 基于密码的企业级安全认证实现

如何实现企业级的网络安全认证

总体思路

从前面 《Freeradius 3.2.10 环境搭建以及 PAP 和 CHAP 两种认证方式的测试》 介绍我们知道,如果基于密码认证的方式,无论认证方式选择 PAP 或者是 CHAP,实际上都很弱鸡。既然 CHAP 和 PAP 在公网上传输不安全,最直接的思路就是:先建一条 TLS 加密隧道,把认证报文丢进隧道里传。外层 TLS 隧道的作用是提供安全的隐形管道,内层协议的作用仅仅是 “在这条安全管道里,客户端如何向服务器证明我是我”。这就是目前基于密码的安全网络环境的标准做法。


常见的几种方案

这里的名称确实五花八门,容易被绕晕,但只要抓住了 “外层建隧道,内层传密码” 这个核心逻辑,所有概念瞬间就会变得非常清晰。在 RADIUS 认证中,EAP(扩展认证协议)其实是一种两层嵌套结构:

  • 外层协议(TLS / PEAP / TTLS):负责建立一条加密隧道(安全快递盒),防止黑客在网络中窃听。
  • 内层协议(PAP / CHAP / MS-CHAPv2 / GTC):装在加密隧道内部的具体凭据(信封里的账号密码或挑战码)。
1
2
3
4
5
6
7
┌─────────────────────────────────────────────────────────────┐
│ 📦 外层隧道(TLS / PEAP / TTLS) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✉️ 内层凭据(PAP / CHAP / MS-CHAPv2 / GTC) │ │
│ │ (用户输入的账号密码、动态口令等) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

对四个词汇的拆解:

  • TLS(Transport Layer Security):传输层安全协议(就是 HTTPS 用的加密协议)。在 RADIUS 里,直接叫 EAP-TLS 时代表双向数字证书认证(不用密码,靠证书)。这个 TLS 既可做外层,也可做独立方案。
  • PEAP(Protected EAP):受保护的 EAP。由微软/思科主导的外层加密隧道协议。规定内层必须封装另一个 EAP 协议。它是一个纯外层隧道。
  • TTLS(Tunneled TLS):隧道化 TLS。由 Funk/FSH 主导的外层加密隧道协议。极其自由,内层可以直接传原始属性或普通协议(如 PAP)。它也是一个纯外层隧道。
  • EAP(Extensible Authentication Protocol):扩展认证协议。它不是具体算法,而是一个通用框架,上面的 PEAP、TTLS、TLS、MS-CHAPv2 全都是它的具体实现。

明白了 “外层” 和 “内层” 的区别,我们来看看企业里最常出现的几种组合:

  • EAP-TTLS + PAP:自建数据库的最佳选择。
    • 外层(TTLS):开辟一条 TLS 加密通道。
    • 内层(PAP):在加密管道里直接发送明文密码。
    • 这么配置是最佳搭档,因为外层已经绝对安全,内层用 PAP 让服务器解密出明文后,可以拿着去和数据库里的 BCrypt / Argon2 强哈希做比对。既防抓包,又防数据库泄漏。
  • EAP-PEAP + EAP-MSCHAPv2:微软/AD 域控的标准默认。
    • 外层(PEAP):开辟一条加密通道。
    • 内层(EAP-MSCHAPv2):在通道里跑微软特有的 MS-CHAPv2 挑战-响应协议。
    • Windows 电脑、iOS、Android 原生对这个组合支持最好。服务器不需要存明文密码,只需要存微软 AD 域控默认的 NT-Hash 即可。
  • EAP-PEAP + EAP-GTC:动态口令 / 2FA 专用。
    • 外层(PEAP):加密通道。
    • 内层(EAP-GTC):通用令牌卡协议(Generic Token Card)。
    • 用来传输一次性动态验证码(OTP)、Google Authenticator 验证码或硬件令牌。
  • EAP-TLS:无密码,纯证书。
    • 不分内外层,或者说内外层都是 TLS。
    • 不用输入任何账号密码。员工电脑里有一张私有证书,服务器也有证书,两者双向验证证书成功即通网。零信任和金融机构最爱,安全性最高,但维护成本也最高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
                            你要用什么方式验证身份?

┌──────────────────────┴───────────────────────────┐
▼ ▼
【使用 账号 + 密码】 【使用 数字证书】
│ │
你的后端账号库是? ▼
┌─────────┴────────────────────┐ #EAP-TLS#
▼ ▼ (安全性最高,全员发证书)
微软 AD 域控 自建 MySQL / LDAP / 现代哈希
│ │
▼ ▼
EAP-PEAP + MSCHAPv2 #EAP-TTLS + PAP#
(Windows 生态开箱即用) (支持 BCrypt,传输与存储双安全)


EAP-TTLS + PAP

前面我们已经搭建了最基础的 freeradius 测试环境,参考 《Freeradius 3.2.10 环境搭建以及 PAP 和 CHAP 两种认证方式的测试》 。我们将在原基础上继续实现 EAP-TTLS + PAP 方案。

建库建表

freeradius 对不同的数据库都提供了支持,官方建表语句放在 mods-config/sql/main 目录下。找到对应的 mysql/schema.sql,将建表语句在对应的已经建好的数据库中执行(比如我们建的库的名称是 radius3)。数据库中初始化一条测试账户:

1
2
3
4
5
6
7
8
# 生成一条 bcrypt测试密码
pip3 install bcrypt

python3 -c 'import bcrypt; print(bcrypt.hashpw(b"123456", bcrypt.gensalt()).decode())'
$2b$12$1dKv2bZWGBPN58J1R4hFBODkMY1S.UhVYQKbqE5IiVO.jDkizVdLe

python3 -c 'import bcrypt; print(bcrypt.checkpw(b"123456", b"$2b$12$1dKv2bZWGBPN58J1R4hFBODkMY1S.UhVYQKbqE5IiVO.jDkizVdLe"))'
True
1
2
3
4
5
-- 插入测试用户 owlias01,密码 123456 的 BCrypt 哈希。使用了 $2a$ 格式的 BCrypt 哈希,FreeRADIUS 的 pap 模块原生支持解析
-- 在 FreeRADIUS 3.x 中,BCrypt 格式(即 $2a$、$2b$、$2y$ 开头的哈希)通常也是作为 Crypt-Password
-- 属性提供给 pap 模块识别的。pap 模块底层会自动使用 OpenSSL 的 crypt() 来匹配 BCrypt。
INSERT INTO radcheck (username, attribute, op, value)
VALUES ('owlias01', 'Crypt-Password', ':=', '$2b$12$1dKv2bZWGBPN58J1R4hFBODkMY1S.UhVYQKbqE5IiVO.jDkizVdLe');


Freeradius 配置修改

第一,修改 /etc/raddb/mods-available/sql

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
35
36
37
38
39
40
sql {
# 使用 rlm_sql_mysql 驱动
driver = "rlm_sql_mysql"
dialect = "mysql"

# MySQL 容器连接配置
server = "192.168.1.251"
port = 3306
login = "root" # 实际环境一般新建一个 radius 账户
password = "xxx"
radius_db = "radius3"

# ===== 禁用 MySQL 连接的 TLS =====
# 如果是生产环境,MySQL 确实开启了 SSL 通信,则将真实的 MySQL CA 根证书挂载或复制到容器内
# tls {
# ca_file = "/etc/ssl/certs/my_ca.crt"
# ca_path = "/etc/ssl/certs/"
# certificate_file = "/etc/ssl/certs/my_client.crt"
# private_key_file = "/etc/ssl/certs/my_client.key"
# cipher = "DHE-RSA-AES256-SHA"
# tls_required = yes
# }
# =================────────────────────

# 连接池设置
pool {
start = 5
min = 4
max = 10
spare = 3
uses = 0
retry_delay = 30
lifetime = 0
idle_timeout = 60
}

# 读取标准的 radcheck 表(nas客户端列表,如果开启,这个列表有变动需要重启 freeradius 才能生效)
read_clients = no
client_table = "nas"
}

开启 freeradius 的 sql 模块。在宿主机对应的 volume 挂载点执行:

1
2
3
4
5
cd xxx/mods-enabled
ln -s ../mods-available/sql sql

ls -la
sql -> ../mods-available/sql

第二,修改 EAP 模块文件 /etc/raddb/mods-available/eap

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
eap {
default_eap_type = ttls
timer_expire = 60
ignore_unknown_eap_types = no
cisco_accounting_username_bug = no

# ===== EAP-TTLS 配置 =====
ttls {
# 允许外层建立 TTLS
tls = tls-common

# 内层默认调用的 EAP 类型(如果内层发的是 EAP)
default_eap_type = md5

# 核心设置:开启内层非 EAP 认证(如 PAP/CHAP)
copy_request_to_tunnel = yes
use_tunneled_reply = yes

# 关键配置:指定内层使用的 Virtual Server,处理解密出来的 PAP 请求
virtual_server = "inner-tunnel"
}

# TLS 证书配置(使用官方镜像自带的默认测试证书)
tls-common {
private_key_password = "whatever"
private_key_file = ${certdir}/server.pem
certificate_file = ${certdir}/server.pem
ca_file = ${cadir}/ca.pem
dh_file = ${certdir}/dh
cipher_list = "DEFAULT@SECLEVEL=1"
}
}

第三,修改 /etc/raddb/sites-available/default(外层 Virtual Server),负责处理网络入口的 EAP 握手。确保 authorize 节点开启了 eap 模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
authorize {
filter_username
preprocess

# 解析 EAP 报文,建立外层 TTLS 隧道
eap {
ok = return
}

expiration
logintime
pap
}

authenticate {
# 处理 EAP 认证逻辑
eap
}

post-auth {
# 允许解密后将内层属性返回给外层
exec
eap
}

第四,修改 /etc/raddb/sites-available/inner-tunnel(内层 Virtual Server - 最关键)。这是最核心的一步:当外层 TTLS 隧道建立成功后,FreeRADIUS 会将解密出来的明文 PAP 请求扔进这个 inner-tunnel 进行数据库比对。

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
server inner-tunnel {
authorize {
filter_username

# 1. 查询 MySQL 数据库,将 owlias01 的 Crypt-Password 属性加载到内存中
sql

# 👈 加上这几行:如果拿到了 User-Password,强行设置 Auth-Type 为 PAP
if (&User-Password) {
update control {
&Auth-Type := PAP
}
}

# 2. PAP 模块拿客户端内层传进来的明文密码,与数据库里的 Crypt-Password 做对比
pap
}

authenticate {
Auth-Type PAP {
pap
}
}

post-auth {

}
}

第五,修改 /etc/raddb/mods-available/pap

1
2
3
4
5
6
7
8
pap {
# 保持 normalize 开启
normalise = yes
# 开启 auto_header,让 pap 自动解析字符串前缀(比如 $2a$10$...)
auto_header = yes
# 允许匹配 Crypt 密码
crypt = yes
}

最后,重启 freeradius 服务器:

1
2
docker-compose up -d
docker restart radius-server


测试验证

我们在 radius-client 容器中进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker exec -it radius-client bash

cd ~
mkdir test & cd test

# 将 freeradius 服务端的 ca.pem 复制到测试端的一个测试目录下:
docker cp xxx/certs/ca.pem radius-client:/root/test/ca.pem

# 直接补全客户端容器缺失的系统证书依赖库,解决 OpenSSL 读取系统库报错的问题
apt-get update && apt-get install -y ca-certificates

# 编写一个 ttls-pap.conf 文件
vim ttls-pap.conf
network={
ssid="Corporate-WiFi"
key_mgmt=IEEE8021X
eap=TTLS
identity="owlias01"
password="123456"
phase2="auth=PAP"
ca_cert="/root/test/ca.pem"
}

在宿主机使用 wireshark 实时抓包。抓包文件请参考 freeradius-ttls-pap-success-pack.pcap。请使用curl -C - -O xxx 命令进行下载。

1
$ docker exec radius-server tcpdump -i eth0 -U -w - "udp port 1812 or port 1813" | /Applications/Wireshark.app/Contents/MacOS/Wireshark -k -i -

在宿主机实时观察 freeradius -X 日志:

1
$ docker logs -f radius-server

运行测试:

1
2
3
4
5
6
7
8
# 这里 -a 172.18.1.3 是指向的 radius 服务器地址(eapol_test 不支持使用域名)
# -s 是与 nas 客户端的共享秘钥
eapol_test -c /root/test/ttls-pap.conf -a 172.18.1.3 -p 1812 -s testing123
...
WPA: Clear old PMK and PTK
EAP: deinitialize previously used EAP method (21, TTLS) at EAP deinit
MPPE keys OK: 1 mismatch: 0
SUCCESS


认证过程分析

完整认证流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
[客户端 (eapol_test)]                        [FreeRADIUS 服务器]
| |
|-------------- 1. EAP-Response/Identity ----->| (账号: owlias01)
|<------------- 2. TLS Server Hello -----------| (下发服务器证书)
|-------------- 3. Client Key Exchange ------->| (完成 TLS 密钥交换)
| |
|======= [ 建立 TLS 加密隧道 (EAP-TTLS) ] =======|
| |
|---- 4. Inner PAP Auth Request (owlias01) --->| (隧道内传输凭据)
| | [数据库 BCrypt 校验成功]
|<--- 5. Inner PAP Auth Response (Success) ----|
| |
|<--- 6. EAP-Success / RADIUS Access-Accept ---| (认证最终通过!)


阶段一

外层 TLS 握手与加密隧道建立(Outer Tunnel):此阶段的目标是在不安全的网络介质上,建立一条受 TLS 保护的安全加密管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[客户端 (eapol_test)]                                  [FreeRADIUS 服务器]
| |
|----- 1. EAP-Response / Identity ("owlias01") -------v-->|
| |
|<---- 2. EAP-Request / EAP-TTLS (Start) -----------------|
| |
|----- 3. Client Hello (TLS) ---------------------------->|
| |
|<---- 4. Server Hello + Certificate + Server Hello Done -|
| |
|---- [ 关键校验:客户端验证服务器证书 ] ----------------------|
| |
|----- 5. Client Key Exchange + Change Cipher Spec ------>|
| |
|<---- 6. Change Cipher Spec + Finished ----------------->|
| |
|======= [ TLS 加密隧道协商成功 (Session Established) ] =====|

Step 1-2:身份声明与协议切

  • 客户端提交外层身份 User-Name = “owlias01”(若开启匿名,外层通常为 anonymous)。
  • 服务器收到后,回应 EAP-TTLS Start,通知客户端准备开启 TLS 握手。

Step 3-4:TLS 握手与证书下发

  • Client Hello:客户端向服务器发送支持的 TLS 版本(如 TLS 1.2/1.3)和加密套件列表(Cipher Suites),并附带客户端随机数 Client Random。
  • Server Hello:服务器挑选加密套件(如 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),并附带服务器随机数 Server Random。
  • Certificate(下发服务器证书):
    • 下发内容:服务器将自身的实体证书(Server Certificate)以及中间 CA 证书链发送给客户端。
    • 证书细节(结合抓包数据):Subject、Issuer…
  • 客户端如何校验服务器证书?👈🏻
    • 客户端拿到服务器证书后,必须在本地完成以下 4 项校验:
      • 签名链有效性(Chain of Trust):客户端使用本地预存的 ca.pem(根证书)的公钥,去解密并校验服务器证书上的 CA 签名。若能解开且哈希匹配,证明证书确实由信任的 CA 颁发。
      • 有效期检查(Validity):检查当前时间(如 2026-07-22)是否处于证书的 notBefore 与 notAfter 之间。
      • 域名/身份匹配(Subject Matching):检查证书的 CN 或 SAN 是否匹配客户端配置预期的服务器名称。
      • 证书吊销状态(CRL/OCSP):检查证书是否在吊销列表

Step 5-6:密钥交换与隧道激活

  • 客户端生成 Pre-Master Secret,通过 ECDHE/RSA 算法与服务器交换。
  • 双方利用 Client Random + Server Random + Pre-Master Secret 共同算出对称密钥 Master Secret。
  • 双方发送 Change Cipher Spec 和 Finished 报文,TLS 加密管道正式建立。


阶段二

隧道内 PAP 凭据传输与数据库校验(Inner Tunnel):此阶段的目标是在安全的 TLS 隧道保护下,安全地传输用户明文密码并完成身份鉴权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[客户端 (eapol_test)]                                  [FreeRADIUS 服务器]
| |
|======= [ 此时所有交互数据均经过 TLS 加密保护 ] =============|
| |
|----- 7. Tunneled Request (User-Name + User-Password) ->|
| |
| [ FreeRADIUS 内部处理逻辑 ] |
| 1. sql 模块查询 radcheck 表 |
| 2. 提取 Crypt-Password 哈希 |
| 3. pap 模块计算 BCrypt 比对 |
| |
|<---- 8. Tunneled Response (Access-Accept) --------------|
| |
|<---- 9. EAP-Success / RADIUS Access-Accept -------------|

Step 7:内层 PAP 请求发送。

  • 客户端将真正的敏感凭据打包为 AVP(Attribute-Value Pairs)送入 TLS 隧道:
    • User-Name = “owlias01”
    • User-Password = “123456” (即便在 PAP 中它是明文,但在外层 TLS 隧道的包裹下是绝对安全的)

Step 8:FreeRADIUS 内部校验过程。

  • 触发 inner-tunnel 虚拟服务器:FreeRADIUS 收到解密后的内层请求,将请求交由 sites-enabled/inner-tunnel 处理。
  • 执行 sql 模块:
    • 执行查询:SELECT attribute, value FROM radcheck WHERE username = ‘owlias01’
    • 查出数据库中的密码哈希:Crypt-Password = “$2b$10$wQ7tX5M…”
  • 执行 pap 模块:
    • pap 模块拿到客户端传来的明文 User-Password(123456)和数据库中的 Crypt-Password(BCrypt 哈希)。
    • 调用底层的 C 库 crypt() 函数,对明文应用相同盐值的 BCrypt 计算,比对摘要值。
    • 比对成功,返回 [pap] = ok。

Step 9:成功响应。

  • inner-tunnel 返回内层 Access-Accept
  • 外层 FreeRADIUS 收到内层成功通知,将最终的 EAP-Success (Code 3) 封装在 RADIUS Access-Accept (Code 2) 报文中,回复给客户端/NAS 设备。