From e6aab7589ab5a136f8869b16e7eecadf4009b81c Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Thu, 20 Aug 2026 03:12:44 -0400 Subject: [PATCH] Stop shipping "hackme" as the Icecast password The source password had a `hackme` fallback in five places -- entrypoint.sh, docker-compose.yml, setup.sh's prompt default, .env.example, edge-node's config.py -- plus op25-container's os.getenv default and two README rows. Any node whose operator pressed Enter through setup.sh is running a credential that is written down in this repo. That matters more than the usual default-password case because Icecast binds all interfaces and the SOURCE password is write access: it does not just let a LAN neighbour listen, it lets them PUSH audio into the stream the frontend and mobile clients play as live radio. Injecting fake traffic into a public-safety feed is the failure worth preventing. Approach: remove every fallback rather than change them to a better default. - icecast/entrypoint.sh refuses to start if either password is empty, and says how to generate one. This is the single hard gate; everything else is defence in depth behind it. - docker-compose.yml uses ${VAR:?message} so a missing value stops the stack at compose time with a readable error instead of becoming an empty string. - setup.sh GENERATES a random password when the operator presses Enter, via openssl rand -base64 24 with a /dev/urandom fallback. Pressing Enter now gives you a random password rather than a known one, which is the actual behaviour change -- a prompt default nobody types over is not a default, it is the value. - .env.example ships the keys empty with the generation command in a comment, and README.md now marks both as required with no default. Client suite: 185 passed. Note this does NOT rotate anything already deployed. node-002's .env still has whatever it was set up with; that is an operational step, tracked in the issue. Closes logan/node-26#3 --- .env.example | 6 ++++-- README.md | 4 ++-- docker-compose.yml | 6 ++++-- drb-edge-node/app/config.py | 3 ++- icecast/entrypoint.sh | 15 +++++++++++++-- op25-container/app/main.py | 2 +- setup.sh | 22 +++++++++++++++++++--- 7 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 0549c4a..ec8456b 100644 --- a/.env.example +++ b/.env.example @@ -38,8 +38,10 @@ C2_URL=http://localhost:8888 # Icecast (local container — usually no need to change) # Live listening only. Call recording and Discord voice use PulseAudio instead. -ICECAST_SOURCE_PASSWORD=hackme -ICECAST_ADMIN_PASSWORD=admin +# REQUIRED, no default — the container refuses to start without them. +# Generate with: openssl rand -base64 24 +ICECAST_SOURCE_PASSWORD= +ICECAST_ADMIN_PASSWORD= ICECAST_HOST=localhost ICECAST_PORT=8000 ICECAST_MOUNT=/radio diff --git a/README.md b/README.md index d8c7a82..3ae350d 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,8 @@ The node will appear as **pending** in the server admin dashboard. An admin must | `ICECAST_HOST` | No | `localhost` | Icecast hostname (leave as localhost — host network mode) | | `ICECAST_PORT` | No | `8000` | Icecast HTTP port | | `ICECAST_MOUNT` | No | `/radio` | Icecast mount point | -| `ICECAST_SOURCE_PASSWORD` | No | `hackme` | Icecast source password — change this | -| `ICECAST_ADMIN_PASSWORD` | No | `hackme` | Icecast admin password — change this | +| `ICECAST_SOURCE_PASSWORD` | **Yes** | none | Icecast source password. No default — the container refuses to start without it. `setup.sh` generates one; otherwise `openssl rand -base64 24` | +| `ICECAST_ADMIN_PASSWORD` | **Yes** | none | Icecast admin password. Same rules | | `OP25_API_URL` | No | `http://localhost:8001` | OP25 container HTTP API | | `OP25_TERMINAL_URL` | No | `http://localhost:8081` | OP25 HTTP terminal (live talkgroup metadata) | diff --git a/docker-compose.yml b/docker-compose.yml index e339da6..b47a795 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,10 @@ services: restart: unless-stopped network_mode: host environment: - ICECAST_SOURCE_PASSWORD: ${ICECAST_SOURCE_PASSWORD:-hackme} - ICECAST_ADMIN_PASSWORD: ${ICECAST_ADMIN_PASSWORD:-admin} + # :? not :- — a missing password must stop the stack, not silently + # become a credential that is published in this file. + ICECAST_SOURCE_PASSWORD: ${ICECAST_SOURCE_PASSWORD:?set ICECAST_SOURCE_PASSWORD in .env (run setup.sh, or openssl rand -base64 24)} + ICECAST_ADMIN_PASSWORD: ${ICECAST_ADMIN_PASSWORD:?set ICECAST_ADMIN_PASSWORD in .env (run setup.sh, or openssl rand -base64 24)} # No `ports:` here — network_mode: host makes it a no-op either way. The # control API (:8001) and OP25's HTTP terminal (:8081) are unauthenticated, diff --git a/drb-edge-node/app/config.py b/drb-edge-node/app/config.py index 44c853b..1ac29e1 100644 --- a/drb-edge-node/app/config.py +++ b/drb-edge-node/app/config.py @@ -42,7 +42,8 @@ class Settings(BaseSettings): icecast_host: str = "localhost" icecast_port: int = 8000 icecast_mount: str = "/radio" - icecast_source_password: str = "hackme" + # No default: see icecast/entrypoint.sh, which refuses to start without one. + icecast_source_password: str = "" # PulseAudio — the low-latency path used for call recording and Discord voice. # Liquidsoap (op25 container) writes into the `drb_sink` null sink; we capture diff --git a/icecast/entrypoint.sh b/icecast/entrypoint.sh index 3334ce7..82fd5e4 100644 --- a/icecast/entrypoint.sh +++ b/icecast/entrypoint.sh @@ -1,8 +1,19 @@ #!/bin/sh set -e -ICECAST_SOURCE_PASSWORD="${ICECAST_SOURCE_PASSWORD:-hackme}" -ICECAST_ADMIN_PASSWORD="${ICECAST_ADMIN_PASSWORD:-admin}" +# No defaults here on purpose. This container binds all interfaces, so a +# fallback password is a published credential on every node that ever accepted +# it -- and the source password is what lets a caller PUSH audio into the +# stream the frontend plays as live radio. Refuse to start instead. +for var in ICECAST_SOURCE_PASSWORD ICECAST_ADMIN_PASSWORD; do + eval "value=\${$var}" + if [ -z "$value" ]; then + echo "icecast: $var is not set." >&2 + echo "icecast: set it in the node's .env -- 'bash setup.sh' generates a random one," >&2 + echo "icecast: or run: openssl rand -base64 24" >&2 + exit 1 + fi +done export ICECAST_SOURCE_PASSWORD ICECAST_ADMIN_PASSWORD diff --git a/op25-container/app/main.py b/op25-container/app/main.py index 671a3ab..db09063 100644 --- a/op25-container/app/main.py +++ b/op25-container/app/main.py @@ -24,7 +24,7 @@ async def lifespan(app: FastAPI): icecast_host=os.getenv("ICECAST_HOST", "localhost"), icecast_port=int(os.getenv("ICECAST_PORT", "8000")), icecast_mountpoint=os.getenv("ICECAST_MOUNT", "/radio"), - icecast_password=os.getenv("ICECAST_SOURCE_PASSWORD", "hackme"), + icecast_password=os.getenv("ICECAST_SOURCE_PASSWORD", ""), ) generate_liquid_script(config) LOGGER.info("op25.liq generated from environment variables.") diff --git a/setup.sh b/setup.sh index 43a3620..1ab0bae 100644 --- a/setup.sh +++ b/setup.sh @@ -95,10 +95,26 @@ read -rp "MQTT username [drb-node]: " MQTT_USER; MQTT_USER="${MQTT_USER:-drb-nod read -rsp "MQTT password: " MQTT_PASS; echo ""; MQTT_PASS="${MQTT_PASS:-change-me-node}" # --- Icecast --- +# Generated, not defaulted. Icecast binds all interfaces, and the SOURCE +# password is what lets a caller push audio into the stream users listen to as +# live radio -- so a shared default is worse here than a forgotten password. +# Pressing Enter gives you a random one rather than a known one. +gen_password() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 24 | tr -d ' +' + else + head -c 24 /dev/urandom | base64 | tr -d ' +' + fi +} + echo "" -echo "Icecast passwords (local container)" -read -rsp "Source password [hackme]: " ICECAST_SOURCE; echo ""; ICECAST_SOURCE="${ICECAST_SOURCE:-hackme}" -read -rsp "Admin password [admin]: " ICECAST_ADMIN; echo ""; ICECAST_ADMIN="${ICECAST_ADMIN:-admin}" +echo "Icecast passwords (local container). Press Enter to generate a random one." +read -rsp "Source password [generate]: " ICECAST_SOURCE; echo "" +if [ -z "$ICECAST_SOURCE" ]; then ICECAST_SOURCE="$(gen_password)"; echo " generated a random source password"; fi +read -rsp "Admin password [generate]: " ICECAST_ADMIN; echo "" +if [ -z "$ICECAST_ADMIN" ]; then ICECAST_ADMIN="$(gen_password)"; echo " generated a random admin password"; fi # --- Write .env --- cat > .env <