Stream Authentication
Overview
Section titled “Overview”Monibuca v6 supports three complementary stream auth / admission capabilities:
- Built-in signature auth (
secret+expire, MD5) - Custom auth handler (takes priority over MD5 when
enable_authis on) - Signed Policy (HMAC-SHA256 URL/query gate, independent of
enable_auth)
There is also a synchronous admission webhook (HTTP admit), invoked by protocol hooks after verify_stream_auth. Its semantics are unchanged.
Verification order
Section titled “Verification order”Every publish/play request passes these gates in order; any failure denies the request:
- MD5 / custom handler (only when
enable_auth: trueand a key is configured) - Signed Policy (when
signed_policyis enabled and action/protocol match) - Admission webhook (when
admission_webhookis enabled and action/protocol match; may rewritestream_path)
The three gates are independent and freely combinable; with all disabled, the default zero-overhead behavior is unchanged.
Protocol coverage
Section titled “Protocol coverage”Protocol (protocol value) | Publish | Play | Notes |
|---|---|---|---|
rtmp | ✅ | ✅ | Deny closes the connection |
webrtc | ✅ WHIP | ✅ WHEP | Deny returns HTTP 401 |
srt | ✅ | ✅ | Handshake rejected as Unauthorized |
rtsp | ✅ record/announce | ✅ describe/play | RTSP 401 |
webtransport | ✅ | ✅ | Session close |
hls | — | ✅ | Admission on playlist/master only; .ts/segments are auth-only |
flv | — | ✅ | HTTP-FLV and WS-FLV |
mp4 | — | ✅ | HTTP MP4 playback |
The protocol values above are also what you put in Signed Policy protocols, admission webhook protocols, and what arrives in the admission request’s protocol field.
Enable Built-in Auth
Section titled “Enable Built-in Auth”Config Example
Section titled “Config Example”global: enableauth: true publish: key: "your-publish-key" secretargname: secret expireargname: expire subscribe: key: "your-subscribe-key"Notes:
- Set
enableauth=trueto enable stream auth checks - Publish uses
publish.key - Subscribe uses
subscribe.key - Parameter names are configurable via
secretargname/expireargname(defaults:secret/expire)
Signature Algorithm
Section titled “Signature Algorithm”secret = md5(key + streamPath + expireHex)Where:
key:publish.keyorsubscribe.keystreamPath: stream path without query, e.g.live/testexpireHex: Unix timestamp in hex (seconds)
Validation rules:
expiremust be a valid hex timestamp and not expiredsecretlength must be 32secretmust match server-side hash (case-insensitive)
URL Examples
Section titled “URL Examples”For live/test:
rtmp://host/live/test?secret=...&expire=...http://host:8180/flv/live/test.flv?secret=...&expire=...http://host:8180/hls/live/test/index.m3u8?secret=...&expire=...http://host:8180/webrtc/push/live/test?secret=...&expire=...http://host:8180/webrtc/play/live/test?secret=...&expire=...srt://host:6000?streamid=publish:/live/test?secret=...&expire=...Admission webhook (admission_webhook)
Section titled “Admission webhook (admission_webhook)”In addition to local secret+expire and custom handlers, you can enable a synchronous HTTP admission webhook (OME AdmissionWebhooks style). Monibuca POSTs to your control server before creating a publish/play session to allow, deny, or rewrite stream_path.
See the protocol coverage table above for wired protocols.
Config
Section titled “Config”global: admission_webhook: enable: false url: "https://ctrl.example/v1/admission" secret: "shared-secret" # optional; sends X-Monibuca-Signature: sha256=<hex> timeout_ms: 3000 # hard per-request deadline (ms); no retries fail_policy: closed # closed | open (opening only) notify_closing: false # best-effort POST status=closing on session end actions: [publish, play] # omit = both protocols: [webrtc, rtmp] # omit = all wired protocolsPath rewrite from admission happens after Signed Policy; for this MVP the policy binds the original requested stream_path.
Request body
Section titled “Request body”Monibuca POSTs JSON to url with headers X-Monibuca-Event: admission.opening, X-Monibuca-Delivery (idempotency ID), and optional X-Monibuca-Signature: sha256=<hex> (HMAC-SHA256 over the body):
{ "action": "publish", "protocol": "rtmp", "status": "opening", "stream_path": "live/cam01", "url": "rtmp://host/live/cam01?token=abc", "client": { "ip": "203.0.113.7", "port": 51234, "user_agent": "OBS" }, "query": { "token": "abc" }, "session_id": null, "time": "2026-07-17T12:00:00Z"}Response body
Section titled “Response body”The control server replies 2xx + JSON:
{ "allowed": true, "stream_path": "live/real-cam01", "reason": "ok"}| Field | Required | Notes |
|---|---|---|
allowed | yes | Missing or false both deny |
stream_path | no | Rewritten stream path (path only, no host/protocol) |
lifetime_ms | no | Recorded but not enforced in the MVP (segment GETs for HLS/FLV/MP4 are not gated) |
reason | no | Log it server-side; clients only see generic errors such as 401 |
fail_policy
Section titled “fail_policy”closed (default) denies on timeout/network/non-2xx/invalid JSON; open allows on those failures with a warn log (dev/break-glass only).
notify_closing
Section titled “notify_closing”When enable and notify_closing: true, session-oriented protocols fire-and-forget a POST on session end (status: "closing", header X-Monibuca-Event: admission.closing). The response body is ignored; failures only warn — they never block teardown and do not apply fail_policy.
| Fires closing | Does not fire closing |
|---|---|
| WebRTC WHIP/WHEP, RTMP, SRT | HLS / HTTP-FLV / MP4 (no persistent session) |
| RTSP / WebTransport (not wired in this slice) |
Current limitations
Section titled “Current limitations”- HLS admission runs only on playlist/master requests;
.ts/ fMP4 segment GETs are auth-only and are not re-admitted individually. - When
enable: falseor unset, admission is a zero-overhead no-op.
Signed Policy (HMAC URL gate)
Section titled “Signed Policy (HMAC URL gate)”OME SignedPolicy-style local deterministic auth: HMAC-SHA256 over protocol + action + stream_path + expire, with the token in the query string. Complements enable_auth and admission webhooks; disabled by default.
Config
Section titled “Config”global: signed_policy: enable: false secret: "your-hmac-secret" token_query: "policy" # token param name expire_query: "expire" # expiry param name (decimal Unix seconds) algorithm: "hmac-sha256" # MVP supports only this actions: ["publish", "play"] protocols: [] # empty = all protocols; e.g. ["rtmp", "webrtc"]Notes:
enable=falseor emptysecret: zero-cost no-op- Unmatched
actions/protocols: skip (no token required) - Signed Policy
expireis decimal Unix seconds; MD5 authexpireis hex. If both are enabled and share a query name, rename one viaexpire_query/expireargname
Canonical string and signing
Section titled “Canonical string and signing”canonical = "v1\n" + protocol + "\n" + action + "\n" + stream_path + "\n" + expirepolicy = hex(HMAC-SHA256(secret, canonical))Where:
protocol: plugin name (e.g.rtmp,webrtc,hls,flv,srt)action:publishorplaystream_path: path without query (e.g.live/test)expire: decimal Unix timestamp (seconds)
Generation example (Python)
Section titled “Generation example (Python)”import hmac, hashlib, time
secret = b"your-hmac-secret"protocol = "rtmp"action = "play"stream_path = "live/test"expire = str(int(time.time()) + 3600)canonical = f"v1\n{protocol}\n{action}\n{stream_path}\n{expire}"policy = hmac.new(secret, canonical.encode(), hashlib.sha256).hexdigest()print(f"?policy={policy}&expire={expire}")Generation example (Shell / openssl)
Section titled “Generation example (Shell / openssl)”SECRET="your-hmac-secret"PROTOCOL="rtmp"ACTION="play"STREAM_PATH="live/test"EXPIRE=$(( $(date +%s) + 3600 ))
POLICY=$(printf 'v1\n%s\n%s\n%s\n%s' "$PROTOCOL" "$ACTION" "$STREAM_PATH" "$EXPIRE" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
echo "?policy=${POLICY}&expire=${EXPIRE}"URL examples
Section titled “URL examples”rtmp://host/live/test?policy=...&expire=1735689600http://host:8180/hls/live/test/index.m3u8?policy=...&expire=1735689600Failures return the same auth errors as existing stream auth (missing / expired / bad signature all deny).
Custom Auth Handler
Section titled “Custom Auth Handler”Use a custom handler when integrating with external IAM, ACL services, or one-time tickets.
Priority order (within the enable_auth path):
- Custom handler (if registered)
- Built-in
secret+expireauth
After the custom handler returns successfully, Signed Policy must still pass when it is enabled and matches the action/protocol.
Registration
Section titled “Registration”StreamManagerApi provides:
set_stream_auth_handler(handler)
Handler input:
StreamAuthRequestplugin_namestream_pathquery_stringparams(parsed query map)is_publish
Handler output:
Ok(()): allowErr(...): deny
Pseudocode
Section titled “Pseudocode”manager.set_stream_auth_handler(Some(Arc::new(|req| { if req.plugin_name == "rtmp" && req.is_publish { let token = req.params.get("token").cloned().unwrap_or_default(); if token == "allow" { return Ok(()); } return Err(sdk::MonibucaError::InvalidInput("auth failed".into())); } Ok(())})));Admin / Global Config Page
Section titled “Admin / Global Config Page”Both signed_policy and admission_webhook are registered on the global config page (the global form under /config, i.e. the Admin config entry) in the “Signed Policy” and “Admission Webhook” groups. The fields map 1:1 to the YAML above; secrets render as password inputs.
Notes:
- Saving from the config page requires the database to be enabled (DB-backed config persistence); without it, edit
config.yamldirectly. - Both features require their companion field to be non-empty to take effect (
secretfor Signed Policy,urlfor admission); toggling onlyenabledoes nothing.
Admin Push URL Generator
Section titled “Admin Push URL Generator”The Admin “Push URL” dialog can generate auth params automatically:
expire(hex timestamp)secret(md5(key + streamPath + expire))
Then appends them to the generated URL query string.
Secret Generation API
Section titled “Secret Generation API”The server exposes a signing helper endpoint:
GET /v6/api/streams/secret?stream_path={streamPath}&type={publish|subscribe}&expire=<hex>&plugin=<pluginName>Parameters:
stream_path: stream pathtype:publishorsubscribe, defaultpublishexpire: optional hex Unix timestamp; defaults to now + 30 minutesplugin: optional plugin name (defaultglobal)
Example response:
{ "secret_type": "publish", "plugin": "rtmp", "stream_path": "live/test", "expire": "6610f4a0", "secret": "0123456789abcdef0123456789abcdef"}