Skip to main content
Enable HTTPS traffic inspection so policy rules, logging, and other controls can see plaintext request data. microsandbox terminates TLS on the host side and re-encrypts to the upstream server. That’s how features like per-host secret injection and URL-level policy checks can see inside what would otherwise be an opaque stream.

How it works

When you enable TLS interception:
  1. microsandbox loads the interception CA. By default this is a host-global development CA persisted under ~/.microsandbox/tls/ca.{crt,key} so repeated sandboxes can reuse the same trusted root. You can provide custom CA paths with intercept_ca_cert() and intercept_ca_key() when a sandbox or environment needs its own CA.
  2. microsandbox updates the guest’s trust store to trust this CA.
  3. When the guest opens an HTTPS connection, the host-side proxy intercepts the handshake, generates a leaf certificate for the target domain on the fly, and signs it with the configured interception CA.
  4. The proxy opens its own TLS connection to the upstream server and bridges the two.
The default CA is intentionally local to the host, not to one sandbox lifetime. Remove ~/.microsandbox/tls/ca.crt and ~/.microsandbox/tls/ca.key if you want microsandbox to generate a fresh default CA. Use explicit CA paths when you do not want to share the default across sandboxes on the same machine.

Enabling interception

use microsandbox::Sandbox;

let sb = Sandbox::builder("worker")
    .image("python")
    .network(|n| n
        .tls(|t| t
            .bypass("pinned-api.example.com")
            .bypass("*.gov")
        )
    )
    .create()
    .await?;
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .network((n) => n.tls((t) =>
        t.bypass("pinned-api.example.com").bypass("*.gov"),
    ))
    .create();
from microsandbox import Network, Sandbox, TlsConfig

sb = await Sandbox.create(
    "worker",
    image="python",
    network=Network(
        tls=TlsConfig(bypass=("pinned-api.example.com", "*.gov")),
    ),
)
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithNetwork(&m.NetworkConfig{
        TLS: &m.TLSConfig{
            Bypass: []string{"pinned-api.example.com", "*.gov"},
        },
    }),
)
msb create python --name worker \
  --tls-intercept \
  --tls-bypass "pinned-api.example.com" \
  --tls-bypass "*.gov"

Bypass patterns

Some domains don’t play well with interception, typically ones whose clients pin a specific certificate or public key instead of trusting the system CA store. Software update channels (browser autoupdate, macOS softwareupdate, package-manager mirrors that ship a pinned signing CA) and vendor SDKs that bundle their own pinned CA are the usual suspects. Add them to the bypass list and their traffic flows through as-is, encrypted end-to-end between the guest and the upstream server. Bypass patterns accept exact matches (api.example.com) and wildcards (*.gov, *.apple.com).

Upstream CA trust

By default, the TLS proxy verifies upstream server certificates using the host’s native root store. Add global upstream CAs when every intercepted host should trust the same private root. Add scoped upstream CAs when only specific host patterns should trust a private root. Disable upstream verification for ephemeral hosts where you cannot get a PEM. Disabling upstream verification is the proxy-side equivalent of curl -k: interception still happens, but microsandbox does not authenticate the proxy-to-upstream TLS certificate for matching hosts.
let sb = Sandbox::builder("agent")
    .image("python")
    .network(|n| n.tls(|t| {
        t.upstream_ca_cert("/etc/ssl/corp-root.pem")
         .upstream_ca_cert_for("api.internal.example.com", "./certs/api-ca.pem")
         .upstream_ca_cert_for("*.svc.cluster.local", "./certs/k8s-ca.pem")
         .verify_upstream_for("*.preview.internal", false)
    }))
    .create()
    .await?;
msb create python --name agent \
  --tls-intercept \
  --tls-upstream-ca-cert /etc/ssl/corp-root.pem \
  --tls-upstream-ca-cert-for 'api.internal.example.com=./certs/api-ca.pem' \
  --tls-upstream-ca-cert-for '*.svc.cluster.local=./certs/k8s-ca.pem' \
  --tls-no-verify-upstream-for '*.preview.internal'

Limits

  • Pinned clients bypass the trust store. Any client that pins the server’s certificate or public key (rather than trusting the system CA) will fail TLS interception and needs to be bypassed explicitly.
  • Bypassed traffic is opaque. Policy checks that depend on inspecting request content (URL-based rules, secret injection with require_tls_identity, and HTTP Host / :authority / SNI alignment checks) don’t apply on bypassed domains.
  • Client certificates are not proxied. mTLS flows where the guest is the one presenting a client certificate aren’t supported through interception; add those hosts to the bypass list.

Trusting host CAs

Corporate TLS-inspecting proxies (Cloudflare Warp Zero Trust, Zscaler, Netskope, Palo Alto, …) terminate outbound HTTPS at the host and re-sign it with a gateway CA. The corporate rollout installs that CA in your macOS Keychain or Linux trust store, so HTTPS works silently on the host. Inside a sandbox it doesn’t: the guest’s stock Mozilla bundle has never seen the gateway CA, so apk update, pip install, curl, and any SDK fetch fail with server certificate not trusted. By default the sandbox does not extend host trust into the guest, so the guest validates TLS strictly against its stock Mozilla bundle. Opt in when you need the guest to trust whatever the host trusts. At sandbox boot, microsandbox copies the host’s trusted root CAs into the guest and appends them to the system CA bundle.
Security: this extends the guest’s trust to whatever the host trusts. That’s the point for MITM-proxy environments, but it also means an attacker who can plant a CA on the host now has that trust inside every sandbox.It matches the existing threat model (a host-access attacker already owns the sandbox), but worth knowing before enabling on a host with an unfamiliar trust store.
let sb = Sandbox::builder("devbox")
    .image("alpine")
    .network(|n| n.trust_host_cas(true))
    .create()
    .await?;
await using sb = await Sandbox.builder("devbox")
    .image("alpine")
    .network((n) => n.trustHostCAs(true))
    .create();
sb = await Sandbox.create(
    "devbox",
    image="alpine",
    network=Network(trust_host_cas=True),
)
trustHostCAs := true
sb, err := m.CreateSandbox(ctx, "devbox",
    m.WithImage("alpine"),
    m.WithNetwork(&m.NetworkConfig{
        TrustHostCAs: &trustHostCAs,
    }),
)
msb run alpine --trust-host-cas -- apk update

Interaction with TLS interception

TLS interception also covers the corporate-proxy case, but only on the ports it’s configured for (default [443]), and only for non-bypassed domains. It doesn’t touch QUIC / HTTP/3 flows. Everything outside that scope (gRPC, Postgres / Redis / Mongo TLS, custom 8443, non-intercepted ports, bypassed domains) goes over raw TLS from the guest, where host-CA trust is what makes certificate validation succeed. The two features compose. Turn on host-CA trust alongside TLS interception when you need both: interception provides richer inspection on the ports it covers, host-CA trust covers everything else. Leave it off to keep the guest’s TLS stack validating strictly against its stock Mozilla bundle.

See also

  • DNS: the DNS interceptor is what makes per-host rules possible, and DoT interception reuses this TLS path
  • Network defenses: how TLS interception interacts with SSRF defenses, and secret handling for credential injection