config: normalize ssh_key_path — expand ~/, reject non-absolute

Bug: resolveSSHKeyPath returned a configured ssh_key_path verbatim.
That meant:

- ssh_key_path = "~/.ssh/id_ed25519" kept the literal "~" — downstream
  readers (internal/guest/ssh.go, internal/daemon/image_seed.go,
  internal/daemon/vm_authsync.go, internal/cli/ssh.go) do raw
  os.ReadFile on the path and fail at runtime with a path that looks
  fine but isn't.
- ssh_key_path = "id_ed25519" (relative) silently worked or didn't
  depending on the daemon's cwd — the daemon process's cwd is not
  the user's shell cwd, so behavior was non-obvious.

Fix: add normalizeSSHKeyPath() run over configured values. It:

  - expands "~/..." against $HOME
  - rejects bare "~" (ambiguous)
  - rejects "~user/..." (we don't do user-tilde)
  - rejects relative paths outright
  - returns filepath.Clean'd absolute paths

Tests cover the accepting case (home-anchored expansion) and every
rejection branch via a table-driven subtests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-22 17:00:34 -03:00
parent b1fbf695ca
commit 617008e8f1
No known key found for this signature in database
GPG key ID: 33112E6833C34679
2 changed files with 88 additions and 1 deletions

View file

@ -50,6 +50,55 @@ func TestLoadDefaultsResolveFirecrackerAndGenerateSSHKey(t *testing.T) {
}
}
func TestLoadSSHKeyPathExpandsHomeAnchored(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
configDir := t.TempDir()
data := []byte("ssh_key_path = \"~/mykeys/id_ed25519\"\n")
if err := os.WriteFile(filepath.Join(configDir, "config.toml"), data, 0o644); err != nil {
t.Fatalf("write config.toml: %v", err)
}
cfg, err := Load(paths.Layout{ConfigDir: configDir, SSHDir: t.TempDir()})
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join(homeDir, "mykeys", "id_ed25519")
if cfg.SSHKeyPath != want {
t.Fatalf("SSHKeyPath = %q, want %q", cfg.SSHKeyPath, want)
}
}
func TestLoadRejectsInvalidSSHKeyPath(t *testing.T) {
cases := []struct {
name string
raw string
want string
}{
{"relative bare", "id_ed25519", "must be absolute"},
{"relative with dot", "./keys/id_ed25519", "must be absolute"},
{"bare tilde", "~", "bare '~' is not supported"},
{"user-tilde", "~other/id_ed25519", "only '~/' is expanded"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
configDir := t.TempDir()
data := []byte("ssh_key_path = \"" + tc.raw + "\"\n")
if err := os.WriteFile(filepath.Join(configDir, "config.toml"), data, 0o644); err != nil {
t.Fatalf("write config.toml: %v", err)
}
_, err := Load(paths.Layout{ConfigDir: configDir, SSHDir: t.TempDir()})
if err == nil {
t.Fatalf("Load %q: want error containing %q", tc.raw, tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Load %q: error = %v, want contains %q", tc.raw, err, tc.want)
}
})
}
}
func TestLoadAppliesConfigOverrides(t *testing.T) {
configDir := t.TempDir()
data := []byte(`