From ebf412b1443792e825f24e2afdf7d5795701b77f Mon Sep 17 00:00:00 2001 From: azens <892553927@qq.com> Date: Mon, 17 Aug 2026 15:54:13 +0800 Subject: [PATCH] fix(mcp): support custom TLS trust for web backends --- Cargo.lock | 3 + crates/dbx-mcp/Cargo.toml | 4 + crates/dbx-mcp/src/backend.rs | 239 +++++++++++++++++++++++++++++++++- docs/content/docs/mcp.cn.mdx | 4 +- docs/content/docs/mcp.mdx | 4 +- packages/mcp-server/README.md | 6 +- 6 files changed, 253 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7fe6299ca..91db5147ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,11 +2133,14 @@ dependencies = [ "dirs", "reqwest 0.12.28", "rmcp", + "rustls 0.23.43", + "rustls-pemfile 2.2.0", "schemars 1.2.2", "serde", "serde_json", "tempfile", "tokio", + "tokio-rustls 0.26.4", "url", "uuid", ] diff --git a/crates/dbx-mcp/Cargo.toml b/crates/dbx-mcp/Cargo.toml index 1594080192..a9a285434e 100644 --- a/crates/dbx-mcp/Cargo.toml +++ b/crates/dbx-mcp/Cargo.toml @@ -27,4 +27,8 @@ uuid = { version = "1", features = ["v4"] } url = "2" [dev-dependencies] +rustls = { version = "0.23", features = ["aws-lc-rs"] } +rustls-pemfile = "2.2" tempfile = "3" +tokio = { version = "1", features = ["net", "io-util"] } +tokio-rustls = "0.26" diff --git a/crates/dbx-mcp/src/backend.rs b/crates/dbx-mcp/src/backend.rs index 98155e973a..aedca827b1 100644 --- a/crates/dbx-mcp/src/backend.rs +++ b/crates/dbx-mcp/src/backend.rs @@ -292,15 +292,30 @@ impl std::fmt::Debug for WebBackend { impl WebBackend { pub fn new(base_url: String, password: String) -> Result { let (proxy, no_proxy) = standard_web_proxy(&base_url); - Self::new_with_config(base_url, password, proxy, no_proxy, std::env::var("DBX_WEB_HEADERS").ok()) + let tls_skip_verify = std::env::var("DBX_WEB_INSECURE_SKIP_VERIFY") + .ok() + .map(|value| value.trim().to_ascii_lowercase()) + .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "yes" | "on")); + Self::new_with_config( + base_url, + password, + proxy, + no_proxy, + std::env::var("DBX_WEB_HEADERS").ok(), + tls_skip_verify, + std::env::var("DBX_WEB_CA_CERT").ok().filter(|value| !value.trim().is_empty()), + ) } + #[allow(clippy::too_many_arguments)] fn new_with_config( base_url: String, password: String, proxy: Option, no_proxy: Option, headers_json: Option, + tls_skip_verify: bool, + ca_cert_path: Option, ) -> Result { let base_url = base_url.trim().trim_end_matches('/').to_string(); if base_url.is_empty() { @@ -318,6 +333,23 @@ impl WebBackend { // Unset or empty proxy configuration: connect directly. builder = builder.no_proxy(); } + // TLS: mirror the Consul/Nacos client convention. Certificate + // verification is on by default; opt out with DBX_WEB_INSECURE_SKIP_VERIFY + // for self-signed endpoints, or trust a private CA with DBX_WEB_CA_CERT. + if tls_skip_verify { + builder = builder.danger_accept_invalid_certs(true); + } + if let Some(ca_cert_path) = ca_cert_path.as_deref().map(str::trim).filter(|value| !value.is_empty()) { + let path = dbx_core::path_utils::expand_tilde(ca_cert_path); + let bytes = + std::fs::read(&path).map_err(|error| format!("Failed to read DBX_WEB_CA_CERT at {path}: {error}"))?; + let certificates = reqwest::Certificate::from_pem_bundle(&bytes) + .or_else(|_| reqwest::Certificate::from_der(&bytes).map(|certificate| vec![certificate])) + .map_err(|error| format!("Failed to parse DBX_WEB_CA_CERT at {path}: {error}"))?; + for certificate in certificates { + builder = builder.add_root_certificate(certificate); + } + } let client = builder.build().map_err(|error| error.to_string())?; Ok(Self { base_url, @@ -2238,6 +2270,8 @@ mod tests { // Target host matches NO_PROXY: the request must go direct. Some("127.0.0.1".to_string()), None, + false, + None, ) .unwrap(); backend.auth.lock().await.checked = true; @@ -2295,6 +2329,8 @@ mod tests { None, None, Some(r#"{"X-API-Key":"secret","X-Tenant":"acme"}"#.to_string()), + false, + None, ) .unwrap(); @@ -2322,6 +2358,8 @@ mod tests { None, None, Some(r#"{"Authorization":"Bearer secret-token","X-Tenant":"acme"}"#.to_string()), + false, + None, ) .unwrap(); @@ -2369,9 +2407,16 @@ mod tests { .unwrap(); }); - let backend = - WebBackend::new_with_config(base_url, String::new(), Some(format!("http://{proxy_address}")), None, None) - .unwrap(); + let backend = WebBackend::new_with_config( + base_url, + String::new(), + Some(format!("http://{proxy_address}")), + None, + None, + false, + None, + ) + .unwrap(); backend.auth.lock().await.checked = true; let connections = backend.load_connections().await.unwrap(); @@ -2425,6 +2470,8 @@ mod tests { Some(format!("http://admin:admin123@{proxy_address}")), None, None, + false, + None, ) .unwrap(); backend.auth.lock().await.checked = true; @@ -2447,6 +2494,8 @@ mod tests { Some("not a url".to_string()), None, None, + false, + None, ) .unwrap_err(); assert!(error.contains("HTTP(S)_PROXY"), "{error}"); @@ -2457,6 +2506,8 @@ mod tests { None, None, Some("not json".to_string()), + false, + None, ) .unwrap_err(); assert!(error.contains("DBX_WEB_HEADERS"), "{error}"); @@ -2467,6 +2518,8 @@ mod tests { None, None, Some(r#"{"Bad Header Name":"v"}"#.to_string()), + false, + None, ) .unwrap_err(); assert!(error.contains("header name"), "{error}"); @@ -2477,6 +2530,8 @@ mod tests { None, None, Some(r#"{"X-Num":42}"#.to_string()), + false, + None, ) .unwrap_err(); assert!(error.contains("expected a string"), "{error}"); @@ -2488,6 +2543,8 @@ mod tests { None, None, Some(r#"{"Cookie":"session=1"}"#.to_string()), + false, + None, ) .unwrap_err(); assert!(error.contains("reserved by DBX"), "{error}"); @@ -2498,11 +2555,185 @@ mod tests { None, None, Some(r#"{"x-dbx-mcp-request":"1"}"#.to_string()), + false, + None, ) .unwrap_err(); assert!(error.contains("reserved by DBX"), "{error}"); } + /// Starts a TLS server with a freshly generated self-signed certificate + /// for 127.0.0.1, answering the DBX Web auth/check + connection/list + /// endpoints. Returns (base_url, ca_pem_path, tempdir) — the caller must + /// hold the tempdir so the certificate file stays readable for the test. + async fn spawn_self_signed_https_server() -> (String, std::path::PathBuf, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let ca_key = dir.path().join("ca.key"); + let ca_pem = dir.path().join("ca.pem"); + let server_key = dir.path().join("server.key"); + let server_csr = dir.path().join("server.csr"); + let server_pem = dir.path().join("server.pem"); + let run = |args: &[&str]| { + let status = std::process::Command::new("openssl") + .args(args) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .unwrap(); + let output = std::process::Command::new("openssl").args(args).output().unwrap(); + assert!( + output.status.success(), + "openssl failed: {args:?} stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + let _ = status; + }; + // Self-signed CA. + run(&[ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + ca_key.to_str().unwrap(), + "-out", + ca_pem.to_str().unwrap(), + "-subj", + "/CN=DBX Test CA", + "-days", + "1", + "-addext", + "basicConstraints=critical,CA:TRUE", + ]); + // Server key + CSR. + run(&[ + "req", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + server_key.to_str().unwrap(), + "-out", + server_csr.to_str().unwrap(), + "-subj", + "/CN=127.0.0.1", + ]); + // Server cert signed by the CA, with SAN for 127.0.0.1. The x509 -req + // subcommand takes extensions via -extfile, not -addext. + let ext_path = dir.path().join("server.ext"); + std::fs::write(&ext_path, "subjectAltName=IP:127.0.0.1\nbasicConstraints=critical,CA:FALSE\n").unwrap(); + run(&[ + "x509", + "-req", + "-in", + server_csr.to_str().unwrap(), + "-CA", + ca_pem.to_str().unwrap(), + "-CAkey", + ca_key.to_str().unwrap(), + "-CAcreateserial", + "-out", + server_pem.to_str().unwrap(), + "-days", + "1", + "-extfile", + ext_path.to_str().unwrap(), + ]); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + // Read certificate material before moving into the server task: the + // tempdir is dropped when this function returns, deleting the files. + let server_cert_bytes = std::fs::read(&server_pem).unwrap(); + let server_key_bytes = std::fs::read(&server_key).unwrap(); + tokio::spawn(async move { + use std::io::BufReader; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + let certs: Vec> = { + let mut reader = BufReader::new(&server_cert_bytes[..]); + rustls_pemfile::certs(&mut reader).collect::>().unwrap() + }; + let key = { + let mut reader = BufReader::new(&server_key_bytes[..]); + rustls_pemfile::private_key(&mut reader).unwrap().unwrap() + }; + // A single default provider avoids the "could not automatically + // determine the process-level CryptoProvider" panic in workspace + // builds where multiple rustls crypto features are present. + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let config = rustls::ServerConfig::builder().with_no_client_auth().with_single_cert(certs, key).unwrap(); + let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(config)); + + let (stream, _) = listener.accept().await.unwrap(); + let mut tls = acceptor.accept(stream).await.unwrap(); + let mut request = Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let count = tls.read(&mut buffer).await.unwrap(); + request.extend_from_slice(&buffer[..count]); + if count == 0 || request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + let request = String::from_utf8(request).unwrap(); + let body = if request.contains("/api/auth/check") { + r#"{"authenticated":true,"required":false,"setup_required":false}"# + } else { + "[]" + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + tls.write_all(response.as_bytes()).await.unwrap(); + }); + + (format!("https://{address}"), ca_pem, dir) + } + + #[tokio::test] + async fn web_backend_rejects_self_signed_certificate_by_default() { + let (base_url, _cert, _dir) = spawn_self_signed_https_server().await; + let backend = WebBackend::new_with_config(base_url, String::new(), None, None, None, false, None).unwrap(); + backend.auth.lock().await.checked = true; + + let error = backend.load_connections().await.unwrap_err(); + // Verification is on by default: the self-signed chain is rejected. + assert!(error.contains("API request /api/connection/list failed"), "{error}"); + } + + #[tokio::test] + async fn web_backend_skips_verification_when_configured() { + let (base_url, _cert, _dir) = spawn_self_signed_https_server().await; + let backend = WebBackend::new_with_config(base_url, String::new(), None, None, None, true, None).unwrap(); + backend.auth.lock().await.checked = true; + + let connections = backend.load_connections().await.unwrap(); + assert!(connections.is_empty()); + } + + #[tokio::test] + async fn web_backend_trusts_custom_ca_certificate() { + let (base_url, ca_pem, _dir) = spawn_self_signed_https_server().await; + let backend = WebBackend::new_with_config( + base_url, + String::new(), + None, + None, + None, + false, + Some(ca_pem.to_str().unwrap().to_string()), + ) + .unwrap(); + backend.auth.lock().await.checked = true; + + let connections = backend.load_connections().await.unwrap(); + assert!(connections.is_empty()); + } + #[tokio::test] async fn local_backend_uses_desktop_plugin_directory() { let data_dir = tempfile::tempdir().unwrap(); diff --git a/docs/content/docs/mcp.cn.mdx b/docs/content/docs/mcp.cn.mdx index e887ce0afb..f20e853f3c 100644 --- a/docs/content/docs/mcp.cn.mdx +++ b/docs/content/docs/mcp.cn.mdx @@ -128,7 +128,7 @@ Oracle、人大金仓和虚谷需要匹配的 DBX 原生 Agent,但不需要 JR 设置 `DBX_WEB_URL` 后,MCP 会使用部署的 DBX Web 后端,而不是读取本机连接。如果 Web 登录启用了密码保护,还要设置 `DBX_WEB_PASSWORD`,值为 Web 登录页使用的密码。 -DBX Web 请求遵循标准系统代理环境变量:https 地址读取 `HTTPS_PROXY`/`https_proxy`,http 地址读取 `HTTP_PROXY`/`http_proxy`,`ALL_PROXY`/`all_proxy` 作为兜底,`NO_PROXY`/`no_proxy`(逗号分隔的主机列表,例如 `localhost,127.0.0.1,.internal.example`)作为绕过列表。代理值为空或未设置时直连(不走代理)。支持 HTTP、HTTPS 与 SOCKS5 代理(`http://127.0.0.1:7890` 或 `socks5://127.0.0.1:1080`);需要认证的代理使用标准 `user:password@host:port` URL 形式,例如 `http://admin:admin123@127.0.0.1:7890`。如需为每次 DBX Web 请求附加自定义请求头(例如网关前的令牌认证),将 `DBX_WEB_HEADERS` 设置为 JSON 对象,键为请求头名称、值为字符串,例如 `{"Authorization":"Bearer "}`。同样的变量在 DBX CLI 的 Web 模式下同样生效。 +DBX Web 请求遵循标准系统代理环境变量:https 地址读取 `HTTPS_PROXY`/`https_proxy`,http 地址读取 `HTTP_PROXY`/`http_proxy`,`ALL_PROXY`/`all_proxy` 作为兜底,`NO_PROXY`/`no_proxy`(逗号分隔的主机列表,例如 `localhost,127.0.0.1,.internal.example`)作为绕过列表。代理值为空或未设置时直连(不走代理)。支持 HTTP、HTTPS 与 SOCKS5 代理(`http://127.0.0.1:7890` 或 `socks5://127.0.0.1:1080`);需要认证的代理使用标准 `user:password@host:port` URL 形式,例如 `http://admin:admin123@127.0.0.1:7890`。如需为每次 DBX Web 请求附加自定义请求头(例如网关前的令牌认证),将 `DBX_WEB_HEADERS` 设置为 JSON 对象,键为请求头名称、值为字符串,例如 `{"Authorization":"Bearer "}`。目标为自签名 HTTPS 时,证书验证默认开启:设置 `DBX_WEB_INSECURE_SKIP_VERIFY=1` 可跳过验证,或设置 `DBX_WEB_CA_CERT` 指向 PEM/DER CA 文件以信任私有 CA。同样的变量在 DBX CLI 的 Web 模式下同样生效。 DBX Web 和 Docker 同样需要独立 DuckDB 驱动。首次启动后可通过 Driver Manager 安装;Docker 会把驱动保存到 `/app/data/agents`,挂载 `/app/data` 数据卷后升级容器不会丢失。 @@ -158,6 +158,8 @@ DBX 在 **设置 → MCP** 中保存一份权威策略,并在每次请求时 | `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` | DBX Web 请求的标准系统代理变量;空值表示不走代理。认证格式 `http://user:pass@host:port` | | `NO_PROXY` | 上述代理的标准绕过列表(逗号分隔主机) | | `DBX_WEB_HEADERS` | DBX Web 请求附加的请求头 JSON 对象,例如 `{"Authorization":"Bearer token"}` | +| `DBX_WEB_INSECURE_SKIP_VERIFY` | `1`/`true` 时跳过 TLS 证书验证(用于自签名 DBX Web 后端;默认验证) | +| `DBX_WEB_CA_CERT` | DBX Web TLS 验证时信任的 PEM/DER CA 文件 | | `DBX_MCP_ALLOW_WRITES` | 仅用于升级兼容:`0`/`false` 使尚未配置的策略保持只读 | | `DBX_MCP_SCOPE_CONNECTION_ID` | 兼容旧配置:限制为一个连接 ID | | `DBX_MCP_SCOPE_CONNECTION_IDS` | 兼容旧配置:限制为多个连接 ID | diff --git a/docs/content/docs/mcp.mdx b/docs/content/docs/mcp.mdx index 2815bc73bf..ab0269f895 100644 --- a/docs/content/docs/mcp.mdx +++ b/docs/content/docs/mcp.mdx @@ -128,7 +128,7 @@ Oracle, KingBase, and XuguDB require their matching native DBX Agent but no JRE. Set `DBX_WEB_URL` to use a deployed DBX Web backend instead of local connections. If the Web login is protected, also set `DBX_WEB_PASSWORD` to the same login password. -DBX Web requests honor the standard system proxy environment variables: `HTTPS_PROXY`/`https_proxy` for https URLs, `HTTP_PROXY`/`http_proxy` for http URLs, with `ALL_PROXY`/`all_proxy` as the fallback and `NO_PROXY`/`no_proxy` (comma-separated hosts, e.g. `localhost,127.0.0.1,.internal.example`) as the bypass list. An empty or unset proxy value means direct connection (no proxy). HTTP, HTTPS, and SOCKS5 proxies are supported (`http://127.0.0.1:7890` or `socks5://127.0.0.1:1080`); proxies requiring authentication use the standard `user:password@host:port` URL form, e.g. `http://admin:admin123@127.0.0.1:7890`. To attach extra headers to every DBX Web request (for example token auth in front of a gateway), set `DBX_WEB_HEADERS` to a JSON object of header names and string values, e.g. `{"Authorization":"Bearer "}`. The same variables are honored by the DBX CLI in Web mode. +DBX Web requests honor the standard system proxy environment variables: `HTTPS_PROXY`/`https_proxy` for https URLs, `HTTP_PROXY`/`http_proxy` for http URLs, with `ALL_PROXY`/`all_proxy` as the fallback and `NO_PROXY`/`no_proxy` (comma-separated hosts, e.g. `localhost,127.0.0.1,.internal.example`) as the bypass list. An empty or unset proxy value means direct connection (no proxy). HTTP, HTTPS, and SOCKS5 proxies are supported (`http://127.0.0.1:7890` or `socks5://127.0.0.1:1080`); proxies requiring authentication use the standard `user:password@host:port` URL form, e.g. `http://admin:admin123@127.0.0.1:7890`. To attach extra headers to every DBX Web request (for example token auth in front of a gateway), set `DBX_WEB_HEADERS` to a JSON object of header names and string values, e.g. `{"Authorization":"Bearer "}`. For self-signed HTTPS backends, certificate verification is on by default; set `DBX_WEB_INSECURE_SKIP_VERIFY=1` to disable verification, or set `DBX_WEB_CA_CERT` to a PEM/DER CA file to trust a private CA. The same variables are honored by the DBX CLI in Web mode. DBX Web and Docker require the same standalone DuckDB driver. Install it from Driver Manager after the first launch. Docker stores installed drivers under `/app/data/agents`, so they persist when `/app/data` is mounted as a volume. @@ -158,6 +158,8 @@ Updated servers do not let `DBX_MCP_ALLOW_WRITES` or `DBX_MCP_ALLOW_DANGEROUS_SQ | `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` | Standard system proxy variables for DBX Web requests; empty value means no proxy. Auth via `http://user:pass@host:port` | | `NO_PROXY` | Standard bypass list for the proxy above (comma-separated hosts) | | `DBX_WEB_HEADERS` | JSON object of extra HTTP headers for DBX Web requests, e.g. `{"Authorization":"Bearer token"}` | +| `DBX_WEB_INSECURE_SKIP_VERIFY` | `1`/`true` disables TLS certificate verification for self-signed DBX Web backends (default: verify) | +| `DBX_WEB_CA_CERT` | PEM/DER CA file to trust for DBX Web TLS verification | | `DBX_MCP_ALLOW_WRITES` | Upgrade compatibility only: `0`/`false` keeps an unconfigured policy read-only | | `DBX_MCP_SCOPE_CONNECTION_ID` | Compatibility scope for one connection ID | | `DBX_MCP_SCOPE_CONNECTION_IDS` | Compatibility scope for multiple connection IDs | diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 831afbc075..96f036cbbb 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -198,7 +198,7 @@ Set `DBX_WEB_URL` to use a deployed DBX Web backend instead of local desktop sto `DBX_WEB_PASSWORD` is the password used on the DBX Web login page. Desktop-local mode does not use it. Desktop UI tools are hidden in Web mode. -DBX Web requests honor the standard system proxy environment variables (`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`, bypass via `NO_PROXY`); an empty value means no proxy. Proxies requiring authentication use the `http://user:pass@host:port` URL form. Extra headers can be attached via `DBX_WEB_HEADERS` (a JSON object, e.g. `{"Authorization":"Bearer "}`) — applied to every request, including authentication. +DBX Web requests honor the standard system proxy environment variables (`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`, bypass via `NO_PROXY`); an empty value means no proxy. Proxies requiring authentication use the `http://user:pass@host:port` URL form. Extra headers can be attached via `DBX_WEB_HEADERS` (a JSON object, e.g. `{"Authorization":"Bearer "}`) — applied to every request, including authentication. For self-signed HTTPS backends, set `DBX_WEB_INSECURE_SKIP_VERIFY=1` to skip certificate verification, or `DBX_WEB_CA_CERT` to trust a private CA (verification is on by default). Docker and DBX Web also require the standalone DuckDB driver. Install it from Driver Manager after the first launch; when `/app/data` is persisted, the driver is stored under `/app/data/agents` and survives container upgrades. @@ -269,6 +269,8 @@ SQL text is not included in normal MCP errors or logged by default. Enable tempo | `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` | Standard system proxy variables for DBX Web requests; empty value means no proxy. Auth via `http://user:pass@host:port` | | `NO_PROXY` | Standard comma-separated bypass list for the proxy above | | `DBX_WEB_HEADERS` | JSON object of extra HTTP headers for DBX Web requests, e.g. `{"Authorization":"Bearer token"}` | +| `DBX_WEB_INSECURE_SKIP_VERIFY` | `1`/`true` disables TLS certificate verification for self-signed backends | +| `DBX_WEB_CA_CERT` | PEM/DER CA file to trust for DBX Web TLS verification | | `DBX_MCP_ALLOW_WRITES` | Upgrade compatibility only: `0`/`false` keeps an unconfigured policy read-only | | `DBX_MCP_SCOPE_CONNECTION_ID` | Compatibility scope for one connection ID | | `DBX_MCP_SCOPE_CONNECTION_IDS` | Compatibility scope for multiple connection IDs | @@ -550,6 +552,8 @@ MongoDB 更新和删除在未启用完全访问时必须提供可验证有效的 | `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` | DBX Web 请求的标准系统代理变量;空值表示不走代理。认证格式 `http://user:pass@host:port` | | `NO_PROXY` | 上述代理的标准逗号分隔绕过列表 | | `DBX_WEB_HEADERS` | DBX Web 请求附加的请求头 JSON 对象,例如 `{"Authorization":"Bearer token"}` | +| `DBX_WEB_INSECURE_SKIP_VERIFY` | `1`/`true` 时跳过 TLS 证书验证(用于自签名后端) | +| `DBX_WEB_CA_CERT` | DBX Web TLS 验证时信任的 PEM/DER CA 文件 | | `DBX_MCP_ALLOW_WRITES` | 仅用于升级兼容:`0`/`false` 使尚未配置的策略保持只读 | | `DBX_MCP_SCOPE_CONNECTION_ID` | 兼容旧配置:限制到指定连接 ID | | `DBX_MCP_SCOPE_CONNECTION_IDS` | 兼容旧配置:限制到多个连接 ID |