From 64489744758fa6c351ebe1060ab8f3002474c7d6 Mon Sep 17 00:00:00 2001 From: sparkDash contributor Date: Sun, 2 Aug 2026 14:26:55 +1000 Subject: [PATCH] feat: default BIND_HOST to loopback so the dashboard is not LAN-exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard is unauthenticated and ships SSH exec plus shutdown-all and Wake-on-LAN, but the server defaulted to 0.0.0.0 — exposing those controls on every LAN interface for bare-metal `npm start` users. Default BIND_HOST to 127.0.0.1, warn at startup when bound to a non-loopback address, and document opting in to the LAN. Docker is unaffected: docker-compose.yml sets BIND_HOST=0.0.0.0 explicitly, so this only changes the unset-env default. --- .env.example | 3 +++ CHANGELOG.md | 7 +++++++ README.md | 9 ++++++--- server/index.js | 12 +++++++++++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 8936f71..73933a2 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ PORT=5555 +# Listen address. Loopback by default (the dashboard exposes SSH + power controls); +# set to the host LAN IP or 0.0.0.0 to allow access from another machine. +BIND_HOST=127.0.0.1 LLM_PORT=8888 POLL_INTERVAL_GPU=2000 POLL_INTERVAL_CPU=2000 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff5aa4..b683866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ Format: version sections are listed newest first. --- +## [Unreleased] + +### Security +- **`BIND_HOST` now defaults to `127.0.0.1` (loopback) instead of `0.0.0.0`** — the dashboard is unauthenticated and can SSH into and power off Sparks, so it is no longer reachable on the LAN by default. Set `BIND_HOST` to the host's LAN IP (or `0.0.0.0`) to opt in to remote access. **Migration:** if you access sparkDash from another machine via bare-metal `npm start`, set `BIND_HOST` explicitly. Docker is unaffected — `docker-compose.yml` already sets `BIND_HOST=0.0.0.0`. Startup now also warns when bound to a non-loopback address. + +--- + ## [1.4.7] — 2026-08-02 ### Added diff --git a/README.md b/README.md index 4fc907c..51bc554 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ Copy `.env.example` to `.env` if needed: | Variable | Default | Description | |----------|---------|-------------| -| `BIND_HOST` | `0.0.0.0` | HTTP and WebSocket listen address | +| `BIND_HOST` | `127.0.0.1` | HTTP and WebSocket listen address. Loopback by default — the dashboard exposes SSH + power controls, so set a LAN IP (or `0.0.0.0`) to allow remote access. | | `PORT` | `5555` | HTTP + WebSocket listen port | | `LLM_PORT` | `8888` | Default LLM probe port | | `POLL_INTERVAL_GPU` | `2000` | GPU poll (ms) | @@ -243,8 +243,11 @@ Copy `.env.example` to `.env` if needed: | `HOST_SYS_PATH` | `/host/sys` | Host sys mount | | `HOST_ROOT_PATH` | `/host/root` | Host root mount | -> When using Docker's default bridge network, keep `BIND_HOST=0.0.0.0`. -> With `network_mode: host`, use `BIND_HOST=127.0.0.1` to restrict access to the local host or a reverse proxy. +> The listener defaults to `127.0.0.1` (loopback) so the dashboard — which can SSH into and +> power off your Sparks — isn't reachable on the LAN by default. Set `BIND_HOST` to the host's +> LAN IP (or `0.0.0.0`) to reach it from another machine. The provided `docker-compose.yml` +> (`network_mode: host`) sets `BIND_HOST=0.0.0.0` explicitly; restrict access at the network +> layer, or set `127.0.0.1` when running behind a reverse proxy. ### Adding a Spark diff --git a/server/index.js b/server/index.js index ba8181d..2a1bba4 100644 --- a/server/index.js +++ b/server/index.js @@ -25,7 +25,10 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ROOT = path.resolve(__dirname, ".."); -const BIND_HOST = process.env.BIND_HOST || "0.0.0.0"; +// Default to loopback: the dashboard exposes SSH and remote power controls, so it +// should not be reachable on the LAN unless explicitly opted in. Set BIND_HOST to the +// host's LAN IP (or 0.0.0.0) to expose it; the provided docker-compose.yml sets it. +const BIND_HOST = process.env.BIND_HOST || "127.0.0.1"; const PORT = parseInt(process.env.PORT || "5555", 10); const LLM_PORT = parseInt(process.env.LLM_PORT || "8888", 10); @@ -1164,6 +1167,13 @@ startBroadcast(); server.listen(PORT, BIND_HOST, () => { console.log(`[sparkDash] server listening on http://${BIND_HOST}:${PORT}`); console.log(`[sparkDash] WebSocket endpoint ws://${BIND_HOST}:${PORT}/ws`); + const isLoopback = + BIND_HOST === "localhost" || BIND_HOST === "::1" || /^127\./.test(BIND_HOST); + if (isLoopback) { + console.log(`[sparkDash] localhost-only; set BIND_HOST= (or 0.0.0.0) to allow remote access`); + } else { + console.warn(`[sparkDash] WARNING: bound to ${BIND_HOST} — reachable on the LAN. This dashboard is unauthenticated and can SSH into and power off your Sparks; restrict access at the network/firewall layer.`); + } startAllMonitors(); });