Skip to content

security(srt-listen): enforce SRT passphrase on the public guest listener - #40

Draft
guycochran wants to merge 1 commit into
masterfrom
feat/srt-listen-passphrase
Draft

guycochran wants to merge 1 commit into
masterfrom
feat/srt-listen-passphrase

Conversation

@guycochran

Copy link
Copy Markdown
Owner

Why

In srt-listen mode (the quickstart default), mediamtx's SRT server is disabled, so MXL_GUEST_SRT_PASSPHRASE / MXL_GUEST1_SRT_PASSPHRASE / MXL_GUEST2_SRT_PASSPHRASE only applied to srt-direct. The public guest ports (8890/8891) accepted any caller, so anyone who could reach the port could publish into program. This was the top item in the recent security review.

What changes

  • tools/guest_av_listen.sh (A/V fan-out, owns the public port): when MXL_GUEST_SRT_PASSPHRASE is set, it adds passphrase= to the public srtsrc listener as its own argv word, never spliced into the URI. The value is checked before anything binds, and a bad value exits 2, so the listener never falls back to open. When the variable is unset, the script prints a loud OPEN warning. The two 127.0.0.1 legs stay unencrypted (loopback only).
  • tools/adapters.py + tools/guest_ingest.py: SrtListenerGuestAdapter takes an optional passphrase. Without one, the launch string is byte-identical, so the golden parity tests are unchanged. guest_ingest.py reads MXL_GUEST_SRT_PASSPHRASE in srt-listen mode. It exits on an invalid value and warns when a non-loopback listener is open. The secret never goes into flow metadata or logs.
  • scripts/quickstart.sh: in listen mode, each slot's passphrase is checked (per-slot value wins over the shared one). Each slot's passphrase reaches its fan-out container through env inheritance (-e NAME with no value), so it stays out of the docker command line. Startup and the final banner list which slots require a passphrase and warn loudly about any open ones, including a partly open setup. The old "passphrase is ignored in srt-listen mode" message is gone.
  • Allowed characters: 10–79 chars of A-Z a-z 0-9 . _ ~ - (URL-unreserved). Wider sets broke in practice: ffmpeg URL-decodes %, + and others in srt://…&passphrase=, so the caller ends up with a different key than the one configured. openssl rand -hex 16 is the suggested generator.
  • docs/TESTERS.md: new "Lock the guest slots" section.

Tests

  • tests/test_srt_listen_passphrase.py (41 cases): adapter fragment with and without a passphrase, back half unchanged, invalid values rejected. guest_ingest.py applies the passphrase, warns when unset, and fails closed. The fan-out runs against a stub gst-launch-1.0: argv placement is checked, the loopback legs stay plain, invalid values exit before anything listens, and output with no passphrase is unchanged. Quickstart listen-mode validation and reporting are covered, plus static checks that the fan-out gets the secret only through env and the legs never get it.
  • Full suite passes locally.
  • Manual end-to-end test (GStreamer 1.26 / libsrt) against the real guest_av_listen.sh:
    • Correct passphrase → both loopback legs receive the TS.
    • Unencrypted caller → rejected (UNSECURE).
    • Wrong key → rejected (BADSECRET).
    • No passphrase configured → still works as before (open).

Deploy / compatibility

  • Nothing changes unless a passphrase variable is set. With nothing set, the launch strings and docker args are the same as today, apart from the added warnings.
  • If a passphrase variable is already set on a listen-mode host, it now takes effect: callers must supply it. An invalid value (spaces, quotes, +, %…) now stops the quickstart instead of being silently ignored.
  • Running hosts only pick this up after quickstart.sh is re-run (the guest image tag is content-hashed, so it rebuilds).

Known limits / follow-ups

  • The "Add your camera" QR / ingest info does not carry the passphrase; it has to be typed in (Larix "Passphrase" field, or &passphrase= in OBS/ffmpeg).
  • The passphrase shows up in the fan-out process's argv (ps) and in docker inspect env on the host. That is fine for the single-tenant VM model; worth noting in SECURITY.md.
  • guest_audio.py in listen mode only ever binds loopback behind the fan-out, so it was left alone.
  • After this and the adopter quick-fix PR land, the listen-mode wording in SECURITY.md, docs/QUICKSTART.md and docs/CONFIG.md needs a small update. Those files were left out of this PR to avoid conflicts with the open PRs that touch them.
  • If the Apache-2.0 PR lands first, the new test file should get an SPDX header.

Conflict risk

The tools/adapters.py change is small and additive (SrtListenerGuestAdapter + one helper near the top). git merge-tree against the open NMOS/timing PR, the adopter quick-fix PR and the Apache-2.0 PR is clean for all three. contribution_core.py is not touched.

Draft: needs a maintainer decision on merge order. Not tested on the live deployment.

…guest listener

In srt-listen mode (the quickstart default) mediamtx's SRT server is off, so the
existing MXL_GUEST*_SRT_PASSPHRASE settings were ignored and the public guest
ports accepted any caller.

- tools/guest_av_listen.sh: the A/V fan-out (owns the public port) passes
  passphrase= to its srtsrc listener when MXL_GUEST_SRT_PASSPHRASE is set;
  validates first and exits 2 on a bad value; loud warning when unset.
  Loopback legs stay unencrypted.
- tools/adapters.py / guest_ingest.py: SrtListenerGuestAdapter takes an optional
  passphrase (launch string byte-identical when unset); guest_ingest reads
  MXL_GUEST_SRT_PASSPHRASE in srt-listen mode and fails closed on invalid values.
- scripts/quickstart.sh: validates per-slot/shared passphrases in listen mode,
  hands them to each fan-out via env inheritance (not docker argv), reports
  which slots require one and warns loudly about open slots.
- Passphrase charset limited to URL-unreserved A-Za-z0-9._~- (10-79 chars) so it
  survives both gst-launch argv and a caller's srt:// URL unescaped.
- docs/TESTERS.md: how to lock guest slots. tests/test_srt_listen_passphrase.py.

No behaviour change unless a passphrase variable is set.

Signed-off-by: Guy Cochran <guycochran@gmail.com>
@guycochran

Copy link
Copy Markdown
Owner Author

Decision (Guy): /join contributor policy = passphrase (this PR). Public contributors must present the SRT passphrase; the /join QR embeds it so legit scanners stay one-tap. Approval-first lobby is a possible later addition, not now. This PR is #2 in the merge order (#38→**#40**→#37→#39→#41). Open sub-question in docs/REVIEW-RESPONSE.md: should the passphrase also gate multiview visibility, or is passphrase-to-publish enough?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant