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
48 lines
1.8 KiB
Python
48 lines
1.8 KiB
Python
from fastapi import FastAPI
|
|
from contextlib import asynccontextmanager
|
|
import os
|
|
import routers.op25_controller as op25_controller
|
|
from internal.logger import create_logger
|
|
from internal.liquidsoap_config_utils import generate_liquid_script
|
|
from models import IcecastConfig
|
|
from config import settings, bind_host
|
|
|
|
LOGGER = create_logger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
if settings.op25_debug_expose:
|
|
LOGGER.warning(
|
|
"OP25_DEBUG_EXPOSE=true — the op25 control API (:8001) and OP25's "
|
|
"HTTP terminal (:8081) are bound to 0.0.0.0 and reachable by "
|
|
"ANYTHING on this node's LAN with NO authentication. This is a "
|
|
"debugging aid only; do not leave it set on a deployed node."
|
|
)
|
|
try:
|
|
config = IcecastConfig(
|
|
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", ""),
|
|
)
|
|
generate_liquid_script(config)
|
|
LOGGER.info("op25.liq generated from environment variables.")
|
|
except Exception as e:
|
|
LOGGER.error(f"Failed to generate op25.liq: {e}")
|
|
yield
|
|
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
app.include_router(op25_controller.create_op25_router(), prefix="/op25")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Launched directly (see Dockerfile CMD) instead of via `uvicorn main:app
|
|
# --host ...` so the bind address is driven by OP25_DEBUG_EXPOSE (config.py)
|
|
# rather than a value baked into the image at build time.
|
|
import uvicorn
|
|
|
|
uvicorn.run("main:app", host=bind_host(), port=8001, reload=True)
|