fix(gateway): add shared-secret fallback to trusted-proxy auth dispatcher
When auth.mode is "trusted-proxy" and proxy auth fails (e.g. internal connections that bypass the reverse proxy), fall back to token/password credentials if configured. This allows CLI, node hosts, ACP, and other internal services to authenticate directly while external users authenticate via the proxy. Also enable the tailscale overlay for trusted-proxy mode by removing the mode exclusion from the allowTailscale default.
This commit is contained in:
parent
8a05c05596
commit
41ea7173a2
@ -269,8 +269,7 @@ export function resolveGatewayAuth(params: {
|
||||
}
|
||||
|
||||
const allowTailscale =
|
||||
authConfig.allowTailscale ??
|
||||
(params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy");
|
||||
authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password");
|
||||
|
||||
return {
|
||||
mode,
|
||||
@ -379,6 +378,13 @@ export async function authorizeGatewayConnect(
|
||||
params.allowRealIpFallback === true,
|
||||
);
|
||||
|
||||
const limiter = params.rateLimiter;
|
||||
const ip =
|
||||
params.clientIp ??
|
||||
resolveRequestClientIp(req, trustedProxies, params.allowRealIpFallback === true) ??
|
||||
req?.socket?.remoteAddress;
|
||||
const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET;
|
||||
|
||||
if (auth.mode === "trusted-proxy") {
|
||||
if (!auth.trustedProxy) {
|
||||
return { ok: false, reason: "trusted_proxy_config_missing" };
|
||||
@ -396,6 +402,48 @@ export async function authorizeGatewayConnect(
|
||||
if ("user" in result) {
|
||||
return { ok: true, method: "trusted-proxy", user: result.user };
|
||||
}
|
||||
|
||||
// Trusted-proxy auth failed — try shared-secret fallback for internal
|
||||
// services (CLI, node host, ACP) that bypass the reverse proxy.
|
||||
// If no token/password is configured, there's no fallback available.
|
||||
if (!auth.token && !auth.password) {
|
||||
return { ok: false, reason: result.reason };
|
||||
}
|
||||
|
||||
// Rate-limit fallback attempts
|
||||
if (limiter) {
|
||||
const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
|
||||
if (!rlCheck.allowed) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "rate_limited",
|
||||
rateLimited: true,
|
||||
retryAfterMs: rlCheck.retryAfterMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Try token fallback
|
||||
if (connectAuth?.token && auth.token) {
|
||||
if (safeEqualSecret(connectAuth.token, auth.token)) {
|
||||
limiter?.reset(ip, rateLimitScope);
|
||||
return { ok: true, method: "token" };
|
||||
}
|
||||
limiter?.recordFailure(ip, rateLimitScope);
|
||||
return { ok: false, reason: "token_mismatch" };
|
||||
}
|
||||
|
||||
// Try password fallback
|
||||
if (connectAuth?.password && auth.password) {
|
||||
if (safeEqualSecret(connectAuth.password, auth.password)) {
|
||||
limiter?.reset(ip, rateLimitScope);
|
||||
return { ok: true, method: "password" };
|
||||
}
|
||||
limiter?.recordFailure(ip, rateLimitScope);
|
||||
return { ok: false, reason: "password_mismatch" };
|
||||
}
|
||||
|
||||
// Client didn't provide matching credentials — return original proxy failure
|
||||
return { ok: false, reason: result.reason };
|
||||
}
|
||||
|
||||
@ -403,12 +451,6 @@ export async function authorizeGatewayConnect(
|
||||
return { ok: true, method: "none" };
|
||||
}
|
||||
|
||||
const limiter = params.rateLimiter;
|
||||
const ip =
|
||||
params.clientIp ??
|
||||
resolveRequestClientIp(req, trustedProxies, params.allowRealIpFallback === true) ??
|
||||
req?.socket?.remoteAddress;
|
||||
const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET;
|
||||
if (limiter) {
|
||||
const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
|
||||
if (!rlCheck.allowed) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user