From f6bf6c6ec4837fef2e82eb5dc8da96dcdaf54530 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 4 Apr 2025 12:51:46 +0400 Subject: [PATCH] fix!: use names not IDs for agent SSH key seed (#17258) Changes the SSH host key seeding to use the owner username, workspace name, and agent name. This prevents SSH from complaining about a mismatched host key if you use Coder Desktop to connect, and delete and recreate your workspace with the same name. Previously this would generate a different key because the workspace ID changed. We also include the owner's username in anticipation of using Coder Desktop to access shared workspaces (or as a superuser) down the road, so that workspaces with the same name owned by different users will not have the same key. This change is **BREAKING** in a limited sense that early access users of Coder Desktop will see their SSH clients complain about host keys changing the first time each workspace is rebuilt with this code. It can be resolved by clearing your `.ssh/known_hosts` file of the Coder workspaces you access this way. --- agent/agent.go | 29 ++++++++++++++++++++++++----- cli/ssh_test.go | 5 ++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index eddebc5d6b..1e18290d84 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1186,9 +1186,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co network := a.network a.closeMutex.Unlock() if network == nil { - keySeed, err := WorkspaceKeySeed(manifest.WorkspaceID, manifest.AgentName) + keySeed, err := SSHKeySeed(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName) if err != nil { - return xerrors.Errorf("generate seed from workspace id: %w", err) + return xerrors.Errorf("generate SSH key seed: %w", err) } // use the graceful context here, because creating the tailnet is not itself tied to the // agent API. @@ -2068,12 +2068,31 @@ func PrometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger sl }) } -// WorkspaceKeySeed converts a WorkspaceID UUID and agent name to an int64 hash. +// SSHKeySeed converts an owner userName, workspaceName and agentName to an int64 hash. // This uses the FNV-1a hash algorithm which provides decent distribution and collision // resistance for string inputs. -func WorkspaceKeySeed(workspaceID uuid.UUID, agentName string) (int64, error) { +// +// Why owner username, workspace name, and agent name? These are the components that are used in hostnames for the +// workspace over SSH, and so we want the workspace to have a stable key with respect to these. We don't use the +// respective UUIDs. The workspace UUID would be different if you delete and recreate a workspace with the same name. +// The agent UUID is regenerated on each build. Since Coder's Tailnet networking is handling the authentication, we +// should not be showing users warnings about host SSH keys. +func SSHKeySeed(userName, workspaceName, agentName string) (int64, error) { h := fnv.New64a() - _, err := h.Write(workspaceID[:]) + _, err := h.Write([]byte(userName)) + if err != nil { + return 42, err + } + // null separators between strings so that (dog, foodstuff) is distinct from (dogfood, stuff) + _, err = h.Write([]byte{0}) + if err != nil { + return 42, err + } + _, err = h.Write([]byte(workspaceName)) + if err != nil { + return 42, err + } + _, err = h.Write([]byte{0}) if err != nil { return 42, err } diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 1097338078..4bd7682067 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -479,6 +479,9 @@ func TestSSH(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name) clitest.SetupConfig(t, client, root) inv.Stdin = clientOutput @@ -490,7 +493,7 @@ func TestSSH(t *testing.T) { assert.NoError(t, err) }) - keySeed, err := agent.WorkspaceKeySeed(workspace.ID, "dev") + keySeed, err := agent.SSHKeySeed(user.Username, workspace.Name, "dev") assert.NoError(t, err) signer, err := agentssh.CoderSigner(keySeed)