fix: harden gateway auth defaults

This commit is contained in:
Peter Steinberger
2026-01-26 18:18:55 +00:00
parent ab73aceb27
commit 3314b3996e
8 changed files with 65 additions and 151 deletions

View File

@@ -23,10 +23,10 @@ import { rawDataToString } from "../../../infra/ws.js";
import type { createSubsystemLogger } from "../../../logging/subsystem.js";
import { isGatewayCliClient, isWebchatClient } from "../../../utils/message-channel.js";
import type { ResolvedGatewayAuth } from "../../auth.js";
import { authorizeGatewayConnect } from "../../auth.js";
import { authorizeGatewayConnect, isLocalDirectRequest } from "../../auth.js";
import { loadConfig } from "../../../config/config.js";
import { buildDeviceAuthPayload } from "../../device-auth.js";
import { isLocalGatewayAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js";
import { isLoopbackAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js";
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
import {
type ConnectParams,
@@ -60,6 +60,17 @@ type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
const DEVICE_SIGNATURE_SKEW_MS = 10 * 60 * 1000;
function resolveHostName(hostHeader?: string): string {
const host = (hostHeader ?? "").trim().toLowerCase();
if (!host) return "";
if (host.startsWith("[")) {
const end = host.indexOf("]");
if (end !== -1) return host.slice(1, end);
}
const [name] = host.split(":");
return name ?? "";
}
type AuthProvidedKind = "token" | "password" | "none";
function formatGatewayAuthFailureMessage(params: {
@@ -189,8 +200,17 @@ export function attachGatewayWsMessageHandler(params: {
const hasProxyHeaders = Boolean(forwardedFor || realIp);
const remoteIsTrustedProxy = isTrustedProxyAddress(remoteAddr, trustedProxies);
const hasUntrustedProxyHeaders = hasProxyHeaders && !remoteIsTrustedProxy;
const isLocalClient = !hasUntrustedProxyHeaders && isLocalGatewayAddress(clientIp);
const reportedClientIp = hasUntrustedProxyHeaders ? undefined : clientIp;
const hostName = resolveHostName(requestHost);
const hostIsLocal = hostName === "localhost" || hostName === "127.0.0.1" || hostName === "::1";
const hostIsTailscaleServe = hostName.endsWith(".ts.net");
const hostIsLocalish = hostIsLocal || hostIsTailscaleServe;
const isLocalClient = isLocalDirectRequest(upgradeReq, trustedProxies);
const reportedClientIp =
isLocalClient || hasUntrustedProxyHeaders
? undefined
: clientIp && !isLoopbackAddress(clientIp)
? clientIp
: undefined;
if (hasUntrustedProxyHeaders) {
logWsControl.warn(
@@ -199,6 +219,13 @@ export function attachGatewayWsMessageHandler(params: {
"Configure gateway.trustedProxies to restore local client detection behind your proxy.",
);
}
if (!hostIsLocalish && isLoopbackAddress(remoteAddr) && !hasProxyHeaders) {
logWsControl.warn(
"Loopback connection with non-local Host header. " +
"Treating it as remote. If you're behind a reverse proxy, " +
"set gateway.trustedProxies and forward X-Forwarded-For/X-Real-IP.",
);
}
const isWebchatConnect = (p: ConnectParams | null | undefined) => isWebchatClient(p?.client);
@@ -347,32 +374,6 @@ export function attachGatewayWsMessageHandler(params: {
isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true;
const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth;
const device = disableControlUiDeviceAuth ? null : deviceRaw;
if (hasUntrustedProxyHeaders && resolvedAuth.mode === "none") {
setHandshakeState("failed");
setCloseCause("proxy-auth-required", {
client: connectParams.client.id,
clientDisplayName: connectParams.client.displayName,
mode: connectParams.client.mode,
version: connectParams.client.version,
});
send({
type: "res",
id: frame.id,
ok: false,
error: errorShape(
ErrorCodes.INVALID_REQUEST,
"gateway auth required behind reverse proxy",
{
details: {
hint: "set gateway.auth or configure gateway.trustedProxies",
},
},
),
});
close(1008, "gateway auth required");
return;
}
if (!device) {
const canSkipDevice = allowControlUiBypass ? hasSharedAuth : hasTokenAuth;
@@ -570,7 +571,8 @@ export function attachGatewayWsMessageHandler(params: {
trustedProxies,
});
let authOk = authResult.ok;
let authMethod = authResult.method ?? "none";
let authMethod =
authResult.method ?? (resolvedAuth.mode === "password" ? "password" : "token");
if (!authOk && connectParams.auth?.token && device) {
const tokenCheck = await verifyDeviceToken({
deviceId: device.id,