repo/services/multistreaming/SECURITY.md
Ezequiel C. 187379de4e multistreaming: scenes/composition, installer build support, service updates
- multistreaming (new): RTMP ingest + multi-platform fan-out with pluggable providers (Twitch/YouTube/Kick/custom), zero-knowledge key vaults, Authelia OIDC auth, shared rooms with editor/streamer roles, single-use invites, per-account streaming grants, and scenes & composition (grid/PiP layouts, text/image overlays, per-output audio routing).
- installer: support Dockerfile build in metadata (not just image) and RSA key generation for the Authelia OIDC JWKS.
- authelia: add OIDC provider with portainer + multistreaming clients (public + PKCE).
- services: remove allprox; add nginx-proxy-manager and portainer; update lldap; regenerate catalog.
2026-09-02 21:18:25 +02:00

9.3 KiB
Raw Blame History

SECURITY.md — multistreaming

This document describes the security model of the self-hosted multistreaming service, its threat model, and — explicitly and honestly — the limits of what it can protect. Read it before exposing the service to anything but a trusted network.


1. What this service does

multistreaming ingests RTMP feeds (typically one per person from OBS) and restreams them to one or more streaming platforms (Twitch, YouTube, Kick, or a custom RTMP endpoint). Two programming modes exist:

  • Passthrough — each live feed is remuxed (-c copy) to its destination accounts without re-encoding.
  • Scenes & composition — multiple live feeds are composed (grid or picture-in-picture, plus text/image overlays) and re-encoded into a single program pushed to destination accounts, with per-output audio routing.

To push to a platform, the service must present that platform's stream key. Stream keys are the highest-value secret in the system.


2. Threat model

Assets

Asset Sensitivity
Platform stream keys (Twitch/YouTube/Kick/custom) Highest — compromise = account takeover of that channel
Vault master passphrase / vault key (VK) Highest — decrypts all keys in a vault
Account metadata, room topology, scene definitions Lowmoderate
Composed/relayed video As sensitive as the stream content itself

Actors

Actor Assumption
Homelab operator / host root Fully trusted. Can read any memory/disk on the machine.
Service account (the container) Trusted with plaintext keys at push time only.
Vault owner Holds the master passphrase; controls their own accounts.
Room owner Full control of a room's production; sees own keys only.
Room editor Edits feeds/outputs/scenes; never sees anyone's keys.
Room streamer Manages their own accounts + grants their own keys; read-only on production.
Streaming platform External; receives the pushed stream.
Network observer (LAN/internet) Assumed able to observe traffic unless TLS/VPN is used.

Trust zones

  1. Browser — holds the vault key (VK) only while a vault is unlocked, in memory; encrypts/decrypts keys client-side.
  2. Panel server (the container, the "center machine") — persists only ciphertext; holds plaintext keys transiently in memory during a stream.
  3. Ingest path — RTMP from OBS to the container on port 1935.

3. Zero-knowledge vault

The server never stores a plaintext stream key. Account secrets are stored as secretCiphertext ({iv, data}, AES-GCM) encrypted under a random 256-bit vault key (VK) that only the user's browser holds after unlock.

Key hierarchy (src/vault.js):

master passphrase ──PBKDF2-HMAC-SHA256 (600k)──▶ KEK ──wraps──▶ VK ──encrypts──▶ account secrets
  • salt — random 16 bytes, stored server-side.
  • serverWrapped — the VK wrapped by the KEK under AAD multistreaming:vault:v1. Stored server-side; useless without the master passphrase.
  • deviceWrapped — the VK wrapped by a non-extractable browser device key under AAD fingerprint|sessionId. Enables silent unlock and is session-bound: a different session id or browser fingerprint fails AES-GCM authentication.
  • Each account secret is encrypted under the VK with AAD acct:<accountId>, so a ciphertext cannot be replayed against a different account id.

Consequences:

  • A database/disk leak of config.json yields only PBKDF2-wrapped blobs and per-account ciphertexts — nothing decryptable without the passphrase or the device key.
  • There is no password recovery. Losing the passphrase and the device wrapping means the keys are unrecoverable (by design).

4. Capability grants (keys in memory)

The zero-knowledge property cannot extend all the way to the push: some machine must present the plaintext key to the platform at push time. That machine is the multistreaming container.

