fix(gateway): keep production app ports private

This commit is contained in:
2026-07-19 22:24:28 +05:30
parent 5ce45b4973
commit 33730c68ab
4 changed files with 30 additions and 1 deletions
+9
View File
@@ -646,6 +646,15 @@ const MIGRATIONS: Migration[] = [
// Existing workspaces can invoke `wrnexus production` without generated-file changes.
},
},
{
version: "0.2.65",
id: "private-production-app-listeners",
description:
"Restricts workspace-managed production app servers to loopback so external clients must use the gateway.",
apply() {
// The production gateway applies the private bind address automatically.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+3
View File
@@ -337,6 +337,9 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
env: {
...process.env,
PORT: String(appPort),
// App ports are private gateway internals. Loopback prevents
// clients from bypassing gateway auth and edge middleware.
WRNEXUS_HOSTNAME: "127.0.0.1",
WRNEXUS_ENV: environment,
WRNEXUS_APP_NAME: app.name,
WRNEXUS_APP_ORIGIN: app.publicOrigin ?? `http://${app.domains[0]}:${port}`,
+10 -1
View File
@@ -141,6 +141,15 @@ function resolvePort(explicit?: number): number {
return Number.isFinite(parsed) ? parsed : 3000;
}
/** Resolve the production bind address. Gateway-managed app servers override
* this with loopback so only the gateway is externally reachable. */
export function resolveProductionHostname(
explicit?: string,
environmentHostname = process.env.WRNEXUS_HOSTNAME,
): string {
return environmentHostname?.trim() || explicit || "0.0.0.0";
}
/** Build the route-matching tables + a module map from the manifest. */
function buildProdRouter(manifest: ProdManifest): {
router: Router;
@@ -353,7 +362,7 @@ export async function createProductionServer(manifest: ProdManifest, opts: ProdO
const server = Bun.serve<WsData>({
port: resolvePort(opts.port),
hostname: opts.hostname ?? "0.0.0.0",
hostname: resolveProductionHostname(opts.hostname),
development: false,
maxRequestBodySize: opts.maxBodyBytes ?? 10 * 1024 * 1024,
fetch: handlers.fetch,
+8
View File
@@ -6,6 +6,14 @@ import {
gatewayProxyHeaders,
gatewayRestartDelay,
} from "../src/gateway.ts";
import { resolveProductionHostname } from "../src/prod.ts";
test("gateway-managed production apps bind to loopback", () => {
expect(resolveProductionHostname(undefined, "127.0.0.1")).toBe("127.0.0.1");
expect(resolveProductionHostname("0.0.0.0", "127.0.0.1")).toBe("127.0.0.1");
expect(resolveProductionHostname("10.0.0.5", "")).toBe("10.0.0.5");
expect(resolveProductionHostname(undefined, "")).toBe("0.0.0.0");
});
test("gateway uses platform-safe hostname defaults", () => {
expect(defaultGatewayHostname("development")).toBe("127.0.0.1");