From 5eb5ae46dd5816c521bbd4630a6a0dd3da7b810f Mon Sep 17 00:00:00 2001 From: Justin Gray Date: Mon, 29 Jun 2026 12:50:18 +0000 Subject: [PATCH] fix(auth): treat Tailscale Serve loopback host as remote-reachable When Tailscale Serve is enabled it proxies the loopback-bound backend onto the tailnet, so the server is reachable from other devices even though `config.host` stays on 127.0.0.1. Previously the auth policy keyed solely off the bind host and selected a local-only policy in that case, skipping the one-time-token bootstrap required for remote access. Treat the server as remote-reachable whenever `tailscaleServeEnabled` is set, in addition to the existing wildcard / non-loopback host checks, so remote clients get the `remote-reachable` policy with one-time-token bootstrap. Co-Authored-By: Claude Opus 4.8 --- .../src/auth/EnvironmentAuthPolicy.test.ts | 18 ++++++++++++++++++ apps/server/src/auth/EnvironmentAuthPolicy.ts | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/server/src/auth/EnvironmentAuthPolicy.test.ts b/apps/server/src/auth/EnvironmentAuthPolicy.test.ts index 95269fb6c37..8205f32a63a 100644 --- a/apps/server/src/auth/EnvironmentAuthPolicy.test.ts +++ b/apps/server/src/auth/EnvironmentAuthPolicy.test.ts @@ -97,6 +97,24 @@ it.layer(NodeServices.layer)("EnvironmentAuthPolicy.layer", (it) => { ), ); + it.effect("uses remote-reachable policy when Tailscale Serve exposes a loopback host", () => + Effect.gen(function* () { + const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy; + const descriptor = yield* policy.getDescriptor(); + + expect(descriptor.policy).toBe("remote-reachable"); + expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]); + }).pipe( + Effect.provide( + makeEnvironmentAuthPolicyLayer({ + mode: "web", + host: "127.0.0.1", + tailscaleServeEnabled: true, + }), + ), + ), + ); + it.effect("uses remote-reachable policy for non-loopback web hosts", () => Effect.gen(function* () { const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy; diff --git a/apps/server/src/auth/EnvironmentAuthPolicy.ts b/apps/server/src/auth/EnvironmentAuthPolicy.ts index 7ffef0ff0a5..0c62d54877a 100644 --- a/apps/server/src/auth/EnvironmentAuthPolicy.ts +++ b/apps/server/src/auth/EnvironmentAuthPolicy.ts @@ -16,7 +16,10 @@ export class EnvironmentAuthPolicy extends Context.Service< export const make = Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; - const isRemoteReachable = isWildcardHost(config.host) || !isLoopbackHost(config.host); + // Tailscale Serve proxies the loopback-bound backend onto the tailnet, so the + // server is remotely reachable even though config.host stays on 127.0.0.1. + const isRemoteReachable = + config.tailscaleServeEnabled || isWildcardHost(config.host) || !isLoopbackHost(config.host); const policy = config.mode === "desktop"