EMQX 日志脱敏增强:阻止 JWT HMAC 密钥在 cluster RPC 配置更新日志中泄露
2026/9/24 16:19:21 网站建设 项目流程

EMQX 日志脱敏增强:阻止 JWT HMAC 密钥在 cluster RPC 配置更新日志中泄露

【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址: https://gitcode.com/gh_mirrors/em/emqx

本文围绕 EMQX 开源仓库中changes/ee/fix-17791.en.md所记录的修复展开:EMQX 改进了日志脱敏(log redaction)能力,使 JWT HMAC 密钥字节不再出现在配置更新期间产生的cluster_rpc_apply_resultcluster_rpc_apply_ok两类 debug 日志行中。读完本文,你将理解 EMQX 集群配置同步(cluster RPC)路径上的日志记录机制、emqx_utils_redact脱敏器的工作原理,以及修复中新增的两道防护:识别内部 JWK record 形状并整体替换、将jwk字段视为敏感字段。

背景:集群配置同步与日志泄露风险

EMQX 在集群模式下通过emqx_cluster_rpc(位于 apps/emqx_conf/src/emqx_cluster_rpc.erl)把配置变更以事务(transaction)方式广播到集群内各节点执行。当管理员通过 Dashboard、HTTP API 或 CLI 修改认证、桥接等配置时,配置更新会被封装为 MFA(模块-函数-参数)提交到集群,各节点随后执行apply_mfa/3完成实际更新。

配置更新携带的参数可能包含敏感信息(密码、Token、私钥等),因此apply_mfa/3在构造日志元数据时特别注明:

%% Do not log args as it might be sensitive information Meta = #{kind => Kind, tnx_id => TnxId, entrypoint => format_mfa(M, F, length(A))},

即日志中只记录 MFA 的入口签名(模块、函数、参数个数),绝不记录参数内容本身。真正存在风险的环节,是 MFA执行成功后的返回值——对于认证器(authenticator)这类带运行时状态的配置项,返回的 state 中可能内嵌编译后的运行时对象,例如 JWT HMAC 认证器会用密钥构造出的jose_jwkrecord,其kty_oct字段中存放的正是原始 HMAC 密钥字节。

修复前的泄露路径:成功结果的"值"进入 debug 日志

修复前,cluster RPC 在记录执行结果时存在一条潜在的密钥泄露路径。配置更新成功后,节点会把返回值写入 debug 日志行,而返回值是一个复合 term——例如 JWT HMAC 认证器的 state 形如:

#{ jwk => {jose_jwk, undefined, {jose_jwk_kty_oct, <<"原始 HMAC 密钥字节">>}, #{}}, allowed_algs => [...], verify_claims => ... }

emqx_utils:redact/1的通用脱敏器(见 apps/emqx_utils/src/emqx_utils_redact.erl)虽然能按敏感键名递归替换值,但在修复前它不认识jose_jwk这种内部 record 结构:泛化的元组遍历逻辑会继续深入 record 的每个字段,{jose_jwk_kty_oct, Secret}这类携带原始密钥的二元组并不命中任何敏感键名,于是原始密钥字节会随 debug 日志行原样写出。

emqx_cluster_rpc.erlsummarize_ok/1的文档注释也印证了这一点:

-doc """ Report the shape of a successful result, never its value. The value can carry compiled runtime state, such as the HTTP authenticator header templates, which holds secrets emqx_utils:redact/1 does not cover. """. summarize_ok(ok) -> ok; summarize_ok({ok, _}) -> {ok, '_'}.

也就是说,成功返回的值中可能携带编译后的运行时状态(例如 HTTP 认证器的 header 模板),而其中包含的秘密无法被emqx_utils:redact/1覆盖。

修复机制一:成功结果只记录"形状",不记录"值"

修复首先从源头收紧日志内容。在 apps/emqx_conf/src/emqx_cluster_rpc.erl 中,成功路径的日志一律通过summarize_ok/1把返回值折叠成形状摘要:

