coturn认证实战:长期凭证与限时密钥双方案全走通
【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn
凌晨3点,WebRTC通话服务的告警响了:TURN请求批量401。排查发现是上一周有人改了coturn的--realm参数却没通知客户端——长期凭证的完整性密钥是MD5(用户名:realm:密码)推导出来的,realm一变,所有客户端算出的Message-Integrity全部对不上,无一幸免。这个故障暴露了coturn认证的两个核心点:密钥到底怎么算、realm和timestamp在链路里起什么作用。下面按"先选型、再走通、后调优"的顺序讲清楚。
先做决策:该用哪套认证机制
coturn支持两套互斥的认证机制,选型只需回答三个问题:
- 用户数量固定且很少变动?→ 长期凭证(long-term credential,
--lt-cred-mech/-a) - 用户动态生成、需要凭证自动过期?→ 限时密钥(auth secret,
--use-auth-secret),基于时间戳+共享密钥派生短期凭证 - 用户数过万、要集中管理?→ 都配数据库后端(MySQL/PostgreSQL/Redis等),
turnserver --use-db-mysql ...
| 维度 | 长期凭证 | 限时密钥 |
|---|---|---|
| 凭据生命周期 | 永久,人工轮换 | 时间戳过期,自动失效 |
| 服务端存储 | 每用户一条密码/密钥 | 仅1个(或几个)共享密钥 |
| 密钥轮换粒度 | 逐用户改密码 | 换一次secret全量生效 |
| 重放风险 | 密码可长期复用 | 过去时间戳直接拒收 |
两套机制不能混用:同时给--lt-cred-mech和--use-auth-secret,启动时会打CONFIG警告,且实际以限时密钥为准。
方案A 长期凭证:从401到认证成功的一条完整链路
客户端第一次发Allocate请求时只带USERNAME,不带密码。coturn在get_user_key()里查不到用户,回401并在响应里塞REALM。客户端拿到realm后开始算密钥:
integrity_key = MD5(username : realm : password) Message-Integrity = base64(HMAC-SHA1(integrity_key, 整个请求包))客户端把USERNAME、REALM、MESSAGE-INTEGRITY三个属性补齐后重发。服务端再次进入get_user_key(),这次先查启动时用--user预计算好的静态账户表:
ur_string_map_lock(turn_params.default_users_db.ram_db.static_accounts); if (ur_string_map_get(..., usname, &ukey)) { ret = 0; // 用户名命中:ukey就是启动时算好的integrity_key } ur_string_map_unlock(...); // ... 静态表未命中时,回落到数据库驱动 dbd->get_user_key(usname, realm, key)源码位置:src/apps/relay/userdb.c→get_user_key()
验证通过,分配流程继续。你要改的配置就三行(参考 examples/scripts/longtermsecure/secure_relay.sh):
turnserver --lt-cred-mech --realm=north.gov --user=alice:s3cret \ --cert=turn_server_cert.pem --pkey=turn_server_pkey.pem # --user 格式 用户名:密码,可写多个;也可用 0x 开头的十六进制直接指定integrity_key注意--user的密码部分如果以0x开头,会被当作已计算好的20字节密钥,跳过MD5推导——批量导入数据库密钥时很有用。
方案B 限时密钥:时间戳就是核心
这套机制不存用户密码,只共享一个secret。客户端自己拼装凭证:
username = 1765432100:alice # 当前Unix时间戳 + ':' + 用户名 password = base64(HMAC-SHA1(secret, 完整username字符串))服务端收到后的处理在同一个get_user_key()里,顺序是:先解析时间戳、再判过期、最后才做HMAC。
ts = get_rest_api_timestamp((char *)usname); // 从 username 里拆出 ':' 前的数字 if (turn_time_before(ts, ctime)) { return ret; // 时间戳在过去 → 直接拒绝,这是重放防护 } // ... 遍历密钥列表(静态 + 数据库),逐个重算HMAC并校验 Message-Integrity源码位置:src/apps/relay/userdb.c→get_rest_api_timestamp()/get_user_key()
两个细节值得注意:时间戳解析用的是默认分隔符:(rest_api_separator),所以username里不能含裸冒号;密钥列表由get_auth_secrets()合并静态配置和数据库两部分,这为无缝轮换留了口子。服务端配置(参考 examples/scripts/restapi/secure_relay_secret.sh):
turnserver --use-auth-secret --static-auth-secret=logen --realm=north.gov # 客户端侧等价写法:turnutils_uclient -u alice -W logen <服务器地址>生产环境必做的5件事
- 限额:
--user-quota=100 --total-quota=300,单用户和全局会话数上限都设上,防止单个凭证被滥刷。 - 轮换:把secret从
--static-auth-secret迁到数据库,用turnadmin --set-secret写入新密钥,旧密钥保留一个窗口期再删——双密钥并存期间新旧客户端都能通过。 - 时钟:coturn只拒收"过去"的时间戳,远未来的时间戳同样能通过校验,所以客户端和服务端都要走NTP(
chronyc sources确认偏差<100ms)。⚠️ - 传输层:加
--tls-listen --cert=... --pkey=...走TLS,别让HMAC和realm在裸UDP里被嗅探。 - 取证:
-v --log-file=stdout保持中等日志级别,认证失败的用户名和key_lookup结果(未找到/已过期/完整性不匹配)都打在日志里,排查时直接看。
避坑速查表
| 现象 | 原因 | 修复 |
|---|---|---|
| 全部客户端401,改密码能短暂恢复 | 客户端realm与--realm不一致,MD5推导的密钥对不上 | 对齐双方realm,如turnserver --realm=north.gov |
| 只有部分客户端失败,重登就好 | 时间戳已过期(客户端时钟偏快过服务器或请求排队太久) | 修NTP:chronyc -a 'makestep' |
| username带自定义分隔符全部失败 | 默认分隔符是:,解析不出合法时间戳 | username保持时间戳:用户名格式,分隔符两侧一致 |
--user配了却不生效 | secret模式下add_static_user_account()直接返回-1,静用户被丢弃 | 二选一:去掉--use-auth-secret或去掉--user |
| 启动报"lt-cred-mech and use-auth-secret at the same time" | 两机制互斥,secret优先,长期凭证被架空 | 只保留一条认证开关 |
✅ 选型原则一句话:账号少且稳定用长期凭证,用户动态或有安全合规要求用限时密钥。完整参数清单见 README.turnserver,配置模板可直接抄 examples/etc/turnserver.conf。
【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考