Skip to content

feat(sdks): honor custom TLS trust with a ca_bundle / caBundle option - #1657

Draft
mishushakov wants to merge 6 commits into
mainfrom
python-sdk-honor-custom-tls-trust-verify_ssl-ca-bundle-on-sdk-320
Draft

feat(sdks): honor custom TLS trust with a ca_bundle / caBundle option#1657
mishushakov wants to merge 6 commits into
mainfrom
python-sdk-honor-custom-tls-trust-verify_ssl-ca-bundle-on-sdk-320

Conversation

@mishushakov

@mishushakov mishushakov commented Aug 10, 2026

Copy link
Copy Markdown
Member

Closes SDK-320.

Every transport the SDKs build trusted only the default CA store, so a self-hosted deployment behind a private CA had no way to make the SDK validate against it. In Python the gap was silent: AuthenticatedClient._verify_ssl only ever configures httpx's default transport, and since the pyqwest migration every client is handed an explicit transport — so verify=self._verify_ssl has been dead for every REST call. JS never exposed a knob at all.

What this adds

A ca_bundle / caBundle connection option — the path of a PEM file — resolving through the standard precedence (explicit option → E2B_CA_BUNDLE → unset), wired into every transport in both SDKs:

  • REST control-plane API (sync + async)
  • envd RPC and envd HTTP API, including the streaming transport
  • volume content, including the streaming transports
  • the template build-context upload, which builds its own client

The certificates are trusted in addition to the default store (tls_include_system_certs stays on in Python, the bundle is appended to tls.rootCertificates in JS), so a deployment that mixes a private CA with public ones keeps working.

Usage

from e2b import Sandbox

sandbox = Sandbox(ca_bundle="/etc/ssl/certs/internal-ca.pem")
sandbox.files.write("/tmp/hello.txt", "world")
import os
os.environ["E2B_CA_BUNDLE"] = "/etc/ssl/certs/internal-ca.pem"
import { Sandbox } from 'e2b'

const sandbox = await Sandbox.create({
  caBundle: '/etc/ssl/certs/internal-ca.pem',
})
E2B_CA_BUNDLE=/etc/ssl/certs/internal-ca.pem node app.mjs

Volumes and template builds take the same option:

volume = Volume.connect(volume_id, ca_bundle="/etc/ssl/certs/internal-ca.pem")
Template.build(template, "my-template", ca_bundle="/etc/ssl/certs/internal-ca.pem")

Nothing fails silently

  • Python: verify_ssl on the SDK's API/volume client factories now raises InvalidArgumentException pointing at ca_bundle instead of being ignored. ssl.SSLContext and disabling verification have no pyqwest equivalent and are named as unsupported in the message.
  • An unreadable file, or one holding no PEM certificate, raises where the connection is configured rather than as an unspecific certificate error per connection attempt (reqwest accepts an unparsable bundle at construction time and only fails later).
  • JS: the option is Node-only — no other runtime lets the SDK configure TLS trust per connection — so setting it under workerd/Deno/Bun, or when undici can't be loaded, raises instead of quietly connecting with the trust it was meant to extend.

Refactors that came with it

  • Python: a TransportConfig NamedTuple now carries the proxy and the TLS trust, keys every transport cache, and produces the pyqwest keyword arguments in one place — the six transport construction sites no longer each spell out tls_include_system_certs=True. The volume clients reuse the shared retrying_http_transport instead of repeating its body.
  • JS: fetcher factories take a FetchTransportOpts object ({ proxy, caBundle }) instead of a bare proxy string, and cache per combination.
  • JS: the build-context upload builds a dispatcher of its own when — and only when — a CA bundle needs one, and refuses one outside Node. Every other build stays on undici's default agent, the path that carries the Content-Length framing presigned uploads require ([JS SDK] uploadFile uses chunked transfer encoding, causing 501 NotImplemented on S3 presigned PUT URLs #1243).

Tests

  • Python (tests/test_ca_bundle.py, new trustme dev dependency): an HTTPS server whose certificate only a test CA vouches for, so the assertions are real handshakes — the API, envd, volume, and template-upload paths (sync and async) connect with ca_bundle and fail without it. Plus precedence, propagation through get_api_params, cache keying, verify_ssl rejection, and the bundle-loading errors.
  • JS (tests/caBundle.test.ts): the certificates reach the Agent's connect and the ProxyAgent's requestTls/proxyTls on top of the default roots, the error paths, and the non-Node rejection; plus E2B_CA_BUNDLE precedence and per-bundle fetcher caching.

🤖 Generated with Claude Code

…tion

Every transport the SDKs build trusted only the default CA store, so a
self-hosted deployment behind a private CA had no way to make the SDK
validate against it: in Python `verify_ssl` on the generated clients was
silently dropped once a pyqwest transport was installed, and JS never
exposed a knob at all.

Add a `ca_bundle` / `caBundle` connection option (explicit option →
`E2B_CA_BUNDLE` → unset) and thread it through every transport in both
SDKs — REST API, envd RPC and HTTP, volume content (including the
streaming transports), and the template build-context upload — so a
build can't validate against a CA the API calls around it ignore. The
certificates are added to the default trust rather than replacing it.

Python gains a `TransportConfig` that carries the proxy and the TLS
trust, keys the transport caches, and builds the pyqwest keyword
arguments in one place; the volume clients now reuse the shared
`retrying_http_transport` instead of repeating it. `verify_ssl` raises
and points at `ca_bundle` instead of being ignored.

In JS the option is Node-only, since no other runtime lets the SDK
configure TLS trust per connection, and setting it elsewhere raises. The
build-context upload now goes through the SDK's dispatcher, so it honors
`proxy` as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