log_and_alarm(IsSuccess, Res, #{kind := ?KIND_INITIATE} = Meta) -> case IsSuccess of true -> ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_result", result => summarize_ok(Res)}); false -> ?SLOG(warning, Meta#{ msg => "cluster_rpc_failed_to_init_transaction", result => emqx_utils:redact(Res) }) end; log_and_alarm(true, Res, Meta) -> Summary = summarize_ok(Res), ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_ok", result => Summary}), do_alarm(deactivate, Summary, Meta); log_and_alarm(false, Res, Meta) -> ?SLOG(error, Meta#{msg => "cluster_rpc_apply_failed", result => emqx_utils:redact(Res)}), do_alarm(activate, Res, Meta).
  • 成功cluster_rpc_apply_result(事务发起节点)与cluster_rpc_apply_ok(各应用节点)都只记录{ok, '_'}这种形状占位,真实返回值完全不进入日志;
  • 失败cluster_rpc_apply_failed仍通过emqx_utils:redact(Res)记录错误项——错误本身是排障必需信息,且经过脱敏器处理。

修复机制二:脱敏器识别内部 JWK record 形状

仅靠"记录形状"并不足够:JWT HMAC 密钥还可能以jose_jwkrecord 的形式出现在其它日志路径(如 trace、告警元数据、进程状态 dump)中。因此修复在脱敏器层面补上了第二道防护——让emqx_utils_redact直接识别内部 JWK record 形状并整体替换。

在 apps/emqx_utils/src/emqx_utils_redact.erl 中,do_redact/2针对jose_jwkrecord 增加了专门的分支:

do_redact({jose_jwk, _Keys, _Kty, _Fields}, _Checker) -> %% A jose_jwk record carries the raw key material (a kty_oct binary for HMAC, %% kty_rsa fields for RSA, etc.) in its inner tuples. Replace it wholesale so %% the generic tuple walk below never descends into the key bytes. {jose_jwk, '******'};

关键点在于"整体替换"(replace it wholesale):无论jose_jwkrecord 嵌套在什么位置、外层键名是否敏感,只要泛化遍历触达该 record,就立刻折叠为{jose_jwk, '******'}占位符,从而保证底层的{jose_jwk_kty_oct, Secret}(HMAC 原始密钥)、kty_rsa字段(RSA 密钥材料)等永远不会被继续遍历输出。通用元组遍历逻辑不会再有机会触达密钥字节。

修复机制三:jwk字段被纳入敏感键名单

除了识别 record 形状,修复还把jwk加入了is_sensitive_key/1的敏感键名单(见 apps/emqx_utils/src/emqx_utils_redact.erl):

is_sensitive_key(jwk) -> true; is_sensitive_key("jwk") -> true; is_sensitive_key(<<"jwk">>) -> true;

该函数同时接受原子(atom)、字符串(string)和二进制(binary)三种键表示形式。这意味着:

  • jwk为键的 map 条目,其值会被redact_v/1直接替换为脱敏占位符(******),无论值是jose_jwkrecord 还是普通 map(例如从 JWKS 端点拉取后缓存的密钥数据);
  • 这一层防护覆盖非 record 形态的密钥表示,与"识别 record 形状"形成互补,共同构成对 JWT 密钥的双重保险。

脱敏占位符在源码中统一定义为:

-define(REDACT_VAL, "******"). redacted_value() -> <<?REDACT_VAL>>.

脱敏器的整体结构:从键名匹配到值替换

理解本次修复,还需要了解emqx_utils_redact的整体工作方式。其对外入口包括redact/1redact/2(可传入自定义敏感判定函数)、redact_headers/1is_sensitive_key/1is_redacted/2,3deobfuscate/2(用于把脱敏占位符恢复为旧配置中的真实值,支撑 API 回显)。

do_redact/2的递归逻辑按数据结构分派:

  • list:逐元素递归脱敏,且能正确处理 improper list(非规范链表);
  • map:遍历每个键值对,do_redact(K, V, Checker)先判断键是否敏感,敏感则替换值,否则对值递归;
  • {headers, Value}特殊形态:走do_redact_headers/1,对 HTTP 头列表/映射做大小写不敏感的键名匹配(Authorizationapi-keycookieproxy-authorizationx-api-keyx-auth-token等头名会被脱敏);
  • tuple:泛化遍历,而{jose_jwk, _, _, _}record 分支在元组遍历之前被优先匹配,直接整体替换;
  • 其它 term:原样保留。

对敏感键的值,redact_v/1还做了几个精细处理:

  • 值为${var}形式的占位符模板(如${password}不脱敏——因为模板本身不含真实秘密(见 apps/emqx_utils/test/emqx_utils_redact_tests.erl 中no_redact_template_var_test);
  • 值为file://...形式的文件路径不脱敏——路径不是密钥本身;
  • emqx_secret:wrap/1包装的密钥值会被识别并脱敏。

测试验证:双重防护的落地证据

本次修复配套了完善的测试用例,可以从源码中直接验证。

JWK record 脱敏单元测试

在 apps/emqx_utils/src/emqx_utils_redact.erl 内置的jose_jwk_redact_test_()测试中,构造了携带原始密钥的 record:

Secret = <<"abcd1234">>, JWK = {jose_jwk, undefined, {jose_jwk_kty_oct, Secret}, #{}},

测试断言:

  • redact(JWK)结果为{jose_jwk, '******'}——record 被整体替换;
  • 嵌套在current_jwk等非敏感键下的 record 同样被替换:#{current_jwk := {jose_jwk, '******'}, other := <<"value">>}
  • 模拟真实配置更新场景的深层嵌套 state(emqx_authn_config => #{state => #{jwk => JWK, ...}})经脱敏后,用io_lib:format("~p", ...)渲染出的日志文本中binary:match找不到原始密钥<<\"abcd1234\">>
  • jwk键的 map 值(非 record 形态)被替换为******#{jwk => ?REDACT_VAL}

cluster RPC 日志不泄露测试

在 apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl 中,t_apply_result_not_logged/1专门验证"成功的 cluster-RPC apply 只记录结果的形状,绝不记录值":

OkReports = emqx_cth_log_capture:capture(debug, fun() -> {M, F, A} = {?MODULE, echo_compiled, [Marker]}, ?assertMatch({ok, _, {ok, #{compiled := Marker}}}, multicall(M, F, A)) end), ?assertMatch( [#{result := {ok, '_'}}], [R || #{msg := "cluster_rpc_apply_result"} = R <- OkReports] ), AppliedReports = [R || #{msg := "cluster_rpc_apply_ok"} = R <- OkReports], ?assertMatch([_, _ | _], AppliedReports), lists:foreach(fun(R) -> ?assertMatch(#{result := {ok, '_'}}, R) end, AppliedReports), ?assertEqual( nomatch, binary:match(iolist_to_binary(io_lib:format("~p", [OkReports])), Marker) ),

测试通过emqx_cth_log_capture:capture(debug, ...)捕获 debug 日志,然后断言:

  • cluster_rpc_apply_resultcluster_rpc_apply_ok两类日志行的result字段都是{ok, '_'}形状占位;
  • 对整批捕获的日志文本全文检索,找不到原始标记值Markerbinary:match返回nomatch);
  • 同时测试还验证了失败路径仍保留错误信息:cluster_rpc_apply_failed日志的result"MFA return not ok"之类的错误项,排障信息不因脱敏而丢失。

JWT 认证路径自身的脱敏

在 JWT 认证模块 apps/emqx_auth_jwt/src/emqx_authn_jwt.erl 中,HMAC 模式通过jose_jwk:from_oct(Secret)把共享密钥构造为 JWK record 并存入认证器 state(jwk => JWK),这正是修复针对的核心场景之一。该模块也维护着自己的日志脱敏函数:

redact_jwk_for_log(#jose_jwk{kty = {jose_jwk_kty_oct, _}}) -> <<"******">>; redact_jwk_for_log(JWK) -> JWK.

kty_oct类型的 JWK(即 HMAC 密钥),一律以******出现在 trace 日志中。与之配套,apps/emqx_auth_jwt/src/emqx_authn_jwks_client.erl 中的 JWKS 客户端在内存中缓存jose_jwkrecord 列表,这些对象在日志、dump 场景中同样受新脱敏逻辑保护。

修复的整体效果与使用建议

综合源码实现与测试用例,本次修复形成了三条环环相扣的防线:

  1. 记录形状而非值cluster_rpc_apply_result/cluster_rpc_apply_ok只输出{ok, '_'}摘要,成功返回的运行时状态(含编译后的认证器、header 模板、JWK)根本不进入日志;
  2. 识别 record 形状:脱敏器优先匹配{jose_jwk, _, _, _}record 并整体折叠为占位符,杜绝泛化元组遍历触及kty_oct/kty_rsa等密钥字段;
  3. 键名兜底jwk字段被纳入is_sensitive_key/1(原子/字符串/二进制三种形态),非 record 形态的密钥 map 也会被替换。

对于使用 EMQX 集群、尤其是启用了 JWT HMAC 认证或基于 JWKS 端点认证的用户,本次修复意味着:即使开启 debug 级别日志、在配置热更新期间排查cluster_rpc_apply_resultcluster_rpc_apply_ok日志,也不会再看到 JWT 共享密钥字节以明文形式出现。如需验证修复效果,可直接运行emqx_utils的 EUnit 测试(jose_jwk_redact_test_)与emqx_cluster_rpc_SUITEt_apply_result_not_logged用例,相关实现与测试均可在当前仓库的 apps/emqx_utils/src/emqx_utils_redact.erl、apps/emqx_conf/src/emqx_cluster_rpc.erl 与 apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl 中查阅。

【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址: https://gitcode.com/gh_mirrors/em/emqx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询