fix(portal): use public key for TURN creds (#9870)

As a followup to #9856, after talking with @bmanifold, we determined
using the public_key as the username for TURN credentials is a safer bet
because:

- It's by definition public and therefore does not need to be obfuscated
- It's shorter-lived than the token, especially for the gateway
- It essentially represents the data plane connection for client/gateway
and naturally rotates along with the key state for those
This commit is contained in:
Jamil
2025-07-14 18:48:02 -07:00
committed by GitHub
parent 1e577d31b9
commit 17d7e29b81
8 changed files with 44 additions and 68 deletions

View File

@@ -110,7 +110,7 @@ defmodule API.Client.Channel do
relays:
Views.Relay.render_many(
relays,
socket.assigns.turn_salt,
socket.assigns.client.public_key,
socket.assigns.subject.expires_at
),
interface:
@@ -447,7 +447,7 @@ defmodule API.Client.Channel do
connected:
Views.Relay.render_many(
relays,
socket.assigns.turn_salt,
socket.assigns.client.public_key,
socket.assigns.subject.expires_at
)
}
@@ -505,7 +505,7 @@ defmodule API.Client.Channel do
connected:
Views.Relay.render_many(
relays,
socket.assigns.turn_salt,
socket.assigns.client.public_key,
socket.assigns.subject.expires_at
)
})

View File

@@ -28,14 +28,8 @@ defmodule API.Client.Socket do
account_id: subject.account.id
})
# For Relay credentials
turn_salt =
Domain.Crypto.hash(:sha256, token)
|> Base.url_encode64(padding: false)
socket =
socket
|> assign(:turn_salt, turn_salt)
|> assign(:subject, subject)
|> assign(:client, client)
|> assign(:opentelemetry_span_ctx, OpenTelemetry.Tracer.current_span_ctx())

View File

@@ -50,7 +50,11 @@ defmodule API.Gateway.Channel do
account_slug: account.slug,
interface: Views.Interface.render(socket.assigns.gateway),
relays:
Views.Relay.render_many(relays, socket.assigns.turn_salt, relay_credentials_expire_at),
Views.Relay.render_many(
relays,
socket.assigns.gateway.public_key,
relay_credentials_expire_at
),
# These aren't used but needed for API compatibility
config: %{
ipv4_masquerade_enabled: true,
@@ -183,7 +187,7 @@ defmodule API.Gateway.Channel do
connected:
Views.Relay.render_many(
relays,
socket.assigns.turn_salt,
socket.assigns.gateway.public_key,
relay_credentials_expire_at
)
}
@@ -244,7 +248,7 @@ defmodule API.Gateway.Channel do
connected:
Views.Relay.render_many(
relays,
socket.assigns.turn_salt,
socket.assigns.gateway.public_key,
relay_credentials_expire_at
)
})

View File

@@ -27,14 +27,8 @@ defmodule API.Gateway.Socket do
version: gateway.last_seen_version
})
# For Relay credentials
turn_salt =
Domain.Crypto.hash(:sha256, encoded_token)
|> Base.url_encode64(padding: false)
socket =
socket
|> assign(:turn_salt, turn_salt)
|> assign(:token_id, token.id)
|> assign(:gateway_group, group)
|> assign(:gateway, gateway)

View File

@@ -137,8 +137,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -192,8 +191,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -215,8 +213,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -234,8 +231,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -249,8 +245,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -263,8 +258,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client") ==
{:error, %{reason: :invalid_version}}
@@ -403,8 +397,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -477,8 +470,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -565,8 +557,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -618,8 +609,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -683,8 +673,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -1446,8 +1435,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -1521,8 +1509,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -1875,8 +1862,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -1945,8 +1931,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -2157,8 +2142,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")
@@ -2369,8 +2353,7 @@ defmodule API.Client.ChannelTest do
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
client: client,
subject: subject,
turn_salt: "test_salt"
subject: subject
})
|> subscribe_and_join(API.Client.Channel, "client")

View File

@@ -23,8 +23,7 @@ defmodule API.Gateway.ChannelTest do
gateway: gateway,
gateway_group: gateway_group,
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
turn_salt: "test_salt"
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test")
})
|> subscribe_and_join(API.Gateway.Channel, "gateway")
@@ -342,8 +341,7 @@ defmodule API.Gateway.ChannelTest do
gateway: gateway,
gateway_group: gateway_group,
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
turn_salt: "test_salt"
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test")
})
|> subscribe_and_join(API.Gateway.Channel, "gateway")
@@ -395,8 +393,7 @@ defmodule API.Gateway.ChannelTest do
gateway: gateway,
gateway_group: gateway_group,
opentelemetry_ctx: OpenTelemetry.Ctx.new(),
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"),
turn_salt: "test_salt"
opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test")
})
|> subscribe_and_join(API.Gateway.Channel, "gateway")

View File

@@ -272,15 +272,17 @@ defmodule Domain.Relays do
# TODO: Relays
# Revisit credential lifetime when https://github.com/firezone/firezone/issues/8222 is implemented
def generate_username_and_password(%Relay{stamp_secret: stamp_secret}, salt, expires_at)
def generate_username_and_password(%Relay{stamp_secret: stamp_secret}, public_key, expires_at)
when is_binary(stamp_secret) do
salt = generate_hash(public_key)
expires_at = DateTime.to_unix(expires_at, :second)
password = generate_hash(expires_at, stamp_secret, salt)
password = generate_hash("#{expires_at}:#{salt}:#{stamp_secret}")
%{username: "#{expires_at}:#{salt}", password: password, expires_at: expires_at}
end
defp generate_hash(expires_at, stamp_secret, salt) do
:crypto.hash(:sha256, "#{expires_at}:#{stamp_secret}:#{salt}")
defp generate_hash(string) do
:crypto.hash(:sha256, string)
|> Base.encode64(padding: false)
end

View File

@@ -790,19 +790,21 @@ defmodule Domain.RelaysTest do
test "returns username and password", %{account: account} do
relay = Fixtures.Relays.create_relay(account: account)
stamp_secret = "test_secret"
turn_salt = "test_salt"
public_key = "test_public_key"
relay = %{relay | stamp_secret: stamp_secret}
{:ok, expires_at, _} = DateTime.from_iso8601("2023-10-01T00:00:00Z")
assert %{username: username, password: password, expires_at: expires_at_unix} =
generate_username_and_password(relay, turn_salt, expires_at)
generate_username_and_password(relay, public_key, expires_at)
assert [username_expires_at_unix, username_salt] =
String.split(username, ":", parts: 2)
assert [username_expires_at_unix, username_salt] = String.split(username, ":", parts: 2)
assert username_expires_at_unix == to_string(expires_at_unix)
assert username_salt == turn_salt
assert username_salt == "5d9CsB7vot2KRIXMGXivBcgmjnW0ClvN5q/DxOeFotA"
assert DateTime.from_unix!(expires_at_unix) == DateTime.truncate(expires_at, :second)
assert username == "1696118400:test_salt"
assert password == "P0+gMB7RdvcvPv3eYFh1VSJUJh/FoAmOjUOqU8dToD8"
assert username == "1696118400:5d9CsB7vot2KRIXMGXivBcgmjnW0ClvN5q/DxOeFotA"
assert password == "GmFbvRR/LGes0VUmNhzwxG2K2Ww6Y0GTaLVS4S5QJOs"
end
end