To minimize exposure, the service uses short-lived, in-memory capability grants (src/grants.js):

  • A streamer clicks Start streaming; their browser decrypts the selected keys client-side and sends them to the server over the panel connection.
  • The server stores them only in memory: accountId → { key, grantedBy, expiresAt }, never on disk.
  • Lifetime is GRANT_TTL_MS (default 6 hours). Expired grants are dropped on access.
  • Grants are revoked when the streamer clicks Stop streaming, when the account is deleted, or on expiry.
  • FFmpeg resolves the destination URL from the live grant at spawn time; the URL (which contains the key) is redacted in all logs (redactUrl).

5. Authentication & authorization

  • Sessions (src/auth.js): HMAC-SHA256-signed tokens (base64url({sid, uid, iat}).signature) in an HttpOnly; SameSite=Lax cookie, 12-hour lifetime. SESSION_SECRET should be set to a stable random value; if unset it is generated per boot (sessions do not survive restarts).
  • Local auth (dev only): scrypt password hashing, constant-time comparison.
  • OIDC (recommended, Authelia): authorization-code + PKCE (S256), with the verifier held server-side (10-minute state). Identity is mapped to a local user record by sub/username. OIDC is for authentication only — it never touches vault key material.

Per-room roles

Capability owner editor streamer
Manage feeds, outputs, scenes, scene outputs, images
Activate/deactivate scenes
View room production
Manage own accounts + grant own keys
See another user's secretCiphertext

Non-members receive 404 (never 403) on room-scoped reads, and insufficient roles receive 403. maskAccount strips secretCiphertext from every account unless the viewer is its owner, so editors and streamers never see others' keys.


6. Single-use invites

POST /api/rooms/:roomId/invites (owner only) mints an invite with a 256-bit random token, a role of editor or streamer (never owner), and a 7-day expiry. Accepting an invite consumes it (deletes the record) and creates the membership. Invites are:

  • single-use (consumed on accept),
  • expiring (pruned at load and on lookup),
  • role-limited (cannot mint an owner),
  • unguessable (256-bit token).

7. The inherent trust boundary (be explicit)

If you compromise the host (or the multistreaming container while a stream is running), you can read every key that is currently granted. That is not a bug; it is the fundamental limit of any restreaming service — the machine that pushes must hold the key at push time.

The mitigations are scope and time, not prevention:

  1. Keys are held only in memory, only for granted accounts, and only for GRANT_TTL_MS.
  2. Revocation is one click ("Stop streaming") or automatic on expiry.
  3. Keys are never written to disk, never logged, and never sent to editors, streamers, or other room members.

If the host is untrusted, do not run this service on it.


8. Scene & composition security

  • FFmpeg argument injection is prevented by: spawning via an argument array (no shell) and escaping overlay text for FFmpeg's filtergraph parser (escapeDrawtext: control chars stripped, \ and ' escaped) plus drawtext=...:expansion=none, a 256-character cap, and NUL stripping.
  • Image overlays accept only decoded payloads ≤ 1 MiB whose magic bytes are PNG/JPEG/WebP; files are stored under dataDir/uploads with server-generated UUID filenames (never client-supplied paths), and the path is single-quote escaped before reaching the movie filter.
  • Log redaction applies to composed outputs exactly as to passthrough: destination URLs (which embed keys) are replaced before logging.

9. Residual risks & known limits

  1. Center-machine memory — see §7. Root/host access during a stream reads live keys.
  2. No CSP / XSS hardening — the panel does not currently ship a strict Content-Security-Policy. A browser XSS could read a decrypted key while the vault is unlocked. Run the panel behind a TLS reverse proxy and keep dependencies updated.
  3. Plaintext RTMP ingest — the ingest path (port 1935) is unencrypted and bypasses the HTTP proxy by design. Keep it on a trusted LAN/VPN; do not expose 1935 to the internet.
  4. Panel TLS — the container serves HTTP on 8080; TLS termination is expected at a reverse proxy (nginx-proxy-manager).
  5. Local auth modeAUTH_MODE=local is intended for development; OIDC is the recommended production mode.
  6. No audit log — grant/activate/invite events are not persisted for audit.
  7. Composition re-encode — scene mode decodes and re-encodes feeds on the server; this does not weaken key handling but is a CPU cost and a single point of processing.

10. Operational checklist

  • Set a stable, high-entropy SESSION_SECRET.
  • Use AUTH_MODE=oidc behind Authelia with PKCE and matching OIDC_REDIRECT_URI.
  • Terminate TLS at the reverse proxy; expose only the panel there, not 1935.
  • Keep RTMP ingest on a private network.
  • Leave GRANT_TTL_MS as short as operationally comfortable.
  • Back up dataDir (it holds the only copy of wrapped keys; there is no recovery without the passphrase).