---
title: "Credential proxies fail open in the details"
description: "Where credential proxies for AI coding agents fail open: Docker's and microsandbox's advisories of autumn 2026, three decisions a proxy makes, and what d0 does at each."
url: "https://mko.run/default-deny/"
language: "en"
published: 2026-11-03
---
# Credential proxies fail open in the details

A credential proxy keeps an agent's real token on the host: it puts the token into each request as it leaves the
sandbox, and masks any real token in what comes back. Many of them can be set to allow only a fixed list of
destinations. Their bugs this autumn were elsewhere:

- Docker Sandboxes, [CVE-2026-105570](https://nvd.nist.gov/vuln/detail/CVE-2026-105570): the check that decided whether to mask a token response compared hostnames
  case-sensitively, routing did not. A case variant of the endpoint returned real access and refresh tokens to the
  sandbox.
- Docker Sandboxes, [CVE-2026-101998](https://nvd.nist.gov/vuln/detail/CVE-2026-101998), in its own words: "could fail open while masking credentials in protected proxy
  responses". A body read returned data and an error together, and the unmasked bytes went through.
- microsandbox, 28 September: a hostname allowlist bypass "via mismatched HTTP Host/:authority", affecting "especially
  deny-by-default configurations"
  ([GHSA-3mpp-52gc-935r](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-3mpp-52gc-935r)).
  And on 9 October: "Host/SNI check fails open on malformed HTTP/1 request lines"
  ([GHSA-gj24-29j2-3x78](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-gj24-29j2-3x78)).

None of these came from the allowlist: no entry was missing, and none was wrong. Each is a place where the proxy met something it did not expect and let it pass.

## Three decisions, one default

A proxy decides three things, and each needs the same answer for the unknown: refuse.

**What may go out.** Listed hosts, methods and exact paths; everything else is a named refusal in the log. This is the
part everyone has.

**What a request is.** The CONNECT target, the TLS server name, the `Host` header and whatever the proxy routes on must
all be one name, read one way. Every normaliser is a second parser, and three of the four bugs above are one name read
two ways. Either canonicalise once and use only the result, or refuse anything not already canonical. Anthropic's
new [`srt-proxy`](https://github.com/anthropics/sandbox-runtime/blob/v0.0.80/docs/srt-proxy.md) pins it: "the Host header and the TLS server name must name the CONNECT target".

**What comes back.** Masking is code, and code has error paths. A read error, an unknown flow, an unparseable body: if
the default there is *forward*, the proxy fails open exactly where it matters most.

## What d0 does

`d0` is a private tool of mine, a productivity tool for my own agentic coding, not a product: it
starts Claude Code, Codex or Antigravity in a throwaway Firecracker microVM, hands them a task and brings their
work back as a branch. Its proxy takes the strict end of each decision:

- One host per CLI, a few exact methods and paths, nothing else.
- The TLS server name and the `Host` header must equal that host byte for byte. No lowercasing, no trimmed dot: an odd
  spelling is refused, not repaired.
- The refresh token never leaves the host. No token endpoint is on the list, so there is no token response to mask.
  The safest masking code is the code you do not need.
- A response whose status line or headers carry the real token is not passed on.

```go
if r.Host != host && r.Host != host+":443" || r.URL.IsAbs() || r.URL.RawPath != "" ||
	r.URL.EscapedPath() != r.URL.Path || !maskedPathAllowed(credentials, r.Method, r.URL.Path) {
	status, reason = http.StatusForbidden, "path-refused"
}
```

One of the three CLIs d0 runs, Antigravity, keeps its token inside the VM. Google lets its unmodified CLI be run by
one's own tools as long as no credential is "extracted, read, or passed to the external tool"
([Google staff, October 2026](https://discuss.ai.google.dev/t/186246)), and masking would have to read it. So d0 only
passes Antigravity's traffic through, and every such run says so. Whether that setup is fine, I
[asked in Google's forum](https://discuss.ai.google.dev/t/personal-sandboxed-wrapper-around-the-unmodified-agy-cli-remaining-questions-separate-sign-in-connect-proxy-research/187827);
an answer is still open.

## The price

Strict costs something, and it shows. On 3 November I wanted OpenAI's Daybreak, a per-request switch for security work, in a
`d0 run`. Codex refused before sending anything: "Daybreak requires the OpenAI model provider". d0 routes Codex through
its own provider, so that the proxy sees every request. A looser proxy might have let the feature work. This one made
the conflict visible on the first try, which is the point.

That only works at my scale: one user, a few CLIs, Linux. A product for every agent and every company would drown in
refusals.

## Four questions for any credential proxy

1. What happens to a request it does not recognise?
2. Do SNI, `Host` and the routed name have to be the same string?
3. What does the masking code do on a read error?
4. Does the sandbox ever talk to a token endpoint at all?

::: nocount
Drafted with Claude from my notes, a research run and d0's code at commit 7e4d685; designed and tested, not externally
audited.
:::
