diff --git a/.changeset/brave-sandboxes-intercept.md b/.changeset/brave-sandboxes-intercept.md new file mode 100644 index 000000000..568714f8f --- /dev/null +++ b/.changeset/brave-sandboxes-intercept.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Broker microsandbox header credentials through its managed secret API so placeholders are replaced at the verified network boundary. diff --git a/packages/eve/src/execution/sandbox/bindings/microsandbox-network.ts b/packages/eve/src/execution/sandbox/bindings/microsandbox-network.ts index 1e17c2f10..ce60fb75a 100644 --- a/packages/eve/src/execution/sandbox/bindings/microsandbox-network.ts +++ b/packages/eve/src/execution/sandbox/bindings/microsandbox-network.ts @@ -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 { @@ -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 { diff --git a/packages/eve/src/execution/sandbox/bindings/microsandbox-runtime.test.ts b/packages/eve/src/execution/sandbox/bindings/microsandbox-runtime.test.ts index cb72e887d..652f600b8 100644 --- a/packages/eve/src/execution/sandbox/bindings/microsandbox-runtime.test.ts +++ b/packages/eve/src/execution/sandbox/bindings/microsandbox-runtime.test.ts @@ -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; }, @@ -649,13 +653,6 @@ function createMockNetworkBuilder() { policyJson() { return builder; }, - secret(configure: (secret: unknown) => unknown) { - configure(createMockSecretBuilder()); - return builder; - }, - trustHostCAs() { - return builder; - }, }; return builder; } diff --git a/packages/eve/src/execution/sandbox/bindings/microsandbox.test.ts b/packages/eve/src/execution/sandbox/bindings/microsandbox.test.ts index 4f8aae65f..8649668b3 100644 --- a/packages/eve/src/execution/sandbox/bindings/microsandbox.test.ts +++ b/packages/eve/src/execution/sandbox/bindings/microsandbox.test.ts @@ -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, @@ -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(methods: readonly Method[]) { + const builder = {} as Record>; + for (const method of methods) { + builder[method] = vi.fn(() => builder); + } + return builder; +}