feat(core): add --insecure flag to skip TLS verification for self-signed endpoints (#3535) - #5962
Conversation
…ned endpoints (QwenLM#3535) Enable skipping TLS certificate verification for outbound model API connections via a new --insecure CLI flag, QWEN_TLS_INSECURE, or NODE_TLS_REJECT_UNAUTHORIZED=0. The setting is applied to the undici dispatcher Qwen Code installs: a direct connection uses connect TLS options, while a proxied connection disables verification for the upstream origin (requestTls) and a self-signed HTTPS proxy (proxyTls). Off by default; behavior is unchanged when not enabled. Fixes QwenLM#3535 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wenLM#3535) - Block project .env from enabling QWEN_TLS_INSECURE by adding it to PROJECT_ENV_HARDCODED_EXCLUSIONS, so an untrusted repo cannot silently disable TLS verification for all API connections. - Remove the unverifiable Bun fetch `tls` special-casing; Bun users can still opt out via NODE_TLS_REJECT_UNAUTHORIZED=0, which Bun honors natively. - Do not suggest `--insecure` in the TLS error hint when verification is already disabled; show a network/protocol-oriented message instead. - Add tests: env-flag regex/falsy branches, loadCliConfig env side-effect, fetch hint variants, and a security guard for the project .env exclusion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review! Addressed all four points in
|
- Block NODE_TLS_REJECT_UNAUTHORIZED from project .env too (initial load only consults PROJECT_ENV_HARDCODED_EXCLUSIONS), since isTlsVerificationDisabled() honors it. - When opting out, set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide in loadCliConfig and emit a stderr MITM warning. This makes the opt-out effective on the Bun runtime and the proxy-creation fallback path (which the undici dispatcher does not cover), and gives a user-visible signal. - Avoid evaluating isTlsVerificationDisabled() twice on the proxy path via a default parameter on getOrCreateSharedDispatcher. - Update tests accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
eaac310 to
59efc10
Compare
…wenLM#3535) - Broaden the TLS-disabled warning to state the process-wide blast radius (API, OAuth, MCP servers, child processes), since NODE_TLS_REJECT_UNAUTHORIZED=0 is set process-wide. - Also emit the warning via debugLogger so the state is discoverable in ~/.qwen/debug/ after terminal scrollback is gone. - Add tests for the env-var-only path (pre-set QWEN_TLS_INSECURE) and the already-disabled guard (no duplicate assignment/warning). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @wenshao for the thorough review — addressed everything across First batch (
Second batch (
Design note: the undici dispatcher options are kept as the deterministic guarantee for the Node path (not reliant on runtime env propagation into undici); the process-wide env var covers the runtimes/paths the dispatcher doesn't (Bun, proxy fallback, and non-dispatcher HTTPS). |
wenshao
left a comment
There was a problem hiding this comment.
All R1 findings are addressed in this round. The warning now correctly covers the full blast radius (API calls, OAuth, MCP servers, child processes), debugLogger.warn is emitted alongside the console warning, and the test gaps (env-var-only path, NODE_TLS already-'0' guard, regex branches) are filled.
Build passes, all 312 tests pass, deterministic analysis (tsc + eslint) is clean.
LGTM ✅
— qwen3.7-max via Qwen Code /review
✅ Maintainer local verification — real self-signed TLS + real
|
| env at request time | real outcome |
|---|---|
| (none — default) | REJECTED — cause = DEPTH_ZERO_SELF_SIGNED_CERT |
QWEN_TLS_INSECURE=1 / yes |
200 OK (real body returned) |
QWEN_TLS_INSECURE=0 / enabled (falsy/unrecognized) |
REJECTED |
NODE_TLS_REJECT_UNAUTHORIZED=0 |
200 OK |
NODE_TLS_REJECT_UNAUTHORIZED=1 |
REJECTED |
Plus the full isTlsVerificationDisabled() parsing matrix (24 cases: 1/true/yes/on incl. case-insensitive + trimmed → enabled; 0/false/no/off/""/2/enabled → ignored) and dispatcher cache separation (secure ≠ insecure instance; same args → same cached instance) — all pass.
4. Real qwen binary — end-to-end (packages/cli/dist/index.js)
Ran the actual CLI against a self-signed HTTPS OpenAI-compatible server that logs every request whose TLS handshake succeeded:
| run | result | requests reaching server |
|---|---|---|
| baseline (no flag) | API Error: Connection error. (cause: fetch failed) |
0 (TLS rejected) |
--insecure |
MITM warning printed + model reply PONG_FROM_SELF_SIGNED_SERVER returned |
2 (TLS bypassed) |
QWEN_TLS_INSECURE=1 (env) |
model reply returned | reached server |
NODE_TLS_REJECT_UNAUTHORIZED=0 (env) |
model reply returned | reached server |
The --insecure run surfaces both the project's own warning "TLS certificate verification is disabled … vulnerable to man-in-the-middle attacks" and Node's native NODE_TLS_REJECT_UNAUTHORIZED=0 warning. Secure-by-default is confirmed: the baseline run is rejected and never reaches the server.
5. Mutation testing — the guard is load-bearing & tests are not vacuous
Removed connect: { rejectUnauthorized: false } from the no-proxy Agent, rebuilt, and re-ran:
- unit: 2 red —
disables verification on the no-proxy Agent via QWEN_TLS_INSECURE,honors NODE_TLS_REJECT_UNAUTHORIZED=0 for parity. - real harness:
QWEN_TLS_INSECURE=1flips from 200 → REJECTED (proves theconnectoption, not something incidental, is what enables the bypass for that path). - (interesting)
NODE_TLS_REJECT_UNAUTHORIZED=0still connected after the mutation — i.e. a plainundiciAgent independently honors the global env here. Restored + rebuilt; harness back to 35/35.
6. ⚠️ Scope-accuracy discrepancy (please reconcile before merge)
The PR's Risk & Scope says: "MCP server transport (EnvHttpProxyAgent) and telemetry use independent paths and are intentionally left verifying; this PR scopes to model API connections." The actual behavior is broader:
config.tssetsNODE_TLS_REJECT_UNAUTHORIZED='0'process-wide whenever insecure is enabled (verified — and asserted byconfig.test.ts).grep rejectUnauthorized packages/core/src(outsideruntimeFetchOptions.ts) returns nothing: MCP transport, telemetry, OAuth and the IDE client do not pin verification, so they inherit that global. §5 empirically showed a plainundiciAgent honorsNODE_TLS_REJECT_UNAUTHORIZED=0.
So --insecure disables TLS verification process-wide, not just for model API calls — exactly what the runtime warning correctly states ("All HTTPS connections in this process (API calls, OAuth, MCP servers, child processes)…"). The warning is honest; the "intentionally left verifying" bullet in the description contradicts it. Not a code bug — the broad, clearly-warned opt-out is a defensible design — but the description should be corrected so a reviewer/operator isn't misled into thinking MCP/telemetry stay verified.
Verdict
Functionally LGTM, with one doc fix requested. Build clean, 312 unit tests green, 35/35 real-TLS checks green, real-binary E2E flips reject → connect under --insecure with a clear MITM warning, secure-by-default confirmed, mutation proves the guard is real. Suggest reconciling the §6 scope wording (the implementation is process-wide; the warning already says so). CI: Test (ubuntu-latest) — the required static-check leg — is green; mac/win show skipping (named-job placeholders), consistent with this repo's required-check mechanism.
🇨🇳 中文版(完整对应)
✅ 维护者本地验证 —— 真实自签名 TLS + 真实 qwen 二进制 E2E
测试基准: commit 755a9c7(== 当前 PR head)· macOS (darwin) · Node v22.22.2 · 基于 origin/main 的独立 git worktree。
作为安全特性,我针对一个真实的自签名 HTTPS 端点(OpenSSL 证书,subject == issuer == CN=localhost)跑通了真实代码路径:单测套件、用编译产物 buildRuntimeFetchOptions() + 真实 undici 驱动的 dispatcher 级 harness、变异测试,以及真实 qwen 二进制的端到端运行。该特性在三种触发方式下都生效,且默认安全。PR 描述里有一处范围表述不准,见 §6,建议合并前修正。
1. 构建与类型检查 —— 干净
tsc --build @qwen-code/qwen-code-core → exit 0 · tsc --noEmit packages/cli → exit 0。
2. 单元测试 —— 4 文件,312 测试,全部通过
vitest run runtimeFetchOptions.test.ts fetch.test.ts config.test.ts shared-env-keys.test.ts → 312 passed。其中 config.test.ts(241)断言 --insecure → QWEN_TLS_INSECURE=1 且 NODE_TLS_REJECT_UNAUTHORIZED=0;fetch.test.ts 断言排障提示同时包含 NODE_EXTRA_CA_CERTS 与 --insecure(且在已禁用校验时省略它们)。
3. 真实自签名 TLS —— dispatcher harness(35/35)
harness 导入编译后的 runtimeFetchOptions.js,构建真实 dispatcher,用真实 undici fetch 访问真实的自签名 https://localhost 服务。单测只针对被 mock 的 undici 断言选项形状;本步证明这些选项真的改变了 socket 行为:
| 请求时的 env | 真实结果 |
|---|---|
| (无 —— 默认) | 被拒 —— cause = DEPTH_ZERO_SELF_SIGNED_CERT |
QWEN_TLS_INSECURE=1 / yes |
200 OK(返回真实 body) |
QWEN_TLS_INSECURE=0 / enabled(假值/不识别) |
被拒 |
NODE_TLS_REJECT_UNAUTHORIZED=0 |
200 OK |
NODE_TLS_REJECT_UNAUTHORIZED=1 |
被拒 |
外加完整的 isTlsVerificationDisabled() 解析矩阵(24 例:1/true/yes/on 含大小写不敏感 + 去空白 → 启用;0/false/no/off/""/2/enabled → 忽略),以及 dispatcher 缓存分离(secure ≠ insecure 实例;相同参数 → 同一缓存实例)—— 全部通过。
4. 真实 qwen 二进制 —— 端到端(packages/cli/dist/index.js)
用真实 CLI 访问一个会记录「TLS 握手成功后到达的每个请求」的自签名 HTTPS OpenAI 兼容服务:
| 运行 | 结果 | 到达服务器的请求数 |
|---|---|---|
| 基线(无 flag) | API Error: Connection error. (cause: fetch failed) |
0(TLS 被拒) |
--insecure |
打印 MITM 警告 + 返回模型回复 PONG_FROM_SELF_SIGNED_SERVER |
2(TLS 被绕过) |
QWEN_TLS_INSECURE=1(env) |
返回模型回复 | 到达服务器 |
NODE_TLS_REJECT_UNAUTHORIZED=0(env) |
返回模型回复 | 到达服务器 |
--insecure 运行同时打印了项目自身的警告*「TLS certificate verification is disabled … vulnerable to man-in-the-middle attacks」*以及 Node 原生的 NODE_TLS_REJECT_UNAUTHORIZED=0 警告。默认安全已确认:基线运行被拒、从未到达服务器。
5. 变异测试 —— 守卫是承重的,测试非空过
从 no-proxy Agent 去掉 connect: { rejectUnauthorized: false },重建并复跑:
- 单测:2 个红 ——
disables verification on the no-proxy Agent via QWEN_TLS_INSECURE、honors NODE_TLS_REJECT_UNAUTHORIZED=0 for parity。 - 真实 harness:
QWEN_TLS_INSECURE=1从 200 → 被拒(证明是connect选项、而非别的偶然因素,使该路径绕过校验)。 - (有意思)变异后
NODE_TLS_REJECT_UNAUTHORIZED=0仍能连上 —— 即此处普通undiciAgent 会独立遵从该全局 env。已还原 + 重建;harness 回到 35/35。
6. ⚠️ 范围表述不准(建议合并前修正)
PR 的 Risk & Scope 写道:「MCP server transport(EnvHttpProxyAgent)与 telemetry 走独立路径,有意保持校验;本 PR 范围限定为模型 API 连接。」 实际行为更广:
- 只要启用 insecure,
config.ts就进程级设置NODE_TLS_REJECT_UNAUTHORIZED='0'(已验证,且config.test.ts有断言)。 grep rejectUnauthorized packages/core/src(除runtimeFetchOptions.ts外)无任何命中:MCP 传输、telemetry、OAuth、IDE client 都没有独立钉住校验,因此会继承该全局;§5 已实证普通undiciAgent 遵从NODE_TLS_REJECT_UNAUTHORIZED=0。
所以 --insecure 是进程级关闭 TLS 校验,而不仅是模型 API —— 这正是运行时警告所如实描述的(「All HTTPS connections in this process(API calls, OAuth, MCP servers, child processes)…」)。警告是诚实的;描述里*「有意保持校验」*那条与之矛盾。这不是代码 bug —— 范围广但有明确警告的 opt-out 是合理设计 —— 但建议修正描述,以免评审者/运维误以为 MCP/telemetry 仍在校验。
结论
功能上 LGTM,仅建议修一处文档。 构建干净、312 单测全绿、35/35 真实 TLS 检查全绿、真实二进制 E2E 在 --insecure 下把「拒绝」翻转为「连接」并带清晰 MITM 警告、默认安全已确认、变异证明守卫真实有效。建议把 §6 的范围措辞对齐(实现是进程级,警告也已如此说明)。CI:Test (ubuntu-latest)(必需的静态检查腿)为绿;mac/win 显示 skipping(named-job 占位),与本仓库 required-check 机制一致。
|
@qwen-code /triage |
|
Thanks for the PR, @TianYuan1024! Template looks good ✓ — all required sections present, bilingual, test plan included. On direction: This solves a real pain point. Users pointing Qwen Code at self-hosted endpoints with self-signed certs have no escape hatch because the custom undici dispatcher (installed for timeout control) doesn't reliably honor On approach: The scope feels right and minimal. The three activation paths ( One observation: Moving on to code review. 🔍 中文说明感谢 @TianYuan1024 的贡献! 模板完整 ✓ — 所有必填部分齐全,双语,含测试计划。 方向:解决了一个真实的痛点。用户将 Qwen Code 指向使用自签名证书的自建端点时没有逃生通道,因为自建 undici dispatcher(用于控制超时)并不可靠地遵循 方案:范围恰当且精简。三种激活路径( 一个观察:当前已安装版本对该特定端点恰好支持 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewReviewed the diff against an independent proposal for this problem. The PR's approach matches what I would have built: a single No critical blockers found:
Unit Tests (all pass)Real-Scenario TestingTested against Baseline — curl confirms self-signed certBefore (installed build, no
|
|
Stepping back to look at the whole picture: This PR solves a well-defined problem (no TLS escape hatch for self-signed endpoints behind the custom undici dispatcher) with a clean, minimal implementation. The security model is genuinely well thought through — the The before/after testing confirms the feature works as designed: Two rounds of maintainer review have already sharpened this (the This is a focused, well-tested feature PR that does one thing and does it right. No scope creep, no unnecessary abstractions, no drive-by refactors. If I had to maintain this in six months, the comments would tell me exactly why the security boundaries exist. Approving. ✅ 中文说明回顾全局:这个 PR 用精简的实现解决了一个明确的问题(自建 undici dispatcher 下自签名端点没有 TLS 逃生通道)。安全模型真正做到了深思熟虑—— 前后对比测试确认功能按设计工作: 两轮维护者审查已经打磨过( 这是一个聚焦、经过充分测试的功能 PR,做好了一件事。没有范围蔓延、没有不必要的抽象、没有顺手重构。如果六个月后需要维护这些代码,注释会清楚地告诉我安全边界存在的原因。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Adds an opt-in way to skip TLS certificate verification for outbound model API connections, so Qwen Code can talk to self-hosted endpoints that use a self-signed certificate. It is enabled by a new
--insecureCLI flag, theQWEN_TLS_INSECUREenvironment variable, or the Node conventionNODE_TLS_REJECT_UNAUTHORIZED=0. When enabled, verification is disabled on the undici dispatcher that Qwen Code installs: a direct connection uses the connector's TLS options, and a proxied connection disables verification for the upstream origin (and for an HTTPS proxy whose own certificate is self-signed). When not enabled, behavior is unchanged.Why it's needed
Because Qwen Code installs its own undici dispatcher (to control request timeouts for local backends), Node's global
NODE_TLS_REJECT_UNAUTHORIZED=0was not reliably honored for model API calls, so users pointing Qwen Code at a self-hosted server with a self-signed certificate hitConnection error. (cause: fetch failed)with no escape hatch — even though the same setting works elsewhere. This gives a clear, intentional opt-out for trusted lab / self-hosted environments, while keeping the saferNODE_EXTRA_CA_CERTS(trust the CA) as the recommended path. The flag carries an explicit man-in-the-middle warning in both--helpand the troubleshooting docs.Reviewer Test Plan
How to verify
https://localhost:8443.qwen --auth-type openai --openaiBaseUrl https://localhost:8443 --openaiApiKey sk-x --prompt "2+2?"→ expectConnection error. (cause: fetch failed)and a troubleshooting hint mentioningNODE_EXTRA_CA_CERTSand--insecure.--insecureto the same command → expect the request to reach the server (no TLS rejection).QWEN_TLS_INSECURE=1 qwen ...andNODE_TLS_REJECT_UNAUTHORIZED=0 qwen ...should behave the same.Unit coverage:
packages/core/src/utils/runtimeFetchOptions.test.tsasserts the direct Agent usesconnect: { rejectUnauthorized: false }, the ProxyAgent usesrequestTls/proxyTls(notconnect), falsyQWEN_TLS_INSECUREvalues are ignored, and secure/insecure dispatchers are cached separately.packages/cli/src/config/config.test.tscovers flag parsing.Evidence (Before & After)
N/A (non-TUI; network/TLS behavior). See test output below.
Tested on
Environment (optional)
Local unit tests (vitest) on macOS; repo-wide
lint/build/typecheckgreen.Risk & Scope
--helpand docs.EnvHttpProxyAgent) and telemetry use independent paths and are intentionally left verifying; this PR scopes to model API connections, which is what the issue asks for.Linked Issues
Fixes #3535
中文说明
这个 PR 做了什么
新增一个可选项,用于在访问模型 API 时跳过 TLS 证书校验,使 Qwen Code 能连接使用自签名证书的自建服务。可通过新增的
--insecure命令行参数、QWEN_TLS_INSECURE环境变量,或 Node 习惯的NODE_TLS_REJECT_UNAUTHORIZED=0任一开启。开启后,会在 Qwen Code 自建的 undici dispatcher 上关闭校验:直连走连接器的 TLS 选项;走代理时关闭对上游源站的校验(以及自签名 HTTPS 代理自身证书的校验)。未开启时行为完全不变。为什么需要
由于 Qwen Code 为控制本地后端的请求超时而自建了 undici dispatcher,Node 全局的
NODE_TLS_REJECT_UNAUTHORIZED=0对模型 API 调用并不可靠生效,导致用户指向自签名证书的自建服务器时遇到Connection error. (cause: fetch failed)且没有任何逃生通道——尽管同样的设置在别处有效。本改动提供一个明确、需主动开启的开关,面向受信任的实验/自建环境,同时仍把更安全的NODE_EXTRA_CA_CERTS(信任 CA)作为推荐方案。该参数在--help和排障文档中都带有中间人攻击风险提示。审阅测试计划
如何验证
https://localhost:8443)。qwen --auth-type openai --openaiBaseUrl https://localhost:8443 --openaiApiKey sk-x --prompt "2+2?"→ 预期报Connection error. (cause: fetch failed),并提示NODE_EXTRA_CA_CERTS与--insecure。--insecure→ 预期请求能到达服务器(不再被 TLS 拒绝)。QWEN_TLS_INSECURE=1 qwen ...与NODE_TLS_REJECT_UNAUTHORIZED=0 qwen ...行为相同。单测覆盖:
runtimeFetchOptions.test.ts断言直连 Agent 使用connect: { rejectUnauthorized: false }、ProxyAgent 使用requestTls/proxyTls(而非connect)、忽略QWEN_TLS_INSECURE的假值、以及 secure/insecure dispatcher 分开缓存;config.test.ts覆盖参数解析。证据(前后对比)
N/A(非 TUI;属网络/TLS 行为),见测试输出。
测试平台
仅在 macOS 本地跑了单测;全仓
lint/build/typecheck通过。风险与范围
--help与文档中警告来缓解。EnvHttpProxyAgent)与遥测走独立路径,本 PR 有意保持校验;范围限定为模型 API 连接,即 issue 的诉求。关联 Issue
Fixes #3535