Skip to content

feature: every client that cannot trust the Cortex CA keeps working — one supported result per client #947

Open

Description

@huang195

Problem

The TLS bridge shows each client a leaf certificate that the Cortex CA signs. A client that does not trust that CA refuses the leaf, and its call fails. That makes Cortex visible to programs that have nothing to do with an agent.

This issue is the home for every client of that kind. Each client gets one supported result, and the result must be the same on every call:

  • Bridged: the client trusts the CA, and plugins read its traffic.
  • Passed through: Cortex tunnels the connection and does not forge a leaf.
  • Unsupported: the call fails with a message that says what to do.

A client must never fail some of the time and work at other times.

Why a client does not trust the CA

A client trusts the CA only if its trust store contains it. Clients get their trusted CAs in one of three ways:

Trust store Examples How Cortex can reach it
A CA file named by an environment variable Node (NODE_EXTRA_CA_CERTS), Python requests (REQUESTS_CA_BUNDLE), curl (CURL_CA_BUNDLE), git (GIT_SSL_CAINFO), Go on Linux (SSL_CERT_FILE) Set the variable. It reaches only the processes that get the environment.
The OS store only Go on macOS, Chromium's network service (every Electron app: IBM Bob, VS Code, Cursor, Slack) Only the keychain, NSS or the Windows store. We do not want Cortex to depend on that.
A pinned or built-in store Clients that pin a certificate, or ship their own roots None.

The variable names do not mean the same thing. NODE_EXTRA_CA_CERTS adds to Node's store, so it gets ca.crt. SSL_CERT_FILE, GIT_SSL_CAINFO, REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE replace the store, so they get bundle.crt. If a client gets the wrong file, every TLS call that does not go through Cortex fails.

What Cortex does today

Mechanism What it covers Issue
tlsbridge.DefaultPassthroughHosts Tunnels GitHub, the Go module proxy, package registries and Docker Hub for all clients. No plugin reads that traffic. #946
os-trust-only prediction (tlsbridge.ClientTrust) Does not forge for a Go executable on macOS when the OS does not trust the CA. #1348
Program memory (program-refused) After a client refuses a leaf, tunnels that program, with a backoff. After 3 rejections, stops forging for it until Cortex restarts. #912
agentop configure <agent> Writes the CA variables into the agent's settings. They reach only the processes that the agent starts.
agentop exec Runs one command with the proxy and CA variables set.

The gaps

  1. A tool that the user runs in their own shell gets no CA variable. The variables reach only the processes that an agent starts. A user's go get, npm, pip, curl or Go CLI has HTTPS_PROXY only if the user exported it, and then it has no CA.

  2. Cortex has no supported answer for Go on macOS. SSL_CERT_FILE has no effect on darwin. The os-trust-only prediction covers Go executables, but only darwinGoNote tells the user what to do. Decide the supported answer: passthrough, or a documented limit.

  3. Chromium's network service trusts only the OS store, and Cortex does not predict this. Seen with IBM Bob 2.2.3:

    • Bob's network helper (IBM Bob Helper.app, --utility-sub-type=network.mojom.NetworkService) sends an hourly CONNECT to api.us-east.bob.ibm.com:443. Cortex forges a leaf and the helper refuses it. 45 failures from 30 Oct to 2 Nov.
    • A host passthrough is not possible. Bob's model calls go to the same host from the Plugin helper (IBM Bob Helper (Plugin).app), which trusts the CA through NODE_EXTRA_CA_CERTS.
    • The two helpers are in one app bundle. Only their --utility-sub-type argument tells them apart.
  4. Cortex counts only 2 of the 6 alerts that mean "I refuse your certificate". handshakeFailureReason (core/listener/forwardproxy/server.go) matches bad certificate (42) and unknown certificate authority (48) as client-rejected-ca. BoringSSL sends certificate_unknown (46) by default, so Chromium's refusals become handshake-failed:

    • handshake-failed is transient. It gives a 30-second skip, so Cortex forges a leaf again for a client that calls once an hour, and each call fails.
    • The program never reaches the stop after 3 rejections.
    • The log line has no CA advice.
    • The other certificate alerts, 43 to 45, have the same problem. No test covers this function.
  5. OpenSSL 3 sends unknown_ca as a plaintext record during the TLS 1.3 handshake. Go cannot decrypt it and reports local error: tls: bad record MAC. We saw this from the podman VM (fwupdmgr). Cortex reports it as handshake-failed. bad record MAC has other real causes, so it is not safe to treat it as a CA rejection without more evidence.

  6. The program memory is lost on each restart. After each Cortex restart, each program that refuses the CA fails up to 3 more calls before Cortex learns again.

Proposed solution

  • Classify a CA rejection by the alert type, not by the error text: peer alerts 42 to 46 and 48, after a forged leaf, become client-rejected-ca. Add a table test, with negative cases such as handshake failure (gap 4).
  • Extend the os-trust-only prediction to Chromium's network service. Detect it from the process arguments (kern.procargs2 on macOS, /proc/<pid>/cmdline on Linux), and use the existing check of OS trust. Leave Electron's Node processes bridged (gap 3).
  • Decide the supported answer for Go on macOS: passthrough by the prediction, or a documented limit (gap 2).
  • Decide how a tool that the user runs in their own shell gets the CA, or document that agentop exec is the supported way (gap 1). Cover:
    • Go module fetch / go get, gh, and other Go CLIs
    • npm / pnpm (Node), pip / uv (Python requests)
    • curl and wget, with and without proxy variables set
    • Shell scripts that inherit HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY
  • Find a safe way to recognise OpenSSL's plaintext unknown_ca, or document it (gap 5).
  • Decide if the program memory must survive a restart, keyed by the executable and the CA fingerprint (gap 6).
  • Document the result for each client: bridged, passed through, or unsupported.
  • Each unsupported combination fails with a message that says what to do, and never fails only some of the time.

Constraints

  • No dependency on the macOS keychain or another OS store. A keychain step can stay as an option that the user selects.
  • Never pass through a host that carries inference. That removes parsing, cost and token savings for every client of that host.

Related

Part of the Cortex v0.9.0 release (#962). Gates exit criterion 2: Cortex is invisible to everything else.

Activity

  1. added this to the Release V0.9.0 milestone on Sep 10, 2026
  2. self-assigned this
    on Sep 10, 2026
  3. changed the title [-]feature: language toolchains and scripted HTTPS clients keep working[/-] [+]feature: every client that cannot trust the Cortex CA keeps working — one supported result per client[/+] on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions