Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-sandboxes-intercept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Broker microsandbox header credentials through its managed secret API so placeholders are replaced at the verified network boundary.
71 changes: 32 additions & 39 deletions packages/eve/src/execution/sandbox/bindings/microsandbox-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ interface MicrosandboxSecretBuilderShape {
interface MicrosandboxNetworkBuilderShape {
enabled(enabled: boolean): this;
policyJson(json: string): this;
secret(
configure: (secret: MicrosandboxSecretBuilderShape) => MicrosandboxSecretBuilderShape,
): this;
trustHostCAs(enabled: boolean): this;
}

interface MicrosandboxTransformHeaderRule {
Expand Down Expand Up @@ -81,48 +77,45 @@ export function applyMicrosandboxNetwork(
return builder;
}

return builder.network((network: MicrosandboxNetworkBuilderShape) => {
let next = builder.network((network: MicrosandboxNetworkBuilderShape) => {
let next = network.enabled(true);
if (networkPlan.policy !== null) {
next = next.policyJson(serializeMicrosandboxNetworkPolicyJson(networkPlan.policy));
}
if (networkPlan.transformHeaderRules.length === 0) {
return next;
}
return next;
});

next = next.trustHostCAs(true);
for (const rule of networkPlan.transformHeaderRules) {
for (const [headerName, headerValue] of Object.entries(rule.headers)) {
const placeholder = rule.placeholderHeaders[headerName];
if (placeholder === undefined) {
continue;
}
const secretEnvName = createSecretEnvName(rule.domain, headerName, headerValue);
next = next.secret((secret) => {
let configured = secret
.env(secretEnvName)
.value(headerValue)
.placeholder(placeholder)
.injectHeaders(true)
.injectBasicAuth(true)
.injectQuery(false)
.injectBody(false)
.requireTlsIdentity(true);

if (rule.domain === "*") {
configured = configured.allowAnyHostDangerous(true);
} else if (rule.domain.startsWith("*.")) {
configured = configured.allowHostPattern(rule.domain);
} else {
configured = configured.allowHost(rule.domain);
}

return configured;
});
for (const rule of networkPlan.transformHeaderRules) {
for (const [headerName, headerValue] of Object.entries(rule.headers)) {
const placeholder = rule.placeholderHeaders[headerName];
if (placeholder === undefined) {
continue;
}
const secretEnvName = createSecretEnvName(rule.domain, headerName, headerValue);
next = next.secret((secret: MicrosandboxSecretBuilderShape) => {
let configured = secret
.env(secretEnvName)
.value(headerValue)
.placeholder(placeholder)
.injectHeaders(true)
.injectBasicAuth(true)
.injectQuery(false)
.injectBody(false)
.requireTlsIdentity(true);

if (rule.domain === "*") {
configured = configured.allowAnyHostDangerous(true);
} else if (rule.domain.startsWith("*.")) {
configured = configured.allowHostPattern(rule.domain);
} else {
configured = configured.allowHost(rule.domain);
}

return configured;
});
}
return next;
});
}
return next;
}

export function serializeMicrosandboxNetworkPolicyJson(policy: MicrosandboxNetworkPolicy): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,10 @@ function createMockSandboxBuilder(create: (fromSnapshot: string) => unknown) {
configure(createMockNetworkBuilder());
return builder;
},
secret(configure: (secret: unknown) => unknown) {
configure(createMockSecretBuilder());
return builder;
},
pullPolicy() {
return builder;
},
Expand Down Expand Up @@ -649,13 +653,6 @@ function createMockNetworkBuilder() {
policyJson() {
return builder;
},
secret(configure: (secret: unknown) => unknown) {
configure(createMockSecretBuilder());
return builder;
},
trustHostCAs() {
return builder;
},
};
return builder;
}
Expand Down
71 changes: 71 additions & 0 deletions packages/eve/src/execution/sandbox/bindings/microsandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

import { createMicrosandboxSandboxBackend } from "#execution/sandbox/bindings/microsandbox.js";
import {
applyMicrosandboxNetwork,
createMicrosandboxNetworkPlan,
createTransformBrokerEnvironment,
serializeMicrosandboxNetworkPolicyJson,
Expand Down Expand Up @@ -225,3 +226,73 @@ describe.skipIf(onWindows)("createMicrosandboxNetworkPlan", () => {
});
});
});

describe("applyMicrosandboxNetwork", () => {
it("registers brokered header transforms through sandbox secrets", () => {
const secret = createFluentMock([
"allowAnyHostDangerous",
"allowHost",
"allowHostPattern",
"env",
"injectBasicAuth",
"injectBody",
"injectHeaders",
"injectQuery",
"placeholder",
"requireTlsIdentity",
"value",
]);
const network = {
enabled: vi.fn(() => network),
policyJson: vi.fn(() => network),
};
const sandbox = {
disableNetwork: vi.fn(),
network: vi.fn((configure: (builder: typeof network) => unknown) => {
configure(network);
return sandbox;
}),
secret: vi.fn((configure: (builder: typeof secret) => unknown) => {
configure(secret);
return sandbox;
}),
};

applyMicrosandboxNetwork(sandbox as never, {
allow: {
"api.example.com": [{ transform: [{ headers: { authorization: "Bearer real-secret" } }] }],
},
});

expect(sandbox.secret).toHaveBeenCalledOnce();
expect(secret.requireTlsIdentity).toHaveBeenCalledWith(true);
expect(secret.allowHost).toHaveBeenCalledWith("api.example.com");
});

it("does not register sandbox secrets without brokered header transforms", () => {
const network = {
enabled: vi.fn(() => network),
policyJson: vi.fn(() => network),
};
const sandbox = {
disableNetwork: vi.fn(),
network: vi.fn((configure: (builder: typeof network) => unknown) => {
configure(network);
return sandbox;
}),
secret: vi.fn(),
};

applyMicrosandboxNetwork(sandbox as never, { allow: ["api.example.com"] });

expect(sandbox.secret).not.toHaveBeenCalled();
});
});

function createFluentMock<const Method extends string>(methods: readonly Method[]) {
const builder = {} as Record<Method, ReturnType<typeof vi.fn>>;
for (const method of methods) {
builder[method] = vi.fn(() => builder);
}
return builder;
}
Loading