diff --git a/.env.example b/.env.example index ea89325e..865e0d2d 100644 --- a/.env.example +++ b/.env.example @@ -49,7 +49,8 @@ ECONOMIC_API_APP_SECRET_TOKEN= # Edge broker defaults for shell relay and gateway dispatch. EDGE_BROKER_URL=http://edge-broker:4300 -EDGE_BROKER_SHARED_SECRET=truckwash-edge-dev +EDGE_BROKER_SHARED_SECRET= +EDGE_AUTH_MODE=strict # Redis credentials REDIS_CONFIG_HOST=redis diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 1e415a4b..51580bdd 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -52,9 +52,10 @@ services: dockerfile: services/edge-broker/Dockerfile container_name: edge-broker environment: - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} + EDGE_AUTH_MODE: ${EDGE_AUTH_MODE:-strict} ports: - - "4300:4300" + - "127.0.0.1:4300:4300" caddy: image: caddy:2.7.6-alpine @@ -98,7 +99,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "true" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -120,7 +121,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro diff --git a/docker-compose.yml b/docker-compose.yml index ccbcc3d2..b8b565b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,9 +86,10 @@ services: dockerfile: services/edge-broker/Dockerfile container_name: edge-broker environment: - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} + EDGE_AUTH_MODE: ${EDGE_AUTH_MODE:-strict} ports: - - "4300:4300" + - "127.0.0.1:4300:4300" caddy: image: caddy:2.7.6-alpine @@ -213,7 +214,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "true" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -234,7 +235,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -255,7 +256,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -276,7 +277,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -297,7 +298,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -318,7 +319,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/staging:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro @@ -339,7 +340,7 @@ services: environment: AUTO_COMPOSER_INSTALL: "false" EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET} volumes: - ./services/nginx/app:/var/www/html - ./services/edge-agent/dist:/services/edge-agent/dist:ro diff --git a/services/edge-broker/server.mjs b/services/edge-broker/server.mjs index 70eed202..8a2fdf35 100644 --- a/services/edge-broker/server.mjs +++ b/services/edge-broker/server.mjs @@ -25,15 +25,24 @@ function jsonResponse(res, statusCode, body) { } export function createBrokerServer(options = {}) { - const authMode = options.authMode || process.env.EDGE_AUTH_MODE || "stub"; + const authMode = options.authMode || process.env.EDGE_AUTH_MODE || "strict"; const sharedSecret = options.sharedSecret ?? process.env.EDGE_BROKER_SHARED_SECRET ?? ""; const commandTimeoutMs = options.commandTimeoutMs ?? 10000; const agents = new Map(); const pendingCommands = new Map(); const browserSessions = new Map(); + const isStubMode = authMode === "stub"; - const validateAgent = options.validateAgent || (async ({ gatewayId }) => ({ id: gatewayId })); - const validateShellSession = options.validateShellSession || (async ({ token }) => ({ id: token, gateway_id: 1, reason: "stub" })); + const validateAgent = options.validateAgent || (isStubMode + ? (async ({ gatewayId }) => ({ id: gatewayId })) + : (async () => { + throw new Error("Unauthorized"); + })); + const validateShellSession = options.validateShellSession || (isStubMode + ? (async ({ token }) => ({ id: token, gateway_id: 1, reason: "stub" })) + : (async () => { + throw new Error("Unauthorized"); + })); const closeShellSession = options.closeShellSession || (async () => ({})); const markBrowserSessionsClosed = (gatewayId, reason) => { @@ -113,9 +122,7 @@ export function createBrokerServer(options = {}) { socket.destroy(); return; } - if (authMode !== "stub") { - await validateAgent({ gatewayId, token, headers: req.headers }); - } + await validateAgent({ gatewayId, token, headers: req.headers }); wss.handleUpgrade(req, socket, head, (ws) => { ws.gatewayId = gatewayId; agents.set(gatewayId, ws); @@ -130,9 +137,7 @@ export function createBrokerServer(options = {}) { socket.destroy(); return; } - const session = authMode === "stub" - ? await validateShellSession({ token }) - : await validateShellSession({ token, headers: req.headers }); + const session = await validateShellSession({ token, headers: req.headers }); wss.handleUpgrade(req, socket, head, (ws) => { ws.sessionToken = token; diff --git a/services/edge-broker/test/broker.test.mjs b/services/edge-broker/test/broker.test.mjs index 4c6cce92..7607f10d 100644 --- a/services/edge-broker/test/broker.test.mjs +++ b/services/edge-broker/test/broker.test.mjs @@ -24,6 +24,18 @@ function waitForClose(socket) { }); } +function waitForCloseOrError(socket) { + return new Promise((resolve) => { + const onDone = () => { + socket.off("error", onDone); + socket.off("close", onDone); + resolve(); + }; + socket.once("error", onDone); + socket.once("close", onDone); + }); +} + async function waitFor(predicate, { timeoutMs = 1000, intervalMs = 10, description = "condition" } = {}) { const deadline = Date.now() + timeoutMs; @@ -215,3 +227,14 @@ test("broker closes browser shell sessions when the agent disconnects before she await broker.close(); }); + +test("broker defaults to strict auth when no validators are configured", async () => { + const broker = createBrokerServer(); + const address = await broker.listen(0); + const port = address.port; + + const agent = new WebSocket(`ws://127.0.0.1:${port}/ws/agent?gatewayId=701&token=agent-token`); + await waitForCloseOrError(agent); + + await broker.close(); +}); diff --git a/services/edge-broker/test/config.test.mjs b/services/edge-broker/test/config.test.mjs index 1783b44b..8c10e658 100644 --- a/services/edge-broker/test/config.test.mjs +++ b/services/edge-broker/test/config.test.mjs @@ -24,19 +24,19 @@ test("traefik does not expose a dedicated public edge broker port", () => { assert.doesNotMatch(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/); }); -test("base docker compose exposes the edge broker service on port 4300", () => { +test("base docker compose restricts edge broker host binding to loopback", () => { assert.match(baseComposeSource, /\bedge-broker:\b/); - assert.match(baseComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "4300:4300"/); + assert.match(baseComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "127\.0\.0\.1:4300:4300"/); }); -test("example docker compose exposes the edge broker service on port 4300", () => { +test("example docker compose restricts edge broker host binding to loopback", () => { assert.match(exampleComposeSource, /\bedge-broker:\b/); - assert.match(exampleComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "4300:4300"/); + assert.match(exampleComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "127\.0\.0\.1:4300:4300"/); }); -test("php services receive broker websocket environment defaults", () => { +test("compose config does not provide insecure broker secret defaults", () => { for (const composeSource of [baseComposeSource, exampleComposeSource]) { assert.match(composeSource, /EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/); - assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev\}/); + assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET\}/); } });