From 55ed4f1c73d01d541a1601d28422a1713ae95a2f Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 13:26:05 +0000 Subject: [PATCH 1/8] feat(auth): allow non-admin user role to use HTTP sink/IO nodes The built-in user role excluded transport::http::* entirely and all streamkit::* nodes, forcing HTTP-transport gateways to run with an admin token. Allow the safe sink/IO nodes (transport::http::mse, streamkit::http_input, streamkit::http_output) while keeping transport::http::fetcher denied (SSRF risk), and document a least-privilege gateway role. Closes #632 Signed-off-by: streamkit-devin --- apps/skit/src/permissions.rs | 18 ++++++++++++ docs/src/content/docs/guides/authorization.md | 29 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/apps/skit/src/permissions.rs b/apps/skit/src/permissions.rs index 032250f9d..ab6438a35 100644 --- a/apps/skit/src/permissions.rs +++ b/apps/skit/src/permissions.rs @@ -162,6 +162,13 @@ impl Permissions { "containers::*".to_string(), // Transport: allow MoQ, deny HTTP fetcher by default (SSRF risk) "transport::moq::*".to_string(), + // Transport HTTP: allow the sink/IO nodes that serve or receive over the + // client's own request (no arbitrary-URL fetch), but keep + // `transport::http::fetcher` denied (SSRF risk). This lets least-privilege + // gateways serve live casts (mse) and run oneshots without admin. + "transport::http::mse".to_string(), + "streamkit::http_input".to_string(), + "streamkit::http_output".to_string(), // Core: explicitly allow safe-ish nodes; deny core::file_writer by default (arbitrary write risk) "core::passthrough".to_string(), "core::file_reader".to_string(), @@ -511,6 +518,17 @@ mod tests { assert!(user.is_node_allowed("plugin::wasm::gain_filter_rust")); } + #[test] + fn test_default_user_allows_http_sink_and_io_nodes() { + let user = Permissions::user(); + // Sink/IO HTTP nodes are safe (serve/receive over the client's own request). + assert!(user.is_node_allowed("transport::http::mse")); + assert!(user.is_node_allowed("streamkit::http_input")); + assert!(user.is_node_allowed("streamkit::http_output")); + // The HTTP fetcher remains denied (arbitrary-URL fetch / SSRF risk). + assert!(!user.is_node_allowed("transport::http::fetcher")); + } + #[test] fn test_global_session_limits() { let config = PermissionsConfig { max_concurrent_sessions: Some(10), ..Default::default() }; diff --git a/docs/src/content/docs/guides/authorization.md b/docs/src/content/docs/guides/authorization.md index cd6893325..34280e2ea 100644 --- a/docs/src/content/docs/guides/authorization.md +++ b/docs/src/content/docs/guides/authorization.md @@ -109,6 +109,35 @@ allowed_assets = ["*"] > [!NOTE] > Role permissions are deny-by-default. If you define a custom role in `skit.toml`, any permission you omit defaults to `false`. +> [!NOTE] +> The built-in `user` role allows the safe HTTP sink/IO nodes — `transport::http::mse` (live-cast playback), `streamkit::http_input` and `streamkit::http_output` (oneshot request body/response) — but **not** `transport::http::fetcher`, which can fetch arbitrary URLs (SSRF risk). A trusted gateway that only serves or receives over the caller's own request therefore does not need `admin`. + +## Example: Least-privilege gateway role + +Trusted intermediaries (e.g. the `web-capture` or `speech-gateway` examples) build a small set of fixed pipelines and should run with a scoped token instead of `admin`. This role can create/destroy sessions and use the HTTP-transport sink/IO nodes plus a specific plugin, but cannot load/delete plugins or touch other users' sessions: + +```toml +[permissions.roles.gateway] +create_sessions = true +destroy_sessions = true +modify_sessions = true +tune_nodes = true +list_sessions = true +list_nodes = true +access_all_sessions = false # Only its own sessions +load_plugins = false +delete_plugins = false +upload_assets = false +delete_assets = false +allowed_nodes = [ + "transport::http::mse", # serve live casts to the browser (MSE) + "streamkit::http_input", # oneshot request body + "streamkit::http_output", # oneshot response + "core::*", +] +allowed_plugins = ["plugin::native::servo"] # only what the gateway needs +``` + ## Permission reference | Permission | Description | From 00c19eb013173cb711e3271593f8d5ab8b5450ec Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 13:29:15 +0000 Subject: [PATCH 2/8] docs(auth): align sample skit.toml with safe HTTP node defaults Tighten the sample user role to deny transport::http::fetcher (was allowed via the transport::* wildcard) and add a least-privilege gateway role example. Signed-off-by: streamkit-devin --- samples/skit.toml | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/samples/skit.toml b/samples/skit.toml index 944e93b40..ded8a0d0c 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -483,10 +483,15 @@ allowed_samples = [ "user/*.yaml", ] -# Users can use most nodes except potentially dangerous ones +# Users can use most nodes except potentially dangerous ones. +# Note: transport::http::fetcher is intentionally excluded (it fetches arbitrary +# URLs - SSRF risk). The safe HTTP sink/IO nodes are allowed instead. allowed_nodes = [ "audio::*", - "transport::*", + "transport::moq::*", + "transport::http::mse", # serve live casts to the browser (MSE) + "streamkit::http_input", # oneshot request body + "streamkit::http_output", # oneshot response "core::*", "containers::*", ] @@ -500,6 +505,37 @@ allowed_assets = [ "samples/audio/user/*", ] +[permissions.roles.gateway] +# Least-privilege role for trusted intermediaries (e.g. the speech-gateway or +# web-capture examples) that build a small set of fixed pipelines. Avoids +# granting admin just to use HTTP-transport nodes. +create_sessions = true +destroy_sessions = true +list_sessions = true +modify_sessions = true +tune_nodes = true +load_plugins = false +delete_plugins = false +list_nodes = true +access_all_sessions = false # Only its own sessions +upload_assets = false +delete_assets = false + +allowed_samples = [] + +# Only the HTTP sink/IO nodes the gateway needs - no fetcher (SSRF risk). +allowed_nodes = [ + "transport::http::mse", # serve live casts to the browser (MSE) + "streamkit::http_input", # oneshot request body + "streamkit::http_output", # oneshot response + "core::*", +] + +# Only the specific plugin(s) the gateway needs (example: servo for web-capture). +allowed_plugins = ["plugin::native::servo"] + +allowed_assets = [] + [permissions.roles.readonly] # Read-only role - can only view, not modify create_sessions = false From b43d7929207206d235ac8080f79ee39bc7726164 Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 13:48:29 +0000 Subject: [PATCH 3/8] fix(auth): align sample user role with built-in secure default Mirror Permissions::user() in samples/skit.toml: drop blanket core::* (which exposed core::file_writer) for the safe core subset, add video::* and plugin usage, keeping transport::http::fetcher denied. Extend the sample config test to assert the user/gateway node allowlists. Signed-off-by: streamkit-devin --- apps/skit/tests/sample_config_test.rs | 19 +++++++++++++++++++ samples/skit.toml | 25 ++++++++++++++++++------- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/apps/skit/tests/sample_config_test.rs b/apps/skit/tests/sample_config_test.rs index 88e4902a7..4192bca2c 100644 --- a/apps/skit/tests/sample_config_test.rs +++ b/apps/skit/tests/sample_config_test.rs @@ -50,6 +50,7 @@ fn samples_skit_toml_parses_and_matches_expected_defaults() { assert!(config.permissions.roles.contains_key("admin")); assert!(config.permissions.roles.contains_key("demo")); assert!(config.permissions.roles.contains_key("user")); + assert!(config.permissions.roles.contains_key("gateway")); assert!(config.permissions.roles.contains_key("readonly")); let readonly = config.permissions.get_role("readonly"); @@ -58,5 +59,23 @@ fn samples_skit_toml_parses_and_matches_expected_defaults() { assert!(!readonly.upload_assets); assert!(!readonly.delete_assets); + // The user role allows the safe HTTP sink/IO nodes but not the SSRF-risk + // fetcher or the arbitrary-write file_writer (mirrors Permissions::user()). + let user = config.permissions.get_role("user"); + assert!(user.is_node_allowed("transport::http::mse")); + assert!(user.is_node_allowed("streamkit::http_input")); + assert!(user.is_node_allowed("streamkit::http_output")); + assert!(!user.is_node_allowed("transport::http::fetcher")); + assert!(!user.is_node_allowed("core::file_writer")); + + // The gateway role is least-privilege: HTTP sink/IO nodes and its plugin + // only, no fetcher, no plugin loading. + let gateway = config.permissions.get_role("gateway"); + assert!(gateway.create_sessions); + assert!(!gateway.load_plugins); + assert!(gateway.is_node_allowed("transport::http::mse")); + assert!(!gateway.is_node_allowed("transport::http::fetcher")); + assert!(gateway.is_plugin_allowed("plugin::native::servo")); + assert!(config.script.global_fetch_allowlist.is_empty()); } diff --git a/samples/skit.toml b/samples/skit.toml index ded8a0d0c..f32533f71 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -483,21 +483,32 @@ allowed_samples = [ "user/*.yaml", ] -# Users can use most nodes except potentially dangerous ones. -# Note: transport::http::fetcher is intentionally excluded (it fetches arbitrary -# URLs - SSRF risk). The safe HTTP sink/IO nodes are allowed instead. +# Users can use most nodes except potentially dangerous ones. Mirrors the +# built-in Permissions::user() default: transport::http::fetcher is excluded +# (arbitrary-URL fetch - SSRF risk) and core::file_writer is excluded +# (arbitrary-write risk), while the safe HTTP sink/IO nodes are allowed. allowed_nodes = [ "audio::*", + "video::*", + "containers::*", "transport::moq::*", "transport::http::mse", # serve live casts to the browser (MSE) "streamkit::http_input", # oneshot request body "streamkit::http_output", # oneshot response - "core::*", - "containers::*", + "core::passthrough", + "core::file_reader", + "core::pacer", + "core::json_serialize", + "core::text_chunker", + "core::script", + "core::telemetry_tap", + "core::telemetry_out", + "core::sink", + "plugin::*", ] -# Users cannot load plugins, so this list is empty -allowed_plugins = [] +# Users cannot load/delete plugins, but may use plugins an admin has already loaded. +allowed_plugins = ["plugin::*"] # Users can list bundled system assets and their uploaded assets allowed_assets = [ From 72ca68d2773012273e07cf1b274c1252f33c99e5 Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 13:51:55 +0000 Subject: [PATCH 4/8] fix(auth): tighten gateway role core nodes and align user samples Drop core::* from the gateway example (it exposed core::file_writer, contradicting the least-privilege intent) for the safe core plumbing subset, and remove demo/* from the sample user role's allowed_samples to match the built-in Permissions::user(). Signed-off-by: streamkit-devin --- docs/src/content/docs/guides/authorization.md | 6 +++++- samples/skit.toml | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/src/content/docs/guides/authorization.md b/docs/src/content/docs/guides/authorization.md index 34280e2ea..10087e740 100644 --- a/docs/src/content/docs/guides/authorization.md +++ b/docs/src/content/docs/guides/authorization.md @@ -133,7 +133,11 @@ allowed_nodes = [ "transport::http::mse", # serve live casts to the browser (MSE) "streamkit::http_input", # oneshot request body "streamkit::http_output", # oneshot response - "core::*", + # Safe core plumbing only - no core::file_writer (arbitrary-write risk). + "core::passthrough", + "core::file_reader", + "core::pacer", + "core::sink", ] allowed_plugins = ["plugin::native::servo"] # only what the gateway needs ``` diff --git a/samples/skit.toml b/samples/skit.toml index f32533f71..fa8b09a72 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -471,14 +471,12 @@ delete_plugins = false list_nodes = true access_all_sessions = false -# Users can access all samples except admin-only ones +# Users can access standard samples (mirrors the built-in Permissions::user()). allowed_samples = [ "oneshot/*.yml", "oneshot/*.yaml", "dynamic/*.yml", "dynamic/*.yaml", - "demo/*.yml", - "demo/*.yaml", "user/*.yml", "user/*.yaml", ] @@ -534,12 +532,16 @@ delete_assets = false allowed_samples = [] -# Only the HTTP sink/IO nodes the gateway needs - no fetcher (SSRF risk). +# Only the HTTP sink/IO nodes the gateway needs plus the safe core plumbing +# nodes - no fetcher (SSRF risk) and no core::file_writer (arbitrary-write risk). allowed_nodes = [ "transport::http::mse", # serve live casts to the browser (MSE) "streamkit::http_input", # oneshot request body "streamkit::http_output", # oneshot response - "core::*", + "core::passthrough", + "core::file_reader", + "core::pacer", + "core::sink", ] # Only the specific plugin(s) the gateway needs (example: servo for web-capture). From 6e5d357da96f28c7d1b843d4cf4794130f5ef384 Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 15:36:13 +0000 Subject: [PATCH 5/8] fix(auth): make gateway role usable and align user role with built-in The gateway example could never run its own pipeline: is_node_allowed() runs before the plugin check, so plugin::native::servo (absent from allowed_nodes) was rejected, and the role lacked the video/containers nodes the servo->encode->mux->serve pipeline needs. Grant exactly those kinds. Also align the sample user role with Permissions::user(): add the list/read/write/delete_samples flags (without which allowed_samples is unreachable) and core::param_bridge (a safe in-graph node). Drop the streamkit::http_input/http_output allowlist entries from both the built-in and sample roles - they are oneshot-only markers never gated by is_node_allowed, so listing them was dead config. Signed-off-by: streamkit-devin --- apps/skit/src/permissions.rs | 29 +++++++++------ apps/skit/tests/sample_config_test.rs | 22 +++++++---- docs/src/content/docs/guides/authorization.md | 22 ++++++----- samples/skit.toml | 37 ++++++++++++------- 4 files changed, 69 insertions(+), 41 deletions(-) diff --git a/apps/skit/src/permissions.rs b/apps/skit/src/permissions.rs index ab6438a35..40dfef449 100644 --- a/apps/skit/src/permissions.rs +++ b/apps/skit/src/permissions.rs @@ -162,17 +162,19 @@ impl Permissions { "containers::*".to_string(), // Transport: allow MoQ, deny HTTP fetcher by default (SSRF risk) "transport::moq::*".to_string(), - // Transport HTTP: allow the sink/IO nodes that serve or receive over the - // client's own request (no arbitrary-URL fetch), but keep - // `transport::http::fetcher` denied (SSRF risk). This lets least-privilege - // gateways serve live casts (mse) and run oneshots without admin. + // Transport HTTP: allow `mse` (serves over the caller's own request, no + // arbitrary-URL fetch) so least-privilege gateways can serve live casts + // without admin. `transport::http::fetcher` stays denied (SSRF risk). + // The oneshot `streamkit::http_input`/`http_output` markers are not gated by + // this allowlist (the oneshot path treats them as implicitly allowed), so + // they are intentionally not listed here. "transport::http::mse".to_string(), - "streamkit::http_input".to_string(), - "streamkit::http_output".to_string(), - // Core: explicitly allow safe-ish nodes; deny core::file_writer by default (arbitrary write risk) + // Core: explicitly allow the safe nodes. Omitted on purpose: + // `core::file_writer` / `core::object_store_writer` (arbitrary/external write). "core::passthrough".to_string(), "core::file_reader".to_string(), "core::pacer".to_string(), + "core::param_bridge".to_string(), "core::json_serialize".to_string(), "core::text_chunker".to_string(), "core::script".to_string(), @@ -519,14 +521,17 @@ mod tests { } #[test] - fn test_default_user_allows_http_sink_and_io_nodes() { + fn test_default_user_http_and_core_node_policy() { let user = Permissions::user(); - // Sink/IO HTTP nodes are safe (serve/receive over the client's own request). + // `mse` serves over the caller's own request and is safe to allow. assert!(user.is_node_allowed("transport::http::mse")); - assert!(user.is_node_allowed("streamkit::http_input")); - assert!(user.is_node_allowed("streamkit::http_output")); - // The HTTP fetcher remains denied (arbitrary-URL fetch / SSRF risk). + // Safe in-graph core nodes (no external side effects) are allowed. + assert!(user.is_node_allowed("core::param_bridge")); + // The HTTP fetcher stays denied (arbitrary-URL fetch / SSRF risk), and the + // write-capable core nodes stay denied (arbitrary / external write). assert!(!user.is_node_allowed("transport::http::fetcher")); + assert!(!user.is_node_allowed("core::file_writer")); + assert!(!user.is_node_allowed("core::object_store_writer")); } #[test] diff --git a/apps/skit/tests/sample_config_test.rs b/apps/skit/tests/sample_config_test.rs index 4192bca2c..3233de2a4 100644 --- a/apps/skit/tests/sample_config_test.rs +++ b/apps/skit/tests/sample_config_test.rs @@ -59,23 +59,31 @@ fn samples_skit_toml_parses_and_matches_expected_defaults() { assert!(!readonly.upload_assets); assert!(!readonly.delete_assets); - // The user role allows the safe HTTP sink/IO nodes but not the SSRF-risk - // fetcher or the arbitrary-write file_writer (mirrors Permissions::user()). + // The user role mirrors Permissions::user(): the allowed_samples list is + // reachable (sample flags set), the HTTP `mse` sink is allowed, but the + // SSRF-risk fetcher and the write-capable core nodes stay denied. let user = config.permissions.get_role("user"); + assert!(user.list_samples && user.read_samples && user.write_samples && user.delete_samples); assert!(user.is_node_allowed("transport::http::mse")); - assert!(user.is_node_allowed("streamkit::http_input")); - assert!(user.is_node_allowed("streamkit::http_output")); + assert!(user.is_node_allowed("core::param_bridge")); assert!(!user.is_node_allowed("transport::http::fetcher")); assert!(!user.is_node_allowed("core::file_writer")); + assert!(!user.is_node_allowed("core::object_store_writer")); - // The gateway role is least-privilege: HTTP sink/IO nodes and its plugin - // only, no fetcher, no plugin loading. + // The gateway role is least-privilege but must actually be able to build the + // servo -> encode -> mux -> serve pipeline. Crucially the plugin kind has to + // pass is_node_allowed() (checked before the plugin allowlist), and the + // fetcher / file_writer stay denied. let gateway = config.permissions.get_role("gateway"); assert!(gateway.create_sessions); assert!(!gateway.load_plugins); + assert!(gateway.is_node_allowed("plugin::native::servo")); + assert!(gateway.is_plugin_allowed("plugin::native::servo")); + assert!(gateway.is_node_allowed("video::vp9::encoder")); + assert!(gateway.is_node_allowed("containers::webm::muxer")); assert!(gateway.is_node_allowed("transport::http::mse")); assert!(!gateway.is_node_allowed("transport::http::fetcher")); - assert!(gateway.is_plugin_allowed("plugin::native::servo")); + assert!(!gateway.is_node_allowed("core::file_writer")); assert!(config.script.global_fetch_allowlist.is_empty()); } diff --git a/docs/src/content/docs/guides/authorization.md b/docs/src/content/docs/guides/authorization.md index 10087e740..90ad1daa2 100644 --- a/docs/src/content/docs/guides/authorization.md +++ b/docs/src/content/docs/guides/authorization.md @@ -110,11 +110,14 @@ allowed_assets = ["*"] > Role permissions are deny-by-default. If you define a custom role in `skit.toml`, any permission you omit defaults to `false`. > [!NOTE] -> The built-in `user` role allows the safe HTTP sink/IO nodes — `transport::http::mse` (live-cast playback), `streamkit::http_input` and `streamkit::http_output` (oneshot request body/response) — but **not** `transport::http::fetcher`, which can fetch arbitrary URLs (SSRF risk). A trusted gateway that only serves or receives over the caller's own request therefore does not need `admin`. +> The built-in `user` role allows `transport::http::mse` (live-cast playback over the caller's own request) but **not** `transport::http::fetcher`, which can fetch arbitrary URLs (SSRF risk). The oneshot `streamkit::http_input` / `streamkit::http_output` markers are always permitted on the oneshot path regardless of `allowed_nodes`, so they need no allowlist entry. A trusted gateway that only serves or receives over the caller's own request therefore does not need `admin`. ## Example: Least-privilege gateway role -Trusted intermediaries (e.g. the `web-capture` or `speech-gateway` examples) build a small set of fixed pipelines and should run with a scoped token instead of `admin`. This role can create/destroy sessions and use the HTTP-transport sink/IO nodes plus a specific plugin, but cannot load/delete plugins or touch other users' sessions: +Trusted intermediaries (e.g. the `web-capture` or `speech-gateway` examples) build a small set of fixed pipelines and should run with a scoped token instead of `admin`. The role below grants exactly the node kinds the web-capture pipeline (`servo → encode → mux → serve`) needs and nothing more. + +> [!IMPORTANT] +> A plugin must appear in **both** `allowed_nodes` and `allowed_plugins`. Enforcement calls `is_node_allowed(kind)` *before* the plugin check, so a plugin kind missing from `allowed_nodes` is rejected before `allowed_plugins` is ever consulted. ```toml [permissions.roles.gateway] @@ -130,16 +133,17 @@ delete_plugins = false upload_assets = false delete_assets = false allowed_nodes = [ - "transport::http::mse", # serve live casts to the browser (MSE) - "streamkit::http_input", # oneshot request body - "streamkit::http_output", # oneshot response - # Safe core plumbing only - no core::file_writer (arbitrary-write risk). - "core::passthrough", - "core::file_reader", + "plugin::native::servo", # render the page (web-capture) + "video::pixel_convert", # servo RGBA -> encoder input format + "video::vp9::encoder", # encode to VP9 + "containers::webm::muxer", # mux into WebM for MSE / http_output + "transport::http::mse", # serve the live cast to the browser (MSE) "core::pacer", "core::sink", + # No core::file_writer (arbitrary-write risk) and no transport::http::fetcher (SSRF). + # The oneshot streamkit::http_output marker is implicitly allowed. ] -allowed_plugins = ["plugin::native::servo"] # only what the gateway needs +allowed_plugins = ["plugin::native::servo"] # must also be listed in allowed_nodes (see note above) ``` ## Permission reference diff --git a/samples/skit.toml b/samples/skit.toml index fa8b09a72..00a4de50f 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -471,6 +471,13 @@ delete_plugins = false list_nodes = true access_all_sessions = false +# Sample management (mirrors the built-in Permissions::user()); without these +# flags the allowed_samples list below would be unreachable. +list_samples = true +read_samples = true +write_samples = true +delete_samples = true + # Users can access standard samples (mirrors the built-in Permissions::user()). allowed_samples = [ "oneshot/*.yml", @@ -482,20 +489,21 @@ allowed_samples = [ ] # Users can use most nodes except potentially dangerous ones. Mirrors the -# built-in Permissions::user() default: transport::http::fetcher is excluded -# (arbitrary-URL fetch - SSRF risk) and core::file_writer is excluded -# (arbitrary-write risk), while the safe HTTP sink/IO nodes are allowed. +# built-in Permissions::user() default. Allowed: MoQ + the HTTP `mse` sink +# (serves over the caller's own request). Denied by omission: +# transport::http::fetcher (SSRF), core::file_writer / core::object_store_writer +# (arbitrary/external write). The oneshot streamkit::http_input/http_output +# markers are implicitly allowed on the oneshot path and need no entry here. allowed_nodes = [ "audio::*", "video::*", "containers::*", "transport::moq::*", - "transport::http::mse", # serve live casts to the browser (MSE) - "streamkit::http_input", # oneshot request body - "streamkit::http_output", # oneshot response + "transport::http::mse", "core::passthrough", "core::file_reader", "core::pacer", + "core::param_bridge", "core::json_serialize", "core::text_chunker", "core::script", @@ -532,14 +540,17 @@ delete_assets = false allowed_samples = [] -# Only the HTTP sink/IO nodes the gateway needs plus the safe core plumbing -# nodes - no fetcher (SSRF risk) and no core::file_writer (arbitrary-write risk). +# Exactly the node kinds the web-capture / live-cast pipeline needs, nothing more. +# A plugin kind must be allowed here too: is_node_allowed() runs before the +# allowed_plugins check, so a plugin missing from allowed_nodes is rejected first. +# No fetcher (SSRF) and no file_writer (arbitrary write). The oneshot +# streamkit::http_output marker is implicitly allowed and needs no entry. allowed_nodes = [ - "transport::http::mse", # serve live casts to the browser (MSE) - "streamkit::http_input", # oneshot request body - "streamkit::http_output", # oneshot response - "core::passthrough", - "core::file_reader", + "plugin::native::servo", # render the page (web-capture) + "video::pixel_convert", # servo RGBA -> encoder input format + "video::vp9::encoder", # encode to VP9 + "containers::webm::muxer", # mux into WebM for MSE / http_output + "transport::http::mse", # serve the live cast to the browser (MSE) "core::pacer", "core::sink", ] From fd26e7952adf622469880f7e06a518417ae69c1b Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 16:52:51 +0000 Subject: [PATCH 6/8] docs(auth): note audio encoder needed for gateway audio capture The documented gateway role serves a video-only WebM cast. Page audio would also need audio::opus::encoder in allowed_nodes, since the mse node advertises codecs="vp9,opus". Noted as a future extension. Signed-off-by: streamkit-devin --- docs/src/content/docs/guides/authorization.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/content/docs/guides/authorization.md b/docs/src/content/docs/guides/authorization.md index 90ad1daa2..ff7921db1 100644 --- a/docs/src/content/docs/guides/authorization.md +++ b/docs/src/content/docs/guides/authorization.md @@ -146,6 +146,10 @@ allowed_nodes = [ allowed_plugins = ["plugin::native::servo"] # must also be listed in allowed_nodes (see note above) ``` +This role serves a **video-only** WebM cast. To also carry page audio (the `mse` +node advertises `codecs="vp9,opus"`), add the audio encoder — e.g. +`"audio::opus::encoder"` — to `allowed_nodes`. + ## Permission reference | Permission | Description | From 8455c91baa19369b5bc5256327a7abac83477327 Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 16:57:29 +0000 Subject: [PATCH 7/8] fix(auth): mirror built-in user asset policy in sample config The sample user role's allowed_assets only listed audio, while Permissions::user() also grants image and font assets. Add the image/font patterns so the sample matches the built-in default the comments claim to mirror, and assert the asset policy in sample_config_test. Signed-off-by: streamkit-devin --- apps/skit/tests/sample_config_test.rs | 4 ++++ samples/skit.toml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/apps/skit/tests/sample_config_test.rs b/apps/skit/tests/sample_config_test.rs index 3233de2a4..324815edc 100644 --- a/apps/skit/tests/sample_config_test.rs +++ b/apps/skit/tests/sample_config_test.rs @@ -69,6 +69,10 @@ fn samples_skit_toml_parses_and_matches_expected_defaults() { assert!(!user.is_node_allowed("transport::http::fetcher")); assert!(!user.is_node_allowed("core::file_writer")); assert!(!user.is_node_allowed("core::object_store_writer")); + // Asset policy also mirrors the built-in: audio, images and fonts. + assert!(user.is_asset_allowed("samples/audio/system/beep.wav")); + assert!(user.is_asset_allowed("samples/images/system/logo.png")); + assert!(user.is_asset_allowed("samples/fonts/system/inter.ttf")); // The gateway role is least-privilege but must actually be able to build the // servo -> encode -> mux -> serve pipeline. Crucially the plugin kind has to diff --git a/samples/skit.toml b/samples/skit.toml index 00a4de50f..7e4c6af40 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -517,9 +517,14 @@ allowed_nodes = [ allowed_plugins = ["plugin::*"] # Users can list bundled system assets and their uploaded assets +# (mirrors the built-in Permissions::user(): audio, images and fonts). allowed_assets = [ "samples/audio/system/*", "samples/audio/user/*", + "samples/images/system/*", + "samples/images/user/*", + "samples/fonts/system/*", + "samples/fonts/user/*", ] [permissions.roles.gateway] From 4f8ebd0720eec85f56895488492aaaae6a521ac0 Mon Sep 17 00:00:00 2001 From: streamkit-devin Date: Sat, 27 Jun 2026 17:00:31 +0000 Subject: [PATCH 8/8] docs(auth): scope gateway role comment to web-capture example The comment claimed the role serves the speech-gateway too, but the node/plugin lists only cover web-capture. Clarify that the lists are web-capture-scoped and other gateways swap in their own nodes/plugins. Signed-off-by: streamkit-devin --- samples/skit.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/skit.toml b/samples/skit.toml index 7e4c6af40..484725d80 100644 --- a/samples/skit.toml +++ b/samples/skit.toml @@ -528,9 +528,10 @@ allowed_assets = [ ] [permissions.roles.gateway] -# Least-privilege role for trusted intermediaries (e.g. the speech-gateway or -# web-capture examples) that build a small set of fixed pipelines. Avoids -# granting admin just to use HTTP-transport nodes. +# Least-privilege role for trusted intermediaries that build a small set of +# fixed pipelines, so they need not run as admin. The node/plugin lists below +# are scoped to the web-capture example; a different gateway (e.g. speech-gateway) +# would swap in its own nodes and plugins (audio nodes, whisper/kokoro, etc.). create_sessions = true destroy_sessions = true list_sessions = true