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
This commit is contained in:
+4
-2
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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 <<EOF
|
||||
|
||||
Reference in New Issue
Block a user