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.
This commit is contained in:
Spike Curtis
2025-04-04 12:51:46 +04:00
committed by GitHub
parent 3a0e8ddf97
commit f6bf6c6ec4
2 changed files with 28 additions and 6 deletions

View File

@ -1186,9 +1186,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
network := a.network network := a.network
a.closeMutex.Unlock() a.closeMutex.Unlock()
if network == nil { if network == nil {
keySeed, err := WorkspaceKeySeed(manifest.WorkspaceID, manifest.AgentName) keySeed, err := SSHKeySeed(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName)
if err != nil { 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 // use the graceful context here, because creating the tailnet is not itself tied to the
// agent API. // 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 // This uses the FNV-1a hash algorithm which provides decent distribution and collision
// resistance for string inputs. // 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() 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 { if err != nil {
return 42, err return 42, err
} }

View File

@ -479,6 +479,9 @@ func TestSSH(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name) inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, root)
inv.Stdin = clientOutput inv.Stdin = clientOutput
@ -490,7 +493,7 @@ func TestSSH(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
}) })
keySeed, err := agent.WorkspaceKeySeed(workspace.ID, "dev") keySeed, err := agent.SSHKeySeed(user.Username, workspace.Name, "dev")
assert.NoError(t, err) assert.NoError(t, err)
signer, err := agentssh.CoderSigner(keySeed) signer, err := agentssh.CoderSigner(keySeed)