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
128 lines
5.8 KiB
Bash
128 lines
5.8 KiB
Bash
# Node Identity
|
|
NODE_ID=node-001
|
|
NODE_NAME="My Radio Node"
|
|
NODE_LAT=0.0
|
|
NODE_LON=0.0
|
|
|
|
# MQTT — point to your C2 server
|
|
#
|
|
# Post-cutover (MQTT-PUBLIC-AUTH-PLAN.md dynsec revision): there is no shared
|
|
# node login any more. The node authenticates as username=NODE_ID,
|
|
# password=<this node's C2-issued api_key> automatically — nothing to set
|
|
# here for that; the api_key is provisioned via MQTT after an admin approves
|
|
# the node (see credentials.json) and does not go in this file.
|
|
#
|
|
# Local/dev, pointed at a plaintext broker on :1883: leave MQTT_TLS unset.
|
|
MQTT_BROKER=localhost
|
|
MQTT_PORT=1883
|
|
MQTT_TLS=false
|
|
|
|
# Production, pointed at the public broker (real Let's Encrypt cert, default
|
|
# CA verification — do not disable it):
|
|
# MQTT_BROKER=mqtt.<domain>
|
|
# MQTT_PORT=8883
|
|
# MQTT_TLS=true
|
|
|
|
# DEPRECATED / REMOVED post-cutover — the shared "drb-node" login these
|
|
# backed no longer exists on the server (dynsec creates only per-node
|
|
# clients, keyed by api_key; see Server/drb-c2-core/app/internal/dynsec.py).
|
|
# Leave unset for any node pointed at a cut-over broker. Only meaningful as a
|
|
# legacy fallback if MQTT_BROKER still points at a pre-cutover broker running
|
|
# mosquitto's old password_file auth.
|
|
# MQTT_USER=drb-node
|
|
# MQTT_PASS=change-me-node
|
|
|
|
# C2 server for audio upload (leave blank to disable upload)
|
|
C2_URL=http://localhost:8888
|
|
# API key is provisioned automatically via MQTT after admin approves the node
|
|
|
|
# Icecast (local container — usually no need to change)
|
|
# Live listening only. Call recording and Discord voice use PulseAudio instead.
|
|
# 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
|
|
|
|
# PulseAudio capture (usually no need to change)
|
|
# Monitor of the drb_sink null sink that Liquidsoap writes into.
|
|
PULSE_SOURCE=drb_sink.monitor
|
|
# Seconds to wait for the shared PulseAudio socket before giving up and retrying.
|
|
PULSE_WAIT_TIMEOUT=30
|
|
|
|
# --- Call segmentation -------------------------------------------------------
|
|
# Recording boundaries come from the AUDIO, not the control channel: a recording
|
|
# starts at voice onset and ends after this many seconds of silence actually
|
|
# heard in the stream. Transmissions on the same talkgroup separated by less
|
|
# than this stay in ONE recording, so back-and-forth traffic is one file.
|
|
# Tune from the "measured trailing silence" line logged on every close.
|
|
CALL_SILENCE_TIMEOUT=3.0
|
|
|
|
# dBFS (RMS over one ~46ms chunk) below which audio counts as silence and the
|
|
# recording is allowed to close.
|
|
#
|
|
# This does NOT need calibrating against your radio's noise floor. Between
|
|
# transmissions the capture is the monitor of a PulseAudio *null sink*, which
|
|
# emits DIGITAL silence: measured on a live node it sits at about -91 dBFS —
|
|
# one least-significant bit of a 16-bit sample — while speech averages about
|
|
# -18 dBFS. Anything from roughly -70 to -40 behaves identically. Only change
|
|
# this if you have replaced the audio path with something that has a real
|
|
# analog noise floor.
|
|
CALL_SILENCE_THRESHOLD_DB=-50
|
|
|
|
# DEPRECATED as a primary control. Used ONLY when PulseAudio capture is not
|
|
# producing audio, where the old control-channel state machine takes over so
|
|
# the node still reports radio activity (with no recordings) while its audio
|
|
# path is broken.
|
|
CALL_IDLE_TIMEOUT=3
|
|
|
|
# Seconds of audio kept past a CONTROL-CHANNEL-derived boundary — a talkgroup
|
|
# change, or a close in the fallback mode above. Buffered audio lags the
|
|
# control channel by ~1.5s (grant-to-speech offset measured 0.84-1.62s), so
|
|
# cutting at the exact control-channel timestamp clipped the last words of the
|
|
# outgoing call. Does NOT apply to the normal end of a call any more; that
|
|
# boundary comes from the audio and needs no pad. Safe to be generous — the
|
|
# extra is trimmed off again before upload.
|
|
CALL_TAIL_PAD_SECONDS=3.0
|
|
|
|
# Strip leading/trailing dead air before upload. Recordings deliberately
|
|
# over-capture at both ends, and silence costs Whisper spend and makes it
|
|
# hallucinate text that was never spoken. Trimming is a sample-offset slice of
|
|
# the buffered PCM (no re-encode) and only ever touches the head and tail, with
|
|
# a guard margin so no syllable is clipped. Set to false to upload raw audio.
|
|
TRIM_SILENCE=true
|
|
# dBFS (RMS) below which audio counts as silence when trimming the ends. Kept
|
|
# stricter than CALL_SILENCE_THRESHOLD_DB on purpose.
|
|
TRIM_SILENCE_THRESHOLD_DB=-40
|
|
# Seconds of audio kept either side of detected speech.
|
|
TRIM_SILENCE_GUARD_SECONDS=0.25
|
|
|
|
# OP25 container (usually no need to change)
|
|
OP25_API_URL=http://localhost:8001
|
|
OP25_TERMINAL_URL=http://localhost:8081
|
|
# DEBUGGING AID, NOT A DEPLOYMENT OPTION. Both OP25's control API (:8001) and
|
|
# its HTTP terminal (:8081) have NO authentication, so they are bound to
|
|
# 127.0.0.1 by default — reachable only from other containers on this same
|
|
# host (they share its network namespace), not from the site's LAN. Setting
|
|
# this to true rebinds both to 0.0.0.0, exposing unauthenticated OP25
|
|
# start/stop/config-rewrite and the raw terminal to anyone on that LAN. Only
|
|
# for local development off a real node; leave false everywhere else.
|
|
OP25_DEBUG_EXPOSE=false
|
|
|
|
# --- Local dashboard / API login ---------------------------------------------
|
|
# Protects the node's local dashboard (port 80) and JSON API. The node is
|
|
# reachable by anyone on whatever site's LAN it's deployed to, so this MUST be
|
|
# changed before the node leaves the bench — the default below is flagged at
|
|
# every startup in the logs until it's changed.
|
|
DASHBOARD_USERNAME=admin
|
|
DASHBOARD_PASSWORD=CHANGE-ME-drb-default
|
|
|
|
# Container registry — set these to pull pre-built images instead of building locally.
|
|
# Must match the DOCKER_ORG variable and repo name configured in Gitea.
|
|
# Leave blank to always build locally.
|
|
IMAGE_REGISTRY=git.vpn.cusano.net
|
|
DOCKER_ORG=
|
|
DOCKER_REPO=
|