From 19126ade89af6e60c897d020f2393d36f0de2540 Mon Sep 17 00:00:00 2001 From: Zach Clendenen Date: Fri, 16 Oct 2026 13:59:13 -0500 Subject: [PATCH 1/2] fix(streaming): let a container's broker_secret override STREAMING_BROKER_SECRET STREAMING_BROKER_SECRET replaced every container's broker_secret, so a pool of containers with different BROKER_SECRETs could not authenticate: each broker after the first answered 403 "bad broker secret". A container's own broker_secret now wins, and the env var is the fallback for containers that set none. A blank broker_secret also falls back instead of becoming the literal secret "None". --- backend/handler/streaming/config.py | 2 +- backend/tests/endpoints/test_streaming.py | 18 ++++++++++++++++++ env.template | 2 +- examples/config.example.yml | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/backend/handler/streaming/config.py b/backend/handler/streaming/config.py index 271bf7fc24..9af1e2e705 100644 --- a/backend/handler/streaming/config.py +++ b/backend/handler/streaming/config.py @@ -442,7 +442,7 @@ def _resolve_one( container_label=container_label if isinstance(container_label, str) else None, memory_card_sync=card_sync, clears_stale_saves=clears_stale_saves, - broker_secret=STREAMING_BROKER_SECRET or str(entry.get("broker_secret", "")), + broker_secret=str(entry.get("broker_secret") or STREAMING_BROKER_SECRET), library_path=str(entry.get("library_path") or LIBRARY_BASE_PATH).rstrip("/"), capabilities=capabilities, state_transfer=state_transfer_limits(emulator), diff --git a/backend/tests/endpoints/test_streaming.py b/backend/tests/endpoints/test_streaming.py index 654c86702a..08eefea7f0 100644 --- a/backend/tests/endpoints/test_streaming.py +++ b/backend/tests/endpoints/test_streaming.py @@ -342,6 +342,24 @@ def test_get_config_warns_on_missing_platform(client, access_token, caplog): assert "missing platform/host" in caplog.text +def test_a_container_broker_secret_overrides_the_env_secret(): + """Containers with different BROKER_SECRETs each authenticate with their own.""" + with patch("handler.streaming.config.STREAMING_BROKER_SECRET", "from-env"): + container = resolve_entry({**_webstation(), "broker_secret": "from-config"}) + assert container is not None + assert container.broker_secret == "from-config" + + +@pytest.mark.parametrize( + "configured", [{}, {"broker_secret": None}, {"broker_secret": ""}] +) +def test_a_container_without_a_broker_secret_falls_back_to_the_env_secret(configured): + with patch("handler.streaming.config.STREAMING_BROKER_SECRET", "from-env"): + container = resolve_entry({**_webstation(), **configured}) + assert container is not None + assert container.broker_secret == "from-env" + + def test_a_config_warning_does_not_print_the_broker_secret( client, access_token, caplog ): diff --git a/env.template b/env.template index 6f0d846066..eb22fffde6 100644 --- a/env.template +++ b/env.template @@ -204,7 +204,7 @@ AUTHENTIK_SECRET_KEY= # Authentik secret key AUTHENTIK_BOOTSTRAP_PASSWORD= # Initial Authentik admin bootstrap password # Emulator Streaming -STREAMING_BROKER_SECRET= # Shared secret matching streaming containers' BROKER_SECRET, required for broker auth +STREAMING_BROKER_SECRET= # BROKER_SECRET for streaming containers that set no broker_secret in config.yml STREAMING_SAVE_TIMEOUT=45 # Seconds to wait for a broker save-and-exit (raise if a broker has SAVE_WAIT > 45) STREAMING_LAUNCH_TIMEOUT=600 # Seconds a webstation activate may take, covering pkg/archive extraction before the emulator starts STREAMING_STATE_HISTORY_LIMIT=50 # Save states kept per ROM, emulator and user; oldest are pruned past this (0 disables) diff --git a/examples/config.example.yml b/examples/config.example.yml index 812d9f955e..a9e9c77209 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -228,7 +228,7 @@ # host: https://192.168.1.56:3010 # browser-facing UI, must be https # subfolder: /streaming # matches the container's SUBFOLDER # # library_path: /mnt/games/library # only if the container can't mount the library at /romm/library -# broker_secret: change-me # matches the container's BROKER_SECRET +# broker_secret: change-me # the container's BROKER_SECRET; overrides STREAMING_BROKER_SECRET # label: Emulation station # broker_host: http://192.168.1.56:3010 # server-to-broker URL, when RomM can't dial `host` itself # platforms: From 5d706d5de88c52ee245f5ceef9818e7d405b56bd Mon Sep 17 00:00:00 2001 From: Zach Clendenen Date: Fri, 16 Oct 2026 14:08:22 -0500 Subject: [PATCH 2/2] test(streaming): cover a blank broker_secret with no env secret Addresses Greptile review on #5259: the fallback cases all set the env secret, so none pinned that YAML null no longer becomes the secret "None". --- backend/tests/endpoints/test_streaming.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/tests/endpoints/test_streaming.py b/backend/tests/endpoints/test_streaming.py index 08eefea7f0..0b37acc449 100644 --- a/backend/tests/endpoints/test_streaming.py +++ b/backend/tests/endpoints/test_streaming.py @@ -360,6 +360,14 @@ def test_a_container_without_a_broker_secret_falls_back_to_the_env_secret(config assert container.broker_secret == "from-env" +def test_a_blank_broker_secret_with_no_env_secret_sends_none(): + """A bare `broker_secret:` key is YAML null, not the secret "None".""" + with patch("handler.streaming.config.STREAMING_BROKER_SECRET", ""): + container = resolve_entry({**_webstation(), "broker_secret": None}) + assert container is not None + assert container.broker_secret == "" + + def test_a_config_warning_does_not_print_the_broker_secret( client, access_token, caplog ):