fix(gateway): trust bootstrap-paired backend handshakes

This commit is contained in:
Rai Butera 2026-03-14 15:49:49 +00:00
parent bb738058e6
commit ecbbc3f8ff
3 changed files with 81 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import {
evaluateMissingDeviceIdentity,
isTrustedProxyControlUiOperatorAuth,
resolveControlUiAuthPolicy,
resolveInternalBackendClientAttestation,
shouldSkipBackendSelfPairing,
shouldSkipControlUiPairing,
} from "./connect-policy.js";
@ -643,4 +644,56 @@ describe("ws connect policy", () => {
}),
).toBe(false);
});
test("promotes bootstrap-paired backend clients to internal attestation", () => {
const backendConnect: ConnectParams = {
client: {
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
mode: GATEWAY_CLIENT_MODES.BACKEND,
version: "1.0.0",
platform: "node",
},
minProtocol: 1,
maxProtocol: 1,
};
expect(
resolveInternalBackendClientAttestation({
connectParams: backendConnect,
hasBrowserOriginHeader: false,
initialIsInternalBackendClient: false,
authMethod: "bootstrap-token",
deviceTokenIssued: true,
}),
).toBe(true);
expect(
resolveInternalBackendClientAttestation({
connectParams: backendConnect,
hasBrowserOriginHeader: true,
initialIsInternalBackendClient: false,
authMethod: "bootstrap-token",
deviceTokenIssued: true,
}),
).toBe(false);
expect(
resolveInternalBackendClientAttestation({
connectParams: {
client: {
id: "desktop",
mode: GATEWAY_CLIENT_MODES.TEST,
version: "1.0.0",
platform: "node",
},
minProtocol: 1,
maxProtocol: 1,
},
hasBrowserOriginHeader: false,
initialIsInternalBackendClient: false,
authMethod: "bootstrap-token",
deviceTokenIssued: true,
}),
).toBe(false);
});
});

View File

@ -113,6 +113,25 @@ export function shouldSkipBackendSelfPairing(params: {
);
}
export function resolveInternalBackendClientAttestation(params: {
connectParams: ConnectParams;
hasBrowserOriginHeader: boolean;
initialIsInternalBackendClient: boolean;
authMethod: GatewayAuthResult["method"];
deviceTokenIssued: boolean;
}): boolean {
if (params.initialIsInternalBackendClient) {
return true;
}
const isGatewayBackendClient =
params.connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT &&
params.connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND;
if (!isGatewayBackendClient || params.hasBrowserOriginHeader) {
return false;
}
return params.authMethod === "bootstrap-token" && params.deviceTokenIssued;
}
export type MissingDeviceIdentityDecision =
| { kind: "allow" }
| { kind: "reject-control-ui-insecure-auth" }

View File

@ -83,6 +83,7 @@ import {
evaluateMissingDeviceIdentity,
isTrustedProxyControlUiOperatorAuth,
resolveControlUiAuthPolicy,
resolveInternalBackendClientAttestation,
shouldSkipBackendSelfPairing,
shouldSkipControlUiPairing,
} from "./connect-policy.js";
@ -891,6 +892,13 @@ export function attachGatewayWsMessageHandler(params: {
const deviceToken = device
? await ensureDeviceToken({ deviceId: device.id, role, scopes })
: null;
const attestedInternalBackendClient = resolveInternalBackendClientAttestation({
connectParams,
hasBrowserOriginHeader,
initialIsInternalBackendClient: isInternalBackendClient,
authMethod,
deviceTokenIssued: deviceToken !== null,
});
if (role === "node") {
const cfg = loadConfig();
@ -995,7 +1003,7 @@ export function attachGatewayWsMessageHandler(params: {
canvasHostUrl,
canvasCapability,
canvasCapabilityExpiresAtMs,
isInternalBackendClient,
isInternalBackendClient: attestedInternalBackendClient,
};
setSocketMaxPayload(socket, MAX_PAYLOAD_BYTES);
setClient(nextClient);