feat(tls): honor SSL_CERT_FILE for corporate-CA / MITM proxies (#418)
Corporate users behind TLS-inspecting proxies (Zscaler, Netskope, Palo Alto, in-house mitmproxy fleets) need to add the proxy's intermediate CA to the trusted-roots set so the deepseek client doesn't fail with `unable to get local issuer certificate`. The reqwest builder already trusts the platform's system store via native-tls. This adds opt-in support for the conventional `SSL_CERT_FILE` env var so users can point at their own bundle: * New `add_extra_root_certs(builder, path)` helper reads the file, tries `Certificate::from_pem_bundle` (covers single-cert files too), falls back to `from_der` for binary cert files. * Wired into `build_http_client` when `SSL_CERT_FILE` is set and non-empty. Failures log a warning via the existing `logging::warn` channel and return the builder unchanged — the existing system trust still applies, so a malformed env var degrades gracefully instead of bricking the launch. * Each successful load logs `info` with the cert count so operators can confirm their bundle was picked up. Documented in `docs/CONFIGURATION.md`'s environment-variables list alongside the existing TLS-related notes. No new dependency — reqwest's `native-tls` feature already exposes `Certificate::from_pem_bundle` / `from_der`.
This commit is contained in:
@@ -63,6 +63,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
(e.g. user `"never"` → project `"on-request"` is allowed
|
||||
even though it loosens) stay v0.8.9 follow-up because they
|
||||
need a richer ordering check.
|
||||
- **`SSL_CERT_FILE` honored in the HTTPS client** (#418) — corporate
|
||||
proxy / TLS-inspecting MITM users can now point at their custom
|
||||
CA bundle and have it added alongside the platform's system
|
||||
trust store. Tries PEM-bundle parsing first (covers single-cert
|
||||
files too), falls back to DER. Failures log a warning and
|
||||
continue — the existing system roots still apply, so a
|
||||
malformed env var won't bring down the launch. Documented in
|
||||
`docs/CONFIGURATION.md`.
|
||||
- **Sub-agent role taxonomy expansion** (#404) — adds `Implementer`
|
||||
("land this change with the minimum surrounding edit") and
|
||||
`Verifier` ("run the test suite, report pass/fail with evidence")
|
||||
|
||||
@@ -412,6 +412,53 @@ fn force_http1_from_env() -> bool {
|
||||
.is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
|
||||
}
|
||||
|
||||
/// Read `SSL_CERT_FILE` and add its contents as extra root
|
||||
/// certificates on the reqwest builder (#418). Tries the PEM-bundle
|
||||
/// parser first (covers single-cert files too), then falls back to
|
||||
/// DER. All failures log a warning and return the builder unchanged
|
||||
/// so a malformed env var degrades gracefully.
|
||||
fn add_extra_root_certs(
|
||||
mut builder: reqwest::ClientBuilder,
|
||||
cert_path: &str,
|
||||
) -> reqwest::ClientBuilder {
|
||||
let bytes = match std::fs::read(cert_path) {
|
||||
Ok(b) => b,
|
||||
Err(err) => {
|
||||
logging::warn(format!(
|
||||
"SSL_CERT_FILE={cert_path} could not be read: {err}"
|
||||
));
|
||||
return builder;
|
||||
}
|
||||
};
|
||||
|
||||
// PEM bundle handles both single-cert and multi-cert files; try
|
||||
// it first since `BEGIN CERTIFICATE` framing is the common case.
|
||||
if let Ok(certs) = reqwest::Certificate::from_pem_bundle(&bytes) {
|
||||
let added = certs.len();
|
||||
for cert in certs {
|
||||
builder = builder.add_root_certificate(cert);
|
||||
}
|
||||
logging::info(format!(
|
||||
"SSL_CERT_FILE={cert_path} loaded ({added} cert(s))"
|
||||
));
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Single-cert DER fallback.
|
||||
match reqwest::Certificate::from_der(&bytes) {
|
||||
Ok(cert) => {
|
||||
builder = builder.add_root_certificate(cert);
|
||||
logging::info(format!("SSL_CERT_FILE={cert_path} loaded (1 DER cert)"));
|
||||
}
|
||||
Err(err) => {
|
||||
logging::warn(format!(
|
||||
"SSL_CERT_FILE={cert_path} could not be parsed as PEM bundle or DER: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
impl DeepSeekClient {
|
||||
/// Create a DeepSeek client from CLI configuration.
|
||||
pub fn new(config: &Config) -> Result<Self> {
|
||||
@@ -473,6 +520,19 @@ impl DeepSeekClient {
|
||||
logging::info("DEEPSEEK_FORCE_HTTP1=1 — pinning HTTP client to HTTP/1.1");
|
||||
builder = builder.http1_only();
|
||||
}
|
||||
// #418: corporate-proxy / MITM-inspector CA support. When
|
||||
// `SSL_CERT_FILE` is set, load the cert(s) it points at and
|
||||
// add them as trusted roots alongside the platform's system
|
||||
// store. We try PEM bundle first (the common case for
|
||||
// multi-cert files), then fall back to single-cert PEM, then
|
||||
// DER. Failures log a warning and continue — the existing
|
||||
// system roots still apply, so a malformed env var won't
|
||||
// bring down the launch.
|
||||
if let Ok(cert_path) = std::env::var("SSL_CERT_FILE")
|
||||
&& !cert_path.is_empty()
|
||||
{
|
||||
builder = add_extra_root_certs(builder, &cert_path);
|
||||
}
|
||||
builder.build().map_err(Into::into)
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,11 @@ These override config values:
|
||||
- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and
|
||||
`fancy_animations = false` at startup, regardless of the saved
|
||||
settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).
|
||||
- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users
|
||||
point this at a PEM bundle (or single DER cert) and the cert(s)
|
||||
get added alongside the platform's system trust store. Failures
|
||||
log a warning and continue — the existing system roots still
|
||||
apply.
|
||||
|
||||
### Instruction sources (`instructions = [...]`, #454)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user