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:
Hunter Bown
2026-05-03 07:35:23 -05:00
parent 6566a59097
commit 604edc9f83
3 changed files with 73 additions and 0 deletions
+8
View File
@@ -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")
+60
View File
@@ -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)
}
+5
View File
@@ -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)