Skip to content
Merged
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
11 changes: 10 additions & 1 deletion src/gateway/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
resolveLeastPrivilegeOperatorScopesForMethod,
type OperatorScope,
} from "./method-scopes.js";
import { allowsGatewayPrivateIngressNoAuth } from "./private-ingress-auth.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
export type { GatewayConnectionDetails };

Expand Down Expand Up @@ -267,10 +268,18 @@ function shouldOmitDeviceIdentityForGatewayCall(params: {
const mode = params.opts.mode ?? GATEWAY_CLIENT_MODES.CLI;
const clientName = params.opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI;
const hasSharedAuth = Boolean(params.token || params.password);
// BrowserOS private-ingress no-auth: backend gateway-client calls on
// loopback can omit device identity entirely when running with the env
// flag. Same trust boundary as the server-side bypasses in
// server-runtime-config and connect-policy. Without this, in-process
// backend tools (e.g. the agent's cron tool) try to load device.json
// and the connection then fails handshake under no-auth.
const allowsLoopbackNoAuth =
isLoopbackGatewayUrl(params.url) && allowsGatewayPrivateIngressNoAuth();
return (
mode === GATEWAY_CLIENT_MODES.BACKEND &&
clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT &&
hasSharedAuth &&
(hasSharedAuth || allowsLoopbackNoAuth) &&
isLoopbackGatewayUrl(params.url)
);
}
Expand Down
18 changes: 16 additions & 2 deletions src/gateway/role-policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isNodeRoleMethod } from "./method-scopes.js";
import { allowsGatewayPrivateIngressNoAuth } from "./private-ingress-auth.js";

const GATEWAY_ROLES = ["operator", "node"] as const;

Expand All @@ -11,8 +12,21 @@ export function parseGatewayRole(roleRaw: unknown): GatewayRole | null {
return null;
}

export function roleCanSkipDeviceIdentity(role: GatewayRole, sharedAuthOk: boolean): boolean {
return role === "operator" && sharedAuthOk;
export function roleCanSkipDeviceIdentity(
role: GatewayRole,
sharedAuthOk: boolean,
isLocalClient = false,
): boolean {
if (role !== "operator") {
return false;
}
if (sharedAuthOk) {
return true;
}
// BrowserOS private-ingress no-auth: trust loopback operator clients when
// the env flag is set. Mirrors the missing-device bypass in connect-policy
// and the bind-time bypass in server-runtime-config.
return isLocalClient && allowsGatewayPrivateIngressNoAuth();
}

export function isRoleAuthorizedForMethod(role: GatewayRole, method: string): boolean {
Expand Down
9 changes: 8 additions & 1 deletion src/gateway/server/ws-connection/connect-policy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { allowsGatewayPrivateIngressNoAuth } from "../../private-ingress-auth.js";
import type { ConnectParams } from "../../protocol/index.js";
import type { GatewayRole } from "../../role-policy.js";
import { roleCanSkipDeviceIdentity } from "../../role-policy.js";
Expand Down Expand Up @@ -140,7 +141,13 @@ export function evaluateMissingDeviceIdentity(params: {
return { kind: "reject-control-ui-insecure-auth" };
}
}
if (roleCanSkipDeviceIdentity(params.role, params.sharedAuthOk)) {
if (roleCanSkipDeviceIdentity(params.role, params.sharedAuthOk, params.isLocalClient)) {
return { kind: "allow" };
}
// BrowserOS private-ingress no-auth: extra defense for any future caller
// that bypasses roleCanSkipDeviceIdentity. Same trust boundary as the
// bind-time bypass in server-runtime-config (env flag set + loopback).
if (params.isLocalClient && allowsGatewayPrivateIngressNoAuth()) {
return { kind: "allow" };
}
if (!params.authOk && params.hasSharedAuth) {
Expand Down
Loading