Summary
When a proxied upstream answers 401 on a host that matches a service carrying header substitutions,
the MITM relay forwards the status line and headers — including the upstream's Content-Length — and then
closes the connection without sending a single body byte. The vault's own request log records the exchange
with an empty error_code, so the truncation is silent. The same service relays 200s and 500s (with
larger bodies) intact, and 401s from hosts with no configured service relay intact, so the trigger is
specifically 401 + matched substitution service.
Environment
infisical/agent-vault:0.39.0 (latest release at the time of writing; main shows no response-relay
changes since), running as a Compose service, in the context of
toryt/scp-173 — a reusable development-container sandbox built
around this vault — MITM ingress, clients trusting the generated CA.
- One service configured for the affected host: match
dev.example.com — a bare hostname, no scheme, no
port, matching the whole host. The bare form is forced by the vault itself: it accepts only four host
forms and has no path field, so composing host + '/' for a whole-host match yields
dev.example.com/, which is none of them and is refused with a bare 400. auth: passthrough, three
credential substitutions with in: ['header'] (sentinel placeholders in the Authorization header,
substituted for real Basic values; in declared because the surface defaults to path and query).
- Upstream: an AWS CloudFront distribution fronting an IIS backend.
Reproduction
Through the proxy, from a client container, no Authorization header:
curl -v https://dev.example.com/server/0/app/userInfo
Result:
< HTTP/1.1 401 Unauthorized
< Content-Type: text/html
< Content-Length: 1293
< WWW-Authenticate: Basic
< Server: Microsoft-IIS/10.0
< Via: 1.1 …cloudfront.net (CloudFront)
< X-Amz-Cf-Id: ADXRDxMnpBQkdgKUTpO8ooS5TMrSU75tECFQRCJmCK1bF6u-48Ymxg==
…
curl: (18) transfer closed with 1293 bytes remaining to read
The X-Amz-Cf-Id above is from a controlled single-request window (2026-08-03T16:55:22Z–16:55:23Z, nothing
else crossing the proxy): it identifies this exact response on the CloudFront side, should the origin's
behavior ever need confirming from there. Note the closure is immediate — status, headers, and close within
about a second — so the proxy is actively ending the connection, not stalling on it.
Control, the identical request from a host without the proxy, forced --http1.1 and default HTTP/2
both: all 1293 bytes received, exit 0, twice. The upstream delivers the complete body to anyone not behind
the vault.
What narrows the trigger
All rows measured through the same proxy instance with plain curl, minutes apart:
| host |
service configured |
status |
WWW-Authenticate |
body delimiter |
outcome |
dev.example.com |
yes |
401 |
Basic |
Content-Length 1293 |
0 bytes, curl: (18) |
dev.example.com |
yes |
500 |
— |
Content-Length 2501 |
complete |
dev.example.com |
yes |
500 |
— |
Content-Length 2513 |
complete |
dev.example.com |
yes |
200 |
— |
Content-Length 855 |
complete |
api.github.com |
no |
401 |
absent |
Content-Length 120 |
complete |
the-internet.herokuapp.com |
no |
401 |
Basic realm=… |
Content-Length 15 |
complete |
authenticationtest.com |
no |
401 |
Basic realm=… |
chunked, 3261 bytes |
complete |
Hypotheses this kills:
- Not the status alone: unconfigured hosts relay 401 bodies intact.
- Not the host alone: the same configured service relays 500 bodies intact.
- Not
WWW-Authenticate: an unconfigured host's 401 carrying that header with a
Content-Length-delimited body — the same delimiter as the truncated response — relays intact (the
herokuapp row; the chunked row widens the evidence across the other relay path).
- Not body size or a cap: the surviving 500 bodies (2501, 2513) are larger than the dropped 1293.
The vault's own log at the moment of truncation
The management API request log for the exact event (gateway-logs.js, i.e. /logs):
{
"ingress": "mitm",
"method": "GET",
"host": "dev.example.com:443",
"path": "/server/0/app/userInfo",
"matched_service": "dev-backend",
"credential_keys": ["USER_A", "USER_B", "USER_C"],
"status": 401,
"latency_ms": 1244,
"error_code": ""
}
No error is recorded, in this entry or anywhere near it. From the vault's own telemetry the relay looks
successful. This entry is the single-request window above — the request log carries no field correlating to
X-Amz-Cf-Id, so the match is by timestamp on an otherwise quiet proxy. The latency_ms is a second,
independent symptom of the same blindness: the vault believes it spent 1.24 s successfully relaying a body
of which it delivered nothing.
Impact
- A direct client gets
curl: (18) — annoying but visible.
- A client behind any Node
http-proxy-family reverse proxy (Vite dev-server proxies, http-server)
hangs forever: those proxies pipe the upstream response and only end the client response on end,
which never comes; the 401 headers are never even flushed. A 30 s client timeout expiring with no status
is the observable symptom, far from the cause.
- Any workflow that must fail authentication on purpose — negative tests, credential probes — breaks
precisely on the service the vault brokers.
Notes on mechanism (speculation, labelled as such)
The trigger shape suggests the response path for a 401 from a service the vault holds credentials for is
special-cased (credential-invalidity detection?) and consumes or discards the body instead of relaying it.
If dropping the body is ever intentional, the honest behaviors would be rewriting Content-Length, or
answering an explicit 502 with an X-Agent-Vault-Proxy-Error before streaming — the pattern #227
established for the body-cap case — and recording a non-empty error_code either way.
Provenance
This report was researched and written by Claude (Claude Code, Anthropic) — an agent running inside a
sandbox this vault brokers, which makes it one of the product's intended users. The measurements were taken
by two Claude Code sessions on either side of the gateway and by the human operator on the host; the
operator reviewed the report and filed it.
Summary
When a proxied upstream answers 401 on a host that matches a service carrying header substitutions,
the MITM relay forwards the status line and headers — including the upstream's
Content-Length— and thencloses the connection without sending a single body byte. The vault's own request log records the exchange
with an empty
error_code, so the truncation is silent. The same service relays 200s and 500s (withlarger bodies) intact, and 401s from hosts with no configured service relay intact, so the trigger is
specifically 401 + matched substitution service.
Environment
infisical/agent-vault:0.39.0(latest release at the time of writing;mainshows no response-relaychanges since), running as a Compose service, in the context of
toryt/scp-173— a reusable development-container sandbox builtaround this vault — MITM ingress, clients trusting the generated CA.
dev.example.com— a bare hostname, no scheme, noport, matching the whole host. The bare form is forced by the vault itself: it accepts only four host
forms and has no
pathfield, so composinghost + '/'for a whole-host match yieldsdev.example.com/, which is none of them and is refused with a bare400.auth: passthrough, threecredential substitutions with
in: ['header'](sentinel placeholders in theAuthorizationheader,substituted for real
Basicvalues;indeclared because the surface defaults to path and query).Reproduction
Through the proxy, from a client container, no
Authorizationheader:Result:
The
X-Amz-Cf-Idabove is from a controlled single-request window (2026-08-03T16:55:22Z–16:55:23Z, nothingelse crossing the proxy): it identifies this exact response on the CloudFront side, should the origin's
behavior ever need confirming from there. Note the closure is immediate — status, headers, and close within
about a second — so the proxy is actively ending the connection, not stalling on it.
Control, the identical request from a host without the proxy, forced
--http1.1and default HTTP/2both: all 1293 bytes received, exit 0, twice. The upstream delivers the complete body to anyone not behind
the vault.
What narrows the trigger
All rows measured through the same proxy instance with plain
curl, minutes apart:WWW-Authenticatedev.example.comBasicContent-Length1293curl: (18)dev.example.comContent-Length2501dev.example.comContent-Length2513dev.example.comContent-Length855api.github.comContent-Length120the-internet.herokuapp.comBasic realm=…Content-Length15authenticationtest.comBasic realm=…Hypotheses this kills:
WWW-Authenticate: an unconfigured host's 401 carrying that header with aContent-Length-delimited body — the same delimiter as the truncated response — relays intact (theherokuapp row; the chunked row widens the evidence across the other relay path).
The vault's own log at the moment of truncation
The management API request log for the exact event (
gateway-logs.js, i.e./logs):{ "ingress": "mitm", "method": "GET", "host": "dev.example.com:443", "path": "/server/0/app/userInfo", "matched_service": "dev-backend", "credential_keys": ["USER_A", "USER_B", "USER_C"], "status": 401, "latency_ms": 1244, "error_code": "" }No error is recorded, in this entry or anywhere near it. From the vault's own telemetry the relay looks
successful. This entry is the single-request window above — the request log carries no field correlating to
X-Amz-Cf-Id, so the match is by timestamp on an otherwise quiet proxy. Thelatency_msis a second,independent symptom of the same blindness: the vault believes it spent 1.24 s successfully relaying a body
of which it delivered nothing.
Impact
curl: (18)— annoying but visible.http-proxy-family reverse proxy (Vite dev-server proxies,http-server)hangs forever: those proxies pipe the upstream response and only end the client response on
end,which never comes; the 401 headers are never even flushed. A 30 s client timeout expiring with no status
is the observable symptom, far from the cause.
precisely on the service the vault brokers.
Notes on mechanism (speculation, labelled as such)
The trigger shape suggests the response path for a 401 from a service the vault holds credentials for is
special-cased (credential-invalidity detection?) and consumes or discards the body instead of relaying it.
If dropping the body is ever intentional, the honest behaviors would be rewriting
Content-Length, oranswering an explicit 502 with an
X-Agent-Vault-Proxy-Errorbefore streaming — the pattern #227established for the body-cap case — and recording a non-empty
error_codeeither way.Provenance
This report was researched and written by Claude (Claude Code, Anthropic) — an agent running inside a
sandbox this vault brokers, which makes it one of the product's intended users. The measurements were taken
by two Claude Code sessions on either side of the gateway and by the human operator on the host; the
operator reviewed the report and filed it.