SDK-320

@cla-bot cla-bot Bot added the cla-signed label Aug 10, 2026
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes TLS verification across every SDK HTTP path; mistakes could break or weaken trust for self-hosted customers, but behavior is additive with explicit errors and broad tests.

Overview
Both SDKs gain caBundle / ca_bundle (and E2B_CA_BUNDLE) so a PEM file of extra CAs is trusted on top of the default store for control-plane API, envd, volume traffic, and template build-context uploads. JS applies it through undici dispatchers (Node only; non-Node or missing undici throws). Python routes it via TransportConfig into pyqwest transports and load_ca_bundle; non-default verify_ssl on generated clients now raises with a pointer to ca_bundle. Fetcher/transport caches key on proxy plus CA bundle path.

Reviewed by Cursor Bugbot for commit 9662374. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9662374

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Minor
@e2b/python-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/python-sdk/tests/test_ca_bundle.py Fixed
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 47a65a5. Download artifacts from this workflow run.

JS SDK (e2b@2.38.4-python-sdk-honor-custom-tls-trust-verify-ssl-ca-bundle-on-sdk-320.0):

npm install ./e2b-2.38.4-python-sdk-honor-custom-tls-trust-verify-ssl-ca-bundle-on-sdk-320.0.tgz

CLI (@e2b/cli@2.16.2-python-sdk-honor-custom-tls-trust-verify-ssl-ca-bundle-on-sdk-320.0):

npm install ./e2b-cli-2.16.2-python-sdk-honor-custom-tls-trust-verify-ssl-ca-bundle-on-sdk-320.0.tgz

Python SDK (e2b==2.38.0+python.sdk.honor.custom.tls.trust.verify.ssl.ca.bundle.on.sdk.320):

pip install ./e2b-2.38.0+python.sdk.honor.custom.tls.trust.verify.ssl.ca.bundle.on.sdk.320-py3-none-any.whl

mishushakov and others added 3 commits August 10, 2026 21:10
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The upload had no dispatcher before, so it used undici's default agent
(HTTP/1.1). Now that it goes through one for the proxy and CA bundle,
opt out of h2 so the framing this path is picky about is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcafd1fdf0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/js-sdk/src/undici.ts Outdated
)
}

return [...tls.rootCertificates, pem]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Node's full default CA set

When caBundle is set in a Node process that already extends TLS trust via NODE_EXTRA_CA_CERTS, --use-system-ca, or tls.setDefaultCACertificates, this replaces the effective default trust with only tls.rootCertificates plus the new bundle. Because Node treats the ca option as a replacement and tls.rootCertificates is only the bundled CA subset, SDK requests through this dispatcher can start failing certificate verification for hosts that previously relied on the configured default CA store. Please build from Node's effective defaults (for example tls.getCACertificates?.('default') with an older-Node fallback) before appending the bundle.

Useful? React with 👍 / 👎.

Comment thread packages/js-sdk/src/template/buildApi.ts Outdated
…ndle asks otherwise

The Bun/Deno/workerd legs run the same suites as Node, where two things
broke: the fetcher-cache tests configured a CA bundle in runtimes that
reject it, and routing the build-context upload through
`buildDispatchedFetch` hit the shared setup's undici-unavailable mock,
which dropped it onto a global fetch that chunks the body on Deno — the
framing #1243 guards against.

The upload now builds a dispatcher only when a CA bundle needs one (and
refuses one outside Node), keeping every other build on the path it has
always used, and its suite opts out of the mock so it exercises the real
undici either way. The CA-bundle cache assertions moved to Node-only
tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread packages/js-sdk/src/template/buildApi.ts Outdated
…d dispatcher

`tls.rootCertificates` is only Node's bundled set, so a process that had
already extended trust through NODE_EXTRA_CA_CERTS, --use-system-ca or
setDefaultCACertificates lost those additions the moment a `caBundle`
replaced `ca`. Build on `tls.getCACertificates('default')` where it
exists (Node >= 22.15), which reports what Node would actually trust.

The upload dispatcher is also cached per CA bundle now: a build
uploading several files was re-reading the bundle and leaving an Agent
behind per upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishushakov

Copy link
Copy Markdown
Member Author

Addressed the automated review:

  • Codex, tls.rootCertificates is not the effective default — right, and it would have silently narrowed trust for anyone already using NODE_EXTRA_CA_CERTS, --use-system-ca or tls.setDefaultCACertificates. The bundle is now appended to tls.getCACertificates('default') where it exists (Node >= 22.15), falling back to rootCertificates below that.
  • Bugbot, uncached upload dispatcher — the upload fetcher is cached per CA bundle now, so a multi-file build reads the bundle once and keeps one Agent instead of one per file. A failed build is evicted so an unreadable bundle isn't replayed forever.
  • CodeQL, insecure TLS versions in the test server — the test HTTPS server is pinned to TLS 1.2+.

Also narrowed the upload change: it only builds its own dispatcher when a CA bundle needs one, so every other build stays on undici's default agent — the path that carries the Content-Length framing presigned uploads require (#1243). Dropping it onto a dispatcher is what broke the Deno leg.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9662374. Configure here.

inflightLimit: 0,
allowH2: false,
...transport,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached upload agent serializes parallels

Medium Severity

uploadFetch now reuses one dispatcher with connections: 1 for every build-context upload that shares a CA bundle. Template builds kick off those uploads in parallel, so with caBundle set they queue on a single HTTP/1.1 connection instead of running concurrently as they did when each upload built its own agent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9662374. Configure here.

@mishushakov
mishushakov marked this pull request as draft August 10, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants