From d89b4a7ae038bbed7b18ce3b0649e76611781862 Mon Sep 17 00:00:00 2001 From: Milos Pesic Date: Fri, 14 Aug 2026 13:37:59 +0200 Subject: [PATCH 1/3] Redact mux/twitch shorthand stream keys and websocket url query values (#1713) * Redact mux/twitch shorthand stream keys and websocket url query values mux:// and twitch:// shorthand stream urls carry the stream key as the entire authority, but RedactStreamKey only matched rtmp(s):// urls, so shorthand keys passed through redaction unchanged and ended up raw in request logs. Track egress websocket urls can carry credentials in query parameters and were not redacted at all. Also fixes RedactUpload reading AssumeRoleExternalId instead of SessionToken when redacting SessionToken. Co-Authored-By: Claude Fable 5 * Fix expected mux redaction suffix in TestRedactStreamOutput Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- egress/redact.go | 7 ++++++- egress/redact_test.go | 31 +++++++++++++++++++++++++++++++ utils/redact.go | 23 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/egress/redact.go b/egress/redact.go index 96ffe66c0..944e84c69 100644 --- a/egress/redact.go +++ b/egress/redact.go @@ -35,7 +35,7 @@ func RedactUpload(req UploadRequest) { s3.AccessKey = utils.Redact(s3.AccessKey, "{access_key}") s3.Secret = utils.Redact(s3.Secret, "{secret}") s3.AssumeRoleExternalId = utils.Redact(s3.AssumeRoleExternalId, "{external_id}") - s3.SessionToken = utils.Redact(s3.AssumeRoleExternalId, "{session_token}") + s3.SessionToken = utils.Redact(s3.SessionToken, "{session_token}") return } @@ -91,6 +91,11 @@ func RedactDirectOutputs(out DirectOutput) { if f := out.GetFile(); f != nil { RedactUpload(f) } + if track, ok := out.(*livekit.TrackEgressRequest); ok { + if ws, ok := track.Output.(*livekit.TrackEgressRequest_WebsocketUrl); ok { + ws.WebsocketUrl = utils.RedactUrlQueryValues(ws.WebsocketUrl) + } + } } func RedactStreamKeys(stream *livekit.StreamOutput) { diff --git a/egress/redact_test.go b/egress/redact_test.go index 561e67276..56791ae07 100644 --- a/egress/redact_test.go +++ b/egress/redact_test.go @@ -71,6 +71,20 @@ func TestRedactUpload(t *testing.T) { require.Equal(t, "{external_id}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.AssumeRoleExternalId) require.Equal(t, "{session_token}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.SessionToken) + sessionTokenOnly := &livekit.EncodedFileOutput{ + Output: &livekit.EncodedFileOutput_S3{ + S3: &livekit.S3Upload{ + AccessKey: "ACCESS_KEY", + Secret: "LONG_SECRET_STRING", + SessionToken: "SESSION_TOKEN", + }, + }, + } + cl = proto.Clone(sessionTokenOnly) + RedactUpload(cl.(UploadRequest)) + + require.Equal(t, "{session_token}", cl.(*livekit.EncodedFileOutput).Output.(*livekit.EncodedFileOutput_S3).S3.SessionToken) + cl = proto.Clone(image) RedactUpload(cl.(UploadRequest)) @@ -93,11 +107,17 @@ func TestRedactStreamOutput(t *testing.T) { so := &livekit.StreamOutput{ Urls: []string{ "rtmps://foo.bar.com/app/secret_stream_key", + "mux://8e0b1b9c-50a2-d893-ec0a-102056d112ae", + "twitch://live_12345678_abcdefghijklmnop", + "srt://foo.bar.com:9999", }, } RedactStreamKeys(so) require.Equal(t, "rtmps://foo.bar.com/app/{sec...key}", so.Urls[0]) + require.Equal(t, "mux://{8e0...2ae}", so.Urls[1]) + require.Equal(t, "twitch://{liv...nop}", so.Urls[2]) + require.Equal(t, "srt://foo.bar.com:9999", so.Urls[3]) } func TestRedactEncodedOutputs(t *testing.T) { @@ -154,4 +174,15 @@ func TestRedactDirectOutput(t *testing.T) { RedactDirectOutputs(track) require.Equal(t, "{access_key}", track.Output.(*livekit.TrackEgressRequest_File).File.Output.(*livekit.DirectFileOutput_S3).S3.AccessKey) require.Equal(t, "{secret}", track.Output.(*livekit.TrackEgressRequest_File).File.Output.(*livekit.DirectFileOutput_S3).S3.Secret) + + websocket := &livekit.TrackEgressRequest{ + Output: &livekit.TrackEgressRequest_WebsocketUrl{ + WebsocketUrl: "wss://foo.bar.com/audio?callId=12145&token=7df13dab0d4437602d6f7056b97e72b9", + }, + } + + RedactDirectOutputs(websocket) + require.Equal(t, + "wss://foo.bar.com/audio?callId={1...5}&token={7df...2b9}", + websocket.Output.(*livekit.TrackEgressRequest_WebsocketUrl).WebsocketUrl) } diff --git a/utils/redact.go b/utils/redact.go index d894f6efa..cf476b264 100644 --- a/utils/redact.go +++ b/utils/redact.go @@ -23,7 +23,14 @@ import ( // rtmp urls must be of format rtmp(s)://{host}(/{path})/{app}/{stream_key}( live=1) var rtmpRegexp = regexp.MustCompile(`^(rtmps?://)(.*/)(.*/)(\S*)( live=1)?$`) +// mux and twitch shorthand urls carry the stream key as the entire authority +var shorthandStreamRegexp = regexp.MustCompile(`^(mux|twitch)://(\S+)$`) + func RedactStreamKey(url string) (string, bool) { + if match := shorthandStreamRegexp.FindStringSubmatch(url); len(match) == 3 { + return match[1] + "://" + RedactIdentifier(match[2]), true + } + match := rtmpRegexp.FindStringSubmatch(url) if len(match) != 6 { return url, false @@ -33,6 +40,22 @@ func RedactStreamKey(url string) (string, bool) { return strings.Join(match[1:], ""), true } +// RedactUrlQueryValues redacts every query parameter value in rawUrl, keeping parameter names intact. +func RedactUrlQueryValues(rawUrl string) string { + base, query, found := strings.Cut(rawUrl, "?") + if !found || query == "" { + return rawUrl + } + + params := strings.Split(query, "&") + for i, p := range params { + if k, v, ok := strings.Cut(p, "="); ok && v != "" { + params[i] = k + "=" + RedactIdentifier(v) + } + } + return base + "?" + strings.Join(params, "&") +} + func RedactIdentifier(identifier string) string { var prefix, suffix string for i := 3; i > 0; i-- { From 8b1ab81c7d00f733bdc19c8f5b30f6893c5a8268 Mon Sep 17 00:00:00 2001 From: Milos Pesic Date: Fri, 14 Aug 2026 14:09:00 +0200 Subject: [PATCH 2/3] Fall back to query-value redaction for non-rtmp stream urls (#1714) Co-authored-by: Claude Fable 5 --- egress/redact_test.go | 4 +++- utils/redact.go | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/egress/redact_test.go b/egress/redact_test.go index 56791ae07..d0ee4e2f0 100644 --- a/egress/redact_test.go +++ b/egress/redact_test.go @@ -110,6 +110,7 @@ func TestRedactStreamOutput(t *testing.T) { "mux://8e0b1b9c-50a2-d893-ec0a-102056d112ae", "twitch://live_12345678_abcdefghijklmnop", "srt://foo.bar.com:9999", + "srt://foo.bar.com:9999?streamid=124939da-5244&passphrase=WnxknzJTUbwYl9SpdqAudX", }, } @@ -118,6 +119,7 @@ func TestRedactStreamOutput(t *testing.T) { require.Equal(t, "mux://{8e0...2ae}", so.Urls[1]) require.Equal(t, "twitch://{liv...nop}", so.Urls[2]) require.Equal(t, "srt://foo.bar.com:9999", so.Urls[3]) + require.Equal(t, "srt://foo.bar.com:9999?streamid={...}&passphrase={...}", so.Urls[4]) } func TestRedactEncodedOutputs(t *testing.T) { @@ -183,6 +185,6 @@ func TestRedactDirectOutput(t *testing.T) { RedactDirectOutputs(websocket) require.Equal(t, - "wss://foo.bar.com/audio?callId={1...5}&token={7df...2b9}", + "wss://foo.bar.com/audio?callId={...}&token={...}", websocket.Output.(*livekit.TrackEgressRequest_WebsocketUrl).WebsocketUrl) } diff --git a/utils/redact.go b/utils/redact.go index cf476b264..7aa5e90bb 100644 --- a/utils/redact.go +++ b/utils/redact.go @@ -33,6 +33,9 @@ func RedactStreamKey(url string) (string, bool) { match := rtmpRegexp.FindStringSubmatch(url) if len(match) != 6 { + if redacted := RedactUrlQueryValues(url); redacted != url { + return redacted, true + } return url, false } @@ -40,7 +43,7 @@ func RedactStreamKey(url string) (string, bool) { return strings.Join(match[1:], ""), true } -// RedactUrlQueryValues redacts every query parameter value in rawUrl, keeping parameter names intact. +// RedactUrlQueryValues fully redacts every query parameter value in rawUrl, keeping parameter names intact. func RedactUrlQueryValues(rawUrl string) string { base, query, found := strings.Cut(rawUrl, "?") if !found || query == "" { @@ -50,7 +53,7 @@ func RedactUrlQueryValues(rawUrl string) string { params := strings.Split(query, "&") for i, p := range params { if k, v, ok := strings.Cut(p, "="); ok && v != "" { - params[i] = k + "=" + RedactIdentifier(v) + params[i] = k + "={...}" } } return base + "?" + strings.Join(params, "&") From 600c6388739ea61c35cc3df321a6804eb45f4cd8 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Fri, 14 Aug 2026 09:19:10 -0700 Subject: [PATCH 3/3] Update max_participants doc. Ingress participants do count, but Agents don't (#1715) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- livekit/livekit_room.pb.go | 2 +- protobufs/livekit_room.proto | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/livekit/livekit_room.pb.go b/livekit/livekit_room.pb.go index b228265cb..1c2535d4e 100644 --- a/livekit/livekit_room.pb.go +++ b/livekit/livekit_room.pb.go @@ -1174,7 +1174,7 @@ type RoomConfiguration struct { EmptyTimeout uint32 `protobuf:"varint,2,opt,name=empty_timeout,json=emptyTimeout,proto3" json:"empty_timeout,omitempty"` // number of seconds to keep the room open after everyone leaves DepartureTimeout uint32 `protobuf:"varint,3,opt,name=departure_timeout,json=departureTimeout,proto3" json:"departure_timeout,omitempty"` - // limit number of participants that can be in a room, excluding Egress and Ingress participants + // limit number of participants that can be in a room, excluding Egress and Agent participants MaxParticipants uint32 `protobuf:"varint,4,opt,name=max_participants,json=maxParticipants,proto3" json:"max_participants,omitempty"` // metadata of room Metadata string `protobuf:"bytes,11,opt,name=metadata,proto3" json:"metadata,omitempty"` diff --git a/protobufs/livekit_room.proto b/protobufs/livekit_room.proto index 6505837c4..3232438d6 100644 --- a/protobufs/livekit_room.proto +++ b/protobufs/livekit_room.proto @@ -61,7 +61,7 @@ service RoomService { rpc SendData(SendDataRequest) returns (SendDataResponse); // Update room metadata, will cause updates to be broadcasted to everyone in the room, Requires `roomAdmin` - rpc UpdateRoomMetadata (UpdateRoomMetadataRequest) returns (Room); + rpc UpdateRoomMetadata(UpdateRoomMetadataRequest) returns (Room); // Cloud-only // Forward a connected participant's track(s) to another room. Requires `roomAdmin` and `destinationRoom`. The forwarding will @@ -125,7 +125,6 @@ message RoomAgent { repeated RoomAgentDispatch dispatches = 1; } - message ListRoomsRequest { // when set, will only return rooms with name match repeated string names = 1; @@ -222,7 +221,7 @@ message SendDataRequest { bytes data = 2; DataPacket.Kind kind = 3; // mark deprecated - repeated string destination_sids = 4 [deprecated=true]; + repeated string destination_sids = 4 [deprecated = true]; // when set, only forward to these identities repeated string destination_identities = 6; optional string topic = 5; @@ -246,12 +245,12 @@ message UpdateRoomMetadataRequest { } message RoomConfiguration { - string name = 1; // Used as ID, must be unique + string name = 1; // Used as ID, must be unique // number of seconds to keep the room open if no one joins uint32 empty_timeout = 2; // number of seconds to keep the room open after everyone leaves uint32 departure_timeout = 3; - // limit number of participants that can be in a room, excluding Egress and Ingress participants + // limit number of participants that can be in a room, excluding Egress and Agent participants uint32 max_participants = 4; // metadata of room string metadata = 11 [