Link to a minimal reproduction
https://github.com/tommy5dollar/eve-microsandbox-injection-repro
Steps to reproduce
The reproduction needs a Linux host with read/write access to /dev/kvm and network access to a
public HTTPS echo endpoint. This defect lives at a real TLS boundary, so it cannot be reproduced
offline.
git clone https://github.com/tommy5dollar/eve-microsandbox-injection-repro
cd eve-microsandbox-injection-repro
npm install
npm test
npm test runs one assertion set against three targets:
| target |
configuration |
stock |
eve's applyMicrosandboxNetwork, unmodified |
fixed |
the same call, plus one .tls((t) => t.interceptedPorts([443]).verifyUpstream(true)) |
control |
no eve: the same microsandbox secret and the same interception, built directly |
The stock run is expected to be red. That red is the reproduction, not a broken repository.
Only the fixed and control runs decide the exit code, so npm test exits 0 when the
reproduction holds.
fixed differs from stock by exactly one builder call. lib/tls-shim.mjs proxies the sandbox
builder so eve's own network(configure) call gains one appended .tls(...). eve's module is not
patched, not copied and not re-modelled - both runs execute the same shipped code from
node_modules.
What each target does, in one paragraph: it builds a microsandbox microVM the way
createMicrosandbox builds one, applies the network policy below, then runs a single HTTPS GET
from inside the guest carrying the placeholder eve exposed, and reads back the authorization
header the origin reports.
{
allow: {
"postman-echo.com": [
{ transform: [{ headers: { authorization: "Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET" } }] },
],
},
}
Single-shot variants, if the suite output is hard to read:
node probe.mjs # eve's chain, unmodified; exits 1 when the placeholder reached the origin
node probe.mjs fixed
node probe.mjs control
If your login session predates being added to the kvm group, run each command under
sg kvm -c "...". npx msb doctor reports the state of the host and prints the fix.
Current vs. expected behavior
Expected. A transform in a sandbox network policy substitutes the real credential at the
firewall. eve's own SandboxNetworkPolicy documentation states the purpose: "A per-domain
transform injects headers at the firewall so secrets never enter the sandbox process."
Actual, on the microsandbox backend. No substitution happens. The literal placeholder eve
generated (__EVE_MSB_SECRET_<hash>__) is sent to the real origin. The request succeeds at the
transport level, nothing throws, and no event distinguishes this from a working injection.
applyMicrosandboxNetwork builds a microsandbox secret per credentialed host with
injectHeaders(true), injectBasicAuth(true), requireTlsIdentity(true) and allowHost(...),
and sets trustHostCAs(true) on the network builder. It never calls .tls(...), so TLS
interception is never configured. Without interception the boundary cannot read or rewrite an
HTTPS request, so a requireTlsIdentity secret can never match and never substitutes.
Three variants, one variable changed:
| variant |
configuration |
authorization seen by the origin |
| 1. stock |
eve's chain (policyJson + secret + trustHostCAs, no .tls) |
__EVE_MSB_SECRET_a47a260bb090c8552fc8b384__ |
| 2. fixed |
identical, plus .tls((t) => t.interceptedPorts([443]).verifyUpstream(true)) |
Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET |
| 3. control |
no eve: the same secret and the same interception, built directly |
Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET |
Variant 3 is there to show that microsandbox substitutes a requireTlsIdentity secret correctly
when interception is on. The defect is in eve's translation, not in the substrate.
The consequence for a user is quiet. A brokered token reaches the origin as a placeholder string.
git clone against a brokered credential gets a 401, which is at least loud; any other
at-boundary credential simply fails to authenticate, or worse - reaches a host that ignores the
header. Nothing in the sandbox layer reports a problem, because nothing was denied and nothing
threw.
eve version
eve@0.44.3 (npm latest; also reproduces on 0.44.0 and 0.39.3; dist/src/execution/sandbox/bindings/microsandbox-network.js is byte-identical across all three - not a regression)
Environment
OS: Ubuntu 24.04.3 LTS on WSL2, Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
Node: v24.18.0, npm 11.16.0
eve: 0.44.3, npm latest (also confirmed on 0.44.0 and 0.39.3)
microsandbox: 0.5.10, which is what eve declares as its peerOptional range (^0.5.0). Also confirmed on 0.6.13.
msb: 0.5.10, supplied by the npm dependency; no separate install
KVM: /dev/kvm present and read/write. Intel APICv is disabled by KVM policy on this host; microsandbox states APICv is optional and that sandbox correctness and isolation are unaffected.
Guest image: alpine, for busybox wget with HTTPS. eve's own default image behaves the same way; the outcome is decided by the network builder, not by the image.
Note on `eve info`: it prints "Could not resolve an eve agent root" in the reproduction directory. That is expected. The repository is not a scaffolded agent - it imports the published binding module directly. The reason is in Additional context.
Where does the bug occur?
Production (eve start / deployed)
Deployment
n/a
Build and runtime logs
npm test, three targets, one assertion set
[FAIL] stock pass=0 fail=3
[pass] fixed pass=3 fail=0
[pass] control pass=3 fail=0
STOCK - eve's applyMicrosandboxNetwork, unmodified
placeholder exposed in the guest: __EVE_MSB_SECRET_a47a260bb090c8552fc8b384__
authorization seen by the origin: __EVE_MSB_SECRET_a47a260bb090c8552fc8b384__
not ok 1 - substitutes the real credential at the boundary
expected: 'Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET'
actual: '__EVE_MSB_SECRET_a47a260bb090c8552fc8b384__'
FIXED - the same call plus .tls((t) => t.interceptedPorts([443]).verifyUpstream(true))
placeholder exposed in the guest: __EVE_MSB_SECRET_a47a260bb090c8552fc8b384__
authorization seen by the origin: Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET
ok 1 - substitutes the real credential at the boundary
CONTROL - no eve: the same secret and the same interception, built directly
placeholder exposed in the guest: __CONTROL_PLACEHOLDER_NOT_A_SECRET__
authorization seen by the origin: Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET
ok 1 - substitutes the real credential at the boundary
The guest request exits 0 in every case. The origin returns 200. No error, no warning, no event.
The eve network plan the stock run applied, for reference:
policy -> default_egress deny, default_ingress deny
allow any:53 udp+tcp
allow domain postman-echo.com
secret -> env EVE_MSB_SECRET_A47A260BB090C8552FC8B384
placeholder __EVE_MSB_SECRET_a47a260bb090c8552fc8b384__
injectHeaders true, requireTlsIdentity true, allowHost postman-echo.com
builder -> trustHostCAs true
tls -> never configured
Suggested implementation prompt
Make the microsandbox backend's credential injection real: when a sandbox network policy carries
at least one transform-header rule, an HTTPS request from the guest to an allowed credentialed
host must arrive at the origin with the real header value, and the placeholder must never leave
the boundary.
The gap: applyMicrosandboxNetwork
(dist/src/execution/sandbox/bindings/microsandbox-network.js) registers each secret with
requireTlsIdentity(true) and sets trustHostCAs(true), but never configures TLS interception
on the network builder. Without interception the boundary cannot read or rewrite an HTTPS
request, so no secret ever matches - the guest's request goes through carrying the literal
placeholder, with no error and no event.
Constraints:
- Enable interception only when the policy has at least one transform-header rule - the branch
that already sets trustHostCAs(true). A policy with no credentials should not pay for
interception.
- The intercepted port list and the upstream-verification setting are your design calls. The
reproduction uses interceptedPorts([443]) and verifyUpstream(true) only because they are
the minimum that makes the boundary work; whatever you choose, this is a credential path, so
upstream certificate verification should stay on by default.
- Prefer loud over silent: if interception or a secret cannot be applied, fail the sandbox setup
rather than sending the placeholder to the real origin.
- Note for a wider fix:
MicrosandboxSandboxCreateOptions and MicrosandboxSessionUseOptions
expose no TLS hook today, so a consumer cannot work around this themselves. Exposing one is
optional; fixing the default translation is the bug.
Acceptance criteria:
- In https://github.com/tommy5dollar/eve-microsandbox-injection-repro, the
stock target's three
assertions go green against the fixed build: the origin receives the real value, the guest
still sees only the placeholder, and the request succeeds. (Today stock is red and only the
fixed/control targets pass; fixed is the same call plus one .tls(...).)
node substrate-checks.mjs still passes unchanged: per-host allow with deny-by-default egress
holds, and "deny-all" still gives the guest no network interface.
- A policy with no transform-header rules behaves exactly as before.
- Separable follow-on, not required here: honouring
transformHeaderRules[].match (method/path
narrowing - see "The second thing that looks wrong" in Additional context). Fixing interception
is what makes that gap observable; node match-drop.mjs fixed in the reproduction demonstrates
it.
Additional context
Why "Production" rather than the shape of the repro. The reproduction runs on a local host,
but the damage lands on a deployed self-hosted instance using the microsandbox backend, where the
brokered credential is the whole point of the policy. Nothing about the failure is local-dev
specific: the same applyMicrosandboxNetwork runs on every path that creates or re-policies a
microsandbox VM.
Not a regression. dist/src/execution/sandbox/bindings/microsandbox-network.js is
byte-identical across eve 0.39.3, 0.44.0 and 0.44.3 (npm latest) -
sha256 cdb48530e5ba5877fb0e41dda5e5eaf57968194eb934fa7b00843993dcf84a80 - and the reproduction was
confirmed on all three. microsandbox-runtime.js and microsandbox-create.js are byte-identical
across the same versions too. No bisect is needed.
Why the repository imports dist/ instead of scaffolding with eve init. The defect sits
below anything the scaffold produces. Reaching it through a scaffolded agent needs a live model, a
credentialed host and a tool call before one line of sandbox configuration decides the outcome,
which buries the signal in a lot of unrelated machinery. The repository calls the published module
directly so the subject under test is eve's own shipped translation and nothing else.
lib/eve-binding.mjs is the only import seam, and it uses a relative path into node_modules
because eve's exports map does not expose dist/. As a side effect eve info reports no agent
root in that directory.
What this report is not claiming. The microsandbox backend is in good shape on the parts I
measured, and none of them are in question here. node substrate-checks.mjs in the repository
prints the first two:
- Per-host allow rules and deny-by-default egress are enforced natively, at host grain, with no
sidecar. Measured: with { allow: { "example.com": [] } }, https://example.com/ is REACHED
and https://www.iana.org/ is BLOCKED.
"deny-all" maps to disableNetwork() rather than to a deny policy, so the guest gets no
network interface at all. Measured: the only interface in the guest is lo. That is a stronger
answer than a proxy that can be detached.
setNetworkPolicy recycles the VM through a snapshot - stop, snapshot, remove, recreate from
the snapshot - and the workspace survives.
- microsandbox itself substitutes a
requireTlsIdentity secret correctly; variant 3 shows it.
The substrate is right. One builder call is missing from the translation.
The second thing that looks wrong, adjacent and separable - a different root cause with a
different fix, so I have not filed it as its own issue. createMicrosandboxNetworkPlan copies each
rule's match into the plan object; applyMicrosandboxNetwork never reads it. Only domain,
headers and placeholderHeaders are used, and the serialized policy carries the host alone. The
type's own documentation states the contract: "When provided, transforms and forwarding rules only
apply to requests that match every specified dimension."
node match-drop.mjs fixed authorises the credential on GET /get only, then sends POST /post
to the same host:
plan.transformHeaderRules[0].match = {"method":["GET"],"path":{"startsWith":"/get"}}
GET /get inside the rule's match reached=true authorization at the origin: Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET
POST /post outside the rule's match reached=true authorization at the origin: Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRET
So a match narrows nothing on this backend: the credential is injected on every request to the
host. On the stock binding this is invisible, because nothing is injected at all - fixing TLS
interception is what makes the gap reachable. Happy to file it separately if you would rather keep
the two apart.
Link to a minimal reproduction
https://github.com/tommy5dollar/eve-microsandbox-injection-repro
Steps to reproduce
The reproduction needs a Linux host with read/write access to
/dev/kvmand network access to apublic HTTPS echo endpoint. This defect lives at a real TLS boundary, so it cannot be reproduced
offline.
npm testruns one assertion set against three targets:stockapplyMicrosandboxNetwork, unmodifiedfixed.tls((t) => t.interceptedPorts([443]).verifyUpstream(true))controlThe
stockrun is expected to be red. That red is the reproduction, not a broken repository.Only the
fixedandcontrolruns decide the exit code, sonpm testexits0when thereproduction holds.
fixeddiffers fromstockby exactly one builder call.lib/tls-shim.mjsproxies the sandboxbuilder so eve's own
network(configure)call gains one appended.tls(...). eve's module is notpatched, not copied and not re-modelled - both runs execute the same shipped code from
node_modules.What each target does, in one paragraph: it builds a microsandbox microVM the way
createMicrosandboxbuilds one, applies the network policy below, then runs a single HTTPSGETfrom inside the guest carrying the placeholder eve exposed, and reads back the
authorizationheader the origin reports.
Single-shot variants, if the suite output is hard to read:
node probe.mjs # eve's chain, unmodified; exits 1 when the placeholder reached the origin node probe.mjs fixed node probe.mjs controlIf your login session predates being added to the
kvmgroup, run each command undersg kvm -c "...".npx msb doctorreports the state of the host and prints the fix.Current vs. expected behavior
Expected. A
transformin a sandbox network policy substitutes the real credential at thefirewall. eve's own
SandboxNetworkPolicydocumentation states the purpose: "A per-domaintransforminjects headers at the firewall so secrets never enter the sandbox process."Actual, on the microsandbox backend. No substitution happens. The literal placeholder eve
generated (
__EVE_MSB_SECRET_<hash>__) is sent to the real origin. The request succeeds at thetransport level, nothing throws, and no event distinguishes this from a working injection.
applyMicrosandboxNetworkbuilds a microsandbox secret per credentialed host withinjectHeaders(true),injectBasicAuth(true),requireTlsIdentity(true)andallowHost(...),and sets
trustHostCAs(true)on the network builder. It never calls.tls(...), so TLSinterception is never configured. Without interception the boundary cannot read or rewrite an
HTTPS request, so a
requireTlsIdentitysecret can never match and never substitutes.Three variants, one variable changed:
authorizationseen by the originpolicyJson+secret+trustHostCAs, no.tls)__EVE_MSB_SECRET_a47a260bb090c8552fc8b384__.tls((t) => t.interceptedPorts([443]).verifyUpstream(true))Bearer EVE-REPRO-REAL-VALUE-NOT-A-SECRETBearer EVE-REPRO-REAL-VALUE-NOT-A-SECRETVariant 3 is there to show that microsandbox substitutes a
requireTlsIdentitysecret correctlywhen interception is on. The defect is in eve's translation, not in the substrate.
The consequence for a user is quiet. A brokered token reaches the origin as a placeholder string.
git cloneagainst a brokered credential gets a401, which is at least loud; any otherat-boundary credential simply fails to authenticate, or worse - reaches a host that ignores the
header. Nothing in the sandbox layer reports a problem, because nothing was denied and nothing
threw.
eve version
eve@0.44.3 (npm latest; also reproduces on 0.44.0 and 0.39.3;
dist/src/execution/sandbox/bindings/microsandbox-network.jsis byte-identical across all three - not a regression)Environment
Where does the bug occur?
Production (
eve start/ deployed)Deployment
n/a
Build and runtime logs
Suggested implementation prompt
Make the microsandbox backend's credential injection real: when a sandbox network policy carries
at least one transform-header rule, an HTTPS request from the guest to an allowed credentialed
host must arrive at the origin with the real header value, and the placeholder must never leave
the boundary.
The gap:
applyMicrosandboxNetwork(
dist/src/execution/sandbox/bindings/microsandbox-network.js) registers each secret withrequireTlsIdentity(true)and setstrustHostCAs(true), but never configures TLS interceptionon the network builder. Without interception the boundary cannot read or rewrite an HTTPS
request, so no secret ever matches - the guest's request goes through carrying the literal
placeholder, with no error and no event.
Constraints:
that already sets
trustHostCAs(true). A policy with no credentials should not pay forinterception.
reproduction uses
interceptedPorts([443])andverifyUpstream(true)only because they arethe minimum that makes the boundary work; whatever you choose, this is a credential path, so
upstream certificate verification should stay on by default.
rather than sending the placeholder to the real origin.
MicrosandboxSandboxCreateOptionsandMicrosandboxSessionUseOptionsexpose no TLS hook today, so a consumer cannot work around this themselves. Exposing one is
optional; fixing the default translation is the bug.
Acceptance criteria:
stocktarget's threeassertions go green against the fixed build: the origin receives the real value, the guest
still sees only the placeholder, and the request succeeds. (Today
stockis red and only thefixed/controltargets pass;fixedis the same call plus one.tls(...).)node substrate-checks.mjsstill passes unchanged: per-host allow with deny-by-default egressholds, and
"deny-all"still gives the guest no network interface.transformHeaderRules[].match(method/pathnarrowing - see "The second thing that looks wrong" in Additional context). Fixing interception
is what makes that gap observable;
node match-drop.mjs fixedin the reproduction demonstratesit.
Additional context
Why "Production" rather than the shape of the repro. The reproduction runs on a local host,
but the damage lands on a deployed self-hosted instance using the microsandbox backend, where the
brokered credential is the whole point of the policy. Nothing about the failure is local-dev
specific: the same
applyMicrosandboxNetworkruns on every path that creates or re-policies amicrosandbox VM.
Not a regression.
dist/src/execution/sandbox/bindings/microsandbox-network.jsisbyte-identical across eve 0.39.3, 0.44.0 and 0.44.3 (npm latest) -
sha256 cdb48530e5ba5877fb0e41dda5e5eaf57968194eb934fa7b00843993dcf84a80- and the reproduction wasconfirmed on all three.
microsandbox-runtime.jsandmicrosandbox-create.jsare byte-identicalacross the same versions too. No bisect is needed.
Why the repository imports
dist/instead of scaffolding witheve init. The defect sitsbelow anything the scaffold produces. Reaching it through a scaffolded agent needs a live model, a
credentialed host and a tool call before one line of sandbox configuration decides the outcome,
which buries the signal in a lot of unrelated machinery. The repository calls the published module
directly so the subject under test is eve's own shipped translation and nothing else.
lib/eve-binding.mjsis the only import seam, and it uses a relative path intonode_modulesbecause eve's
exportsmap does not exposedist/. As a side effecteve inforeports no agentroot in that directory.
What this report is not claiming. The microsandbox backend is in good shape on the parts I
measured, and none of them are in question here.
node substrate-checks.mjsin the repositoryprints the first two:
sidecar. Measured: with
{ allow: { "example.com": [] } },https://example.com/is REACHEDand
https://www.iana.org/is BLOCKED."deny-all"maps todisableNetwork()rather than to a deny policy, so the guest gets nonetwork interface at all. Measured: the only interface in the guest is
lo. That is a strongeranswer than a proxy that can be detached.
setNetworkPolicyrecycles the VM through a snapshot - stop, snapshot, remove, recreate fromthe snapshot - and the workspace survives.
requireTlsIdentitysecret correctly; variant 3 shows it.The substrate is right. One builder call is missing from the translation.
The second thing that looks wrong, adjacent and separable - a different root cause with a
different fix, so I have not filed it as its own issue.
createMicrosandboxNetworkPlancopies eachrule's
matchinto the plan object;applyMicrosandboxNetworknever reads it. Onlydomain,headersandplaceholderHeadersare used, and the serialized policy carries the host alone. Thetype's own documentation states the contract: "When provided, transforms and forwarding rules only
apply to requests that match every specified dimension."
node match-drop.mjs fixedauthorises the credential onGET /getonly, then sendsPOST /postto the same host:
So a
matchnarrows nothing on this backend: the credential is injected on every request to thehost. On the stock binding this is invisible, because nothing is injected at all - fixing TLS
interception is what makes the gap reachable. Happy to file it separately if you would rather keep
the two apart.