
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_result与cluster_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.erl中summarize_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.erlis_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/1、redact/2可传入自定义敏感判定函数、redact_headers/1、is_sensitive_key/1、is_redacted/2,3与deobfuscate/2用于把脱敏占位符恢复为旧配置中的真实值支撑 API 回显。do_redact/2的递归逻辑按数据结构分派list逐元素递归脱敏且能正确处理 improper list非规范链表map遍历每个键值对do_redact(K, V, Checker)先判断键是否敏感敏感则替换值否则对值递归{headers, Value}特殊形态走do_redact_headers/1对 HTTP 头列表/映射做大小写不敏感的键名匹配Authorization、api-key、cookie、proxy-authorization、x-api-key、x-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_()测试中构造了携带原始密钥的 recordSecret abcd1234, JWK {jose_jwk, undefined, {jose_jwk_kty_oct, Secret}, #{}},测试断言redact(JWK)结果为{jose_jwk, ******}——record 被整体替换嵌套在current_jwk等非敏感键下的 record 同样被替换#{current_jwk : {jose_jwk, ******}, other : value}模拟真实配置更新场景的深层嵌套 stateemqx_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_result与cluster_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 并存入认证器 statejwk 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 场景中同样受新脱敏逻辑保护。修复的整体效果与使用建议综合源码实现与测试用例本次修复形成了三条环环相扣的防线记录形状而非值cluster_rpc_apply_result/cluster_rpc_apply_ok只输出{ok, _}摘要成功返回的运行时状态含编译后的认证器、header 模板、JWK根本不进入日志识别 record 形状脱敏器优先匹配{jose_jwk, _, _, _}record 并整体折叠为占位符杜绝泛化元组遍历触及kty_oct/kty_rsa等密钥字段键名兜底jwk字段被纳入is_sensitive_key/1原子/字符串/二进制三种形态非 record 形态的密钥 map 也会被替换。对于使用 EMQX 集群、尤其是启用了 JWT HMAC 认证或基于 JWKS 端点认证的用户本次修复意味着即使开启 debug 级别日志、在配置热更新期间排查cluster_rpc_apply_result与cluster_rpc_apply_ok日志也不会再看到 JWT 共享密钥字节以明文形式出现。如需验证修复效果可直接运行emqx_utils的 EUnit 测试jose_jwk_redact_test_与emqx_cluster_rpc_SUITE的t_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),仅供参考