Skip to content

Stream Authentication

Monibuca v6 supports three complementary stream auth / admission capabilities:

  1. Built-in signature auth (secret + expire, MD5)
  2. Custom auth handler (takes priority over MD5 when enable_auth is on)
  3. 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.

Every publish/play request passes these gates in order; any failure denies the request:

  1. MD5 / custom handler (only when enable_auth: true and a key is configured)
  2. Signed Policy (when signed_policy is enabled and action/protocol match)
  3. Admission webhook (when admission_webhook is enabled and action/protocol match; may rewrite stream_path)

The three gates are independent and freely combinable; with all disabled, the default zero-overhead behavior is unchanged.

Protocol (protocol value)PublishPlayNotes
rtmpDeny closes the connection
webrtc✅ WHIP✅ WHEPDeny returns HTTP 401
srtHandshake rejected as Unauthorized
rtsp✅ record/announce✅ describe/playRTSP 401
webtransportSession close
hlsAdmission on playlist/master only; .ts/segments are auth-only
flvHTTP-FLV and WS-FLV
mp4HTTP 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.

global:
enableauth: true
publish:
key: "your-publish-key"
secretargname: secret
expireargname: expire
subscribe:
key: "your-subscribe-key"

Notes:

  • Set enableauth=true to enable stream auth checks
  • Publish uses publish.key
  • Subscribe uses subscribe.key
  • Parameter names are configurable via secretargname / expireargname (defaults: secret / expire)
secret = md5(key + streamPath + expireHex)

Where:

  • key: publish.key or subscribe.key
  • streamPath: stream path without query, e.g. live/test
  • expireHex: Unix timestamp in hex (seconds)

Validation rules:

  • expire must be a valid hex timestamp and not expired
  • secret length must be 32
  • secret must match server-side hash (case-insensitive)

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=...

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.

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 protocols

Path rewrite from admission happens after Signed Policy; for this MVP the policy binds the original requested stream_path.

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"
}

The control server replies 2xx + JSON:

{
"allowed": true,
"stream_path": "live/real-cam01",
"reason": "ok"
}
FieldRequiredNotes
allowedyesMissing or false both deny
stream_pathnoRewritten stream path (path only, no host/protocol)
lifetime_msnoRecorded but not enforced in the MVP (segment GETs for HLS/FLV/MP4 are not gated)
reasonnoLog it server-side; clients only see generic errors such as 401

closed (default) denies on timeout/network/non-2xx/invalid JSON; open allows on those failures with a warn log (dev/break-glass only).

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 closingDoes not fire closing
WebRTC WHIP/WHEP, RTMP, SRTHLS / HTTP-FLV / MP4 (no persistent session)
RTSP / WebTransport (not wired in this slice)
  • HLS admission runs only on playlist/master requests; .ts / fMP4 segment GETs are auth-only and are not re-admitted individually.
  • When enable: false or unset, admission is a zero-overhead no-op.

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.

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=false or empty secret: zero-cost no-op
  • Unmatched actions / protocols: skip (no token required)
  • Signed Policy expire is decimal Unix seconds; MD5 auth expire is hex. If both are enabled and share a query name, rename one via expire_query / expireargname
canonical = "v1\n" + protocol + "\n" + action + "\n" + stream_path + "\n" + expire
policy = hex(HMAC-SHA256(secret, canonical))

Where:

  • protocol: plugin name (e.g. rtmp, webrtc, hls, flv, srt)
  • action: publish or play
  • stream_path: path without query (e.g. live/test)
  • expire: decimal Unix timestamp (seconds)
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}")
Terminal window
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}"
rtmp://host/live/test?policy=...&expire=1735689600
http://host:8180/hls/live/test/index.m3u8?policy=...&expire=1735689600

Failures return the same auth errors as existing stream auth (missing / expired / bad signature all deny).

Use a custom handler when integrating with external IAM, ACL services, or one-time tickets.

Priority order (within the enable_auth path):

  1. Custom handler (if registered)
  2. Built-in secret+expire auth

After the custom handler returns successfully, Signed Policy must still pass when it is enabled and matches the action/protocol.

StreamManagerApi provides:

  • set_stream_auth_handler(handler)

Handler input:

  • StreamAuthRequest
    • plugin_name
    • stream_path
    • query_string
    • params (parsed query map)
    • is_publish

Handler output:

  • Ok(()): allow
  • Err(...): deny
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(())
})));

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.yaml directly.
  • Both features require their companion field to be non-empty to take effect (secret for Signed Policy, url for admission); toggling only enable does nothing.

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.

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 path
  • type: publish or subscribe, default publish
  • expire: optional hex Unix timestamp; defaults to now + 30 minutes
  • plugin: optional plugin name (default global)

Example response:

{
"secret_type": "publish",
"plugin": "rtmp",
"stream_path": "live/test",
"expire": "6610f4a0",
"secret": "0123456789abcdef0123456789abcdef"
}