package proxmox import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "errors" "log/slog" "net" "os" "path/filepath" "strconv" "strings" "testing" "time" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" "git.cloudinit.dev/coreci/orca/internal/security" ) func TestSudoersContent(t *testing.T) { content := sudoersContent("orca") if !strings.Contains(content, "NOPASSWD: NOEXEC: /usr/bin/pct") { t.Error("missing NOEXEC on pct (AD-020)") } if !strings.Contains(content, "NOPASSWD: NOEXEC: /usr/bin/qm") { t.Error("missing NOEXEC on qm (AD-020)") } if !strings.Contains(content, "NOPASSWD: /usr/bin/apt-get") { t.Error("missing NOPASSWD on apt-get") } if !strings.Contains(content, "NOPASSWD: /usr/bin/dpkg") { t.Error("missing NOPASSWD on dpkg") } if strings.Contains(content, "NOEXEC: /usr/bin/apt-get") { t.Error("apt-get must NOT have NOEXEC (breaks maintainer scripts)") } if strings.Contains(content, "NOEXEC: /usr/bin/dpkg") { t.Error("dpkg must NOT have NOEXEC (breaks maintainer scripts)") } for _, line := range strings.Split(content, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "#") || trimmed == "" { continue } if strings.Contains(trimmed, "pvesh") { t.Errorf("pvesh must be EXCLUDED from sudoers command lines (AD-020): %s", trimmed) } } if !strings.HasPrefix(content, "# /etc/sudoers.d/orca") { t.Error("missing managed-by-orca header") } if !strings.Contains(content, "orca ALL=(root)") { t.Error("missing orca user in sudoers") } } func TestSudoersContent_CustomUser(t *testing.T) { content := sudoersContent("custom-orca") if !strings.Contains(content, "custom-orca ALL=(root)") { t.Error("missing custom-orca user in sudoers") } } func TestOrcaOperatorPrivileges(t *testing.T) { privs := strings.Fields(OrcaOperatorPrivileges) expected := map[string]bool{ "VM.Audit": true, "Datastore.AllocateSpace": true, "SDN.Use": true, } if len(privs) != 3 { t.Errorf("expected 3 privileges, got %d: %v", len(privs), privs) } for _, p := range privs { if !expected[p] { t.Errorf("unexpected privilege %q", p) } } } func TestBootstrapProxmox_Validation(t *testing.T) { ctx := context.Background() _, err := BootstrapProxmox(ctx, Options{Password: "pw"}) if err == nil || !strings.Contains(err.Error(), "host is required") { t.Errorf("expected host-required error, got %v", err) } _, err = BootstrapProxmox(ctx, Options{Host: "10.0.0.1"}) if err == nil || !strings.Contains(err.Error(), "password is required") { t.Errorf("expected password-required error, got %v", err) } } func TestDefaultOptions(t *testing.T) { if DefaultProxmoxUser != "orca" { t.Errorf("DefaultProxmoxUser = %q, want orca", DefaultProxmoxUser) } if DefaultProxmoxRole != "OrcaOperator" { t.Errorf("DefaultProxmoxRole = %q, want OrcaOperator", DefaultProxmoxRole) } if DefaultSSHPort != 22 { t.Errorf("DefaultSSHPort = %d, want 22", DefaultSSHPort) } } type mockSSHDialer struct { client *ssh.Client err error calls int lastAddr string lastCfg *ssh.ClientConfig } func (m *mockSSHDialer) DialContext(ctx context.Context, network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { m.calls++ m.lastAddr = addr m.lastCfg = config if m.err != nil { return nil, m.err } return m.client, nil } func setupORCAHome(t *testing.T) string { t.Helper() dir := t.TempDir() t.Setenv("ORCA_HOME", dir) knownHosts := filepath.Join(dir, "known_hosts") if err := os.WriteFile(knownHosts, []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } return dir } func TestBootstrapProxmox_SSHAuthFailure(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("ssh: handshake failed: ssh: unable to authenticate")} setupORCAHome(t) _, err := BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "ssh") { t.Errorf("error should mention ssh, got: %v", err) } if !strings.Contains(err.Error(), "ssh dial") { t.Errorf("error should mention ssh dial, got: %v", err) } } func TestBootstrapProxmox_SSHDialCalledWithCorrectAddr(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.42", Password: "pw", SSHPort: 2222, }) if dialer.calls != 1 { t.Errorf("dialer calls = %d, want 1", dialer.calls) } if dialer.lastAddr != "10.0.0.42:2222" { t.Errorf("dial addr = %q, want 10.0.0.42:2222", dialer.lastAddr) } } func TestBootstrapProxmox_DefaultSSHPort(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.99", Password: "pw", }) if dialer.lastAddr != "10.0.0.99:22" { t.Errorf("dial addr = %q, want 10.0.0.99:22 (default port)", dialer.lastAddr) } } func TestBootstrapProxmox_CustomSSHUser(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() dialer := &mockSSHDialer{err: errors.New("connection refused")} sshDialer = dialer setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", SSHUser: "custom-admin", }) if dialer.calls != 1 { t.Errorf("dialer calls = %d, want 1", dialer.calls) } if dialer.lastCfg == nil || dialer.lastCfg.User != "custom-admin" { t.Errorf("ssh user not propagated, got %+v", dialer.lastCfg) } } func TestBootstrapProxmox_SSHKeyGenerated(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} dir := setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) keyPath := filepath.Join(dir, "orca_ssh_key") pubPath := filepath.Join(dir, "orca_ssh_key.pub") if _, err := os.Stat(keyPath); err != nil { t.Errorf("SSH key not generated at %s: %v", keyPath, err) } if _, err := os.Stat(pubPath); err != nil { t.Errorf("SSH pub not generated at %s: %v", pubPath, err) } } func TestBootstrapProxmox_KnownHostsFileCreated(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} dir := setupORCAHome(t) _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", }) knownHosts := filepath.Join(dir, "known_hosts") if _, err := os.Stat(knownHosts); err != nil { t.Errorf("known_hosts not created at %s: %v", knownHosts, err) } } func TestBootstrapProxmox_NilLogger(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) defer func() { if r := recover(); r != nil { t.Fatalf("nil logger panicked: %v", r) } }() _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", Logger: nil, }) } func TestBootstrapProxmox_CustomLogger(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) var buf bytes.Buffer log := slog.New(slog.NewTextHandler(&buf, nil)) defer func() { if r := recover(); r != nil { t.Fatalf("custom logger panicked: %v", r) } }() _, _ = BootstrapProxmox(context.Background(), Options{ Host: "10.0.0.1", Password: "pw", Logger: log, }) _ = buf.String() } func TestBootstrapProxmox_ContextCancelled(t *testing.T) { orig := sshDialer defer func() { sshDialer = orig }() sshDialer = &mockSSHDialer{err: errors.New("connection refused")} setupORCAHome(t) ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := BootstrapProxmox(ctx, Options{ Host: "10.0.0.1", Password: "pw", }) if err == nil { t.Fatal("expected error with cancelled context") } } func TestDeployPubKey_EmptyPubLine(t *testing.T) { err := deployPubKey("orca", "") if err == nil { t.Error("expected error for empty pub line") } if !strings.Contains(err.Error(), "empty pub line") { t.Errorf("error should mention empty pub line, got: %v", err) } } func TestDeployPubKey_WhitespaceOnlyPubLine(t *testing.T) { err := deployPubKey("orca", " \n \t ") if err == nil { t.Error("expected error for whitespace-only pub line") } } func TestBootstrapProxmox_FullFlow_IdempotentReRun(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer defer func() { sshDialer = orig }() origRunner := sessionRunner defer func() { sessionRunner = origRunner }() host, _, _ := net.SplitHostPort(srv.addr()) sshDialer = &funcDialer{fn: func(ctx context.Context, network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { return fakeSSHClient(t, srv), nil }} for i := 0; i < 2; i++ { sessionRunner = nil if _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", }); err != nil { t.Fatalf("bootstrap run %d: %v", i+1, err) } } } func TestBootstrapProxmox_FullFlow_NoPasswordInLogs(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer defer func() { sshDialer = orig }() origRunner := sessionRunner defer func() { sessionRunner = origRunner }() sessionRunner = nil sshDialer = &staticDialer{client: fakeSSHClient(t, srv)} host, _, _ := net.SplitHostPort(srv.addr()) var logBuf bytes.Buffer _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "super-secret-pw-12345", Logger: slog.New(slog.NewTextHandler(&logBuf, nil)), }) if err != nil { t.Fatalf("BootstrapProxmox: %v", err) } out := logBuf.String() if strings.Contains(out, "super-secret-pw-12345") { t.Errorf("password leaked into logs (D-031): %s", out) } } func TestBootstrapProxmox_FullFlow_ValidateSudoersFails(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() srv.forceSudoersInvalid = true home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer defer func() { sshDialer = orig }() origRunner := sessionRunner defer func() { sessionRunner = origRunner }() sessionRunner = nil sshDialer = &staticDialer{client: fakeSSHClient(t, srv)} host, _, _ := net.SplitHostPort(srv.addr()) _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", }) if err == nil { t.Fatal("expected error for invalid sudoers") } if !strings.Contains(err.Error(), "validate sudoers") { t.Errorf("error should mention validate sudoers, got: %v", err) } } func TestDefaultSSHDialer_DialContext_ConnectionRefused(t *testing.T) { d := defaultSSHDialer{} cfg := &ssh.ClientConfig{ User: "root", Auth: []ssh.AuthMethod{ssh.Password("pw")}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 200 * time.Millisecond, } _, err := d.DialContext(context.Background(), "tcp", "127.0.0.1:1", cfg) if err == nil { t.Fatal("expected error for connection refused") } } func TestBootstrapProxmox_FullFlow_CreateLinuxUserFails(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer defer func() { sshDialer = orig }() origRunner := sessionRunner defer func() { sessionRunner = origRunner }() sessionRunner = nil sshDialer = &staticDialer{client: fakeSSHClient(t, srv)} host, _, _ := net.SplitHostPort(srv.addr()) // ProxmoxUser=root exercises the /root home branch in deployPubKey. _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", ProxmoxUser: "root", }) if err != nil { t.Fatalf("BootstrapProxmox with ProxmoxUser=root: %v", err) } } func TestSSHSessionRunner_CombinedOutput_NewSessionError(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() conn := fakeSSHClient(t, srv) conn.Close() r := &sshSessionRunner{client: conn} _, err := r.CombinedOutput("echo hi") if err == nil { t.Fatal("expected error from NewSession on closed client") } if !strings.Contains(err.Error(), "new session") { t.Errorf("error should mention new session, got: %v", err) } } // TestPinnedHostKeyCallback_Match verifies the pinned callback returns // nil when the server-presented key matches the operator-supplied // fingerprint (T02.5, REQ-058). func TestPinnedHostKeyCallback_Match(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() host, port, _ := net.SplitHostPort(srv.addr()) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } expectedFP := security.SSHFingerprintSHA256(hostKey) var captured ssh.PublicKey cb, err := pinnedHostKeyCallback(expectedFP, &captured) if err != nil { t.Fatalf("pinnedHostKeyCallback: %v", err) } if err := cb(host+":"+port, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey); err != nil { t.Errorf("match callback returned error: %v", err) } if !bytes.Equal(captured.Marshal(), hostKey.Marshal()) { t.Error("captured key does not match server host key") } } // TestPinnedHostKeyCallback_Mismatch verifies the pinned callback fails // closed on mismatch (T02.5, REQ-058). func TestPinnedHostKeyCallback_Mismatch(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() host, _, _ := net.SplitHostPort(srv.addr()) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } cb, err := pinnedHostKeyCallback("SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", nil) if err != nil { t.Fatalf("pinnedHostKeyCallback: %v", err) } err = cb(host+":22", &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey) if err == nil { t.Fatal("expected mismatch error, got nil") } if !strings.Contains(err.Error(), "REQ-058") { t.Errorf("mismatch error should mention REQ-058, got: %v", err) } } // TestPinnedHostKeyCallback_RejectsRawHex verifies the constructor // rejects a non-SHA256:-prefixed fingerprint (T02.5, D-045). func TestPinnedHostKeyCallback_RejectsRawHex(t *testing.T) { _, err := pinnedHostKeyCallback("abcdef0123456789", nil) if err == nil { t.Fatal("expected error for raw hex fingerprint, got nil") } if !strings.Contains(err.Error(), "SHA256:") { t.Errorf("error should mention SHA256: prefix requirement, got: %v", err) } } // TestTOFUHostKeyCallback_FirstConnectCapturesKey verifies that on // first connect (empty known_hosts) the TOFU callback captures the // server key, writes it to known_hosts, and allows the dial (T02.6 — // v0.6 ship-defect fix). func TestTOFUHostKeyCallback_FirstConnectCapturesKey(t *testing.T) { home := setupORCAHome(t) // empty known_hosts srv := newFakeSSHServer(t) defer srv.close() host, port, _ := net.SplitHostPort(srv.addr()) addr := host + ":" + port hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } cb, err := TOFUHostKeyCallback(addr, nil) if err != nil { t.Fatalf("TOFUHostKeyCallback: %v", err) } if err := cb(addr, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey); err != nil { t.Fatalf("first-connect callback returned error: %v", err) } data, err := os.ReadFile(filepath.Join(home, "known_hosts")) if err != nil { t.Fatalf("read known_hosts: %v", err) } if len(data) == 0 { t.Fatal("known_hosts is empty — TOFU capture did not write the key (v0.6 ship-defect not fixed)") } if !strings.Contains(string(data), knownhosts.Normalize(addr)) { t.Errorf("known_hosts missing the normalized addr %q: %s", knownhosts.Normalize(addr), data) } if !strings.Contains(string(data), hostKey.Type()) { t.Errorf("known_hosts missing the host key type %q: %s", hostKey.Type(), data) } } // TestTOFUHostKeyCallback_SecondConnectMatches verifies that on a // second connect (known_hosts already has the key) the TOFU callback // matches and returns nil (T02.6). func TestTOFUHostKeyCallback_SecondConnectMatches(t *testing.T) { setupORCAHome(t) srv := newFakeSSHServer(t) defer srv.close() host, port, _ := net.SplitHostPort(srv.addr()) addr := host + ":" + port hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } // First connect: capture + write. cb1, err := TOFUHostKeyCallback(addr, nil) if err != nil { t.Fatalf("TOFUHostKeyCallback #1: %v", err) } if err := cb1(addr, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey); err != nil { t.Fatalf("first connect: %v", err) } // Second connect: the fresh knownhosts.New reads the written key. cb2, err := TOFUHostKeyCallback(addr, nil) if err != nil { t.Fatalf("TOFUHostKeyCallback #2: %v", err) } if err := cb2(addr, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey); err != nil { t.Fatalf("second connect should match, got: %v", err) } } // TestTOFUHostKeyCallback_MismatchFails verifies that on a mismatch // (known_hosts has a different key) the TOFU callback fails closed // (MITM detection) (T02.6). func TestTOFUHostKeyCallback_MismatchFails(t *testing.T) { setupORCAHome(t) srv := newFakeSSHServer(t) defer srv.close() host, port, _ := net.SplitHostPort(srv.addr()) addr := host + ":" + port hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } // Capture the real key first so known_hosts is populated. cb1, err := TOFUHostKeyCallback(addr, nil) if err != nil { t.Fatalf("TOFUHostKeyCallback #1: %v", err) } if err := cb1(addr, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, hostKey); err != nil { t.Fatalf("first connect: %v", err) } // Generate a different key + present it: callback must fail. pub, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("ed25519 gen: %v", err) } altKey, err := ssh.NewPublicKey(pub) if err != nil { t.Fatalf("new pub: %v", err) } cb2, err := TOFUHostKeyCallback(addr, nil) if err != nil { t.Fatalf("TOFUHostKeyCallback #2: %v", err) } err = cb2(addr, &net.TCPAddr{IP: net.ParseIP(host), Port: 22}, altKey) if err == nil { t.Fatal("expected mismatch error, got nil") } } // TestBootstrapProxmox_PopulatesHostKeyFingerprint verifies that after // a successful bootstrap via TOFU, Result.HostKeyFingerprint is // non-empty and SHA256:-prefixed (T02.7). func TestBootstrapProxmox_PopulatesHostKeyFingerprint(t *testing.T) { srv := newFakeSSHServer(t) defer srv.close() home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer defer func() { sshDialer = orig }() origRunner := sessionRunner defer func() { sessionRunner = origRunner }() sessionRunner = nil // Use the real dialer so the TOFU HostKeyCallback actually runs // against the fake server (a static dialer with an insecure client // would bypass the callback and leave HostKeyFingerprint empty). sshDialer = defaultSSHDialer{} host, port, _ := net.SplitHostPort(srv.addr()) portNum, _ := strconv.Atoi(port) result, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }) if err != nil { t.Fatalf("BootstrapProxmox: %v", err) } if result.HostKeyFingerprint == "" { t.Fatal("Result.HostKeyFingerprint is empty") } if !strings.HasPrefix(result.HostKeyFingerprint, "SHA256:") { t.Errorf("Result.HostKeyFingerprint = %q, want SHA256: prefix", result.HostKeyFingerprint) } } // TestResetHostKey_RemovesTargetLines verifies that ResetHostKey // removes all known_hosts lines for the target host while leaving // other hosts' lines intact (T02.8, REQ-059, D-046). func TestResetHostKey_RemovesTargetLines(t *testing.T) { home := setupORCAHome(t) path := filepath.Join(home, "known_hosts") original := []byte("[10.0.0.1]:22 ssh-ed25519 AAAAKEY1 host1\n" + "10.0.0.1 ssh-ed25519 AAAAKEY1ALT host1-alt\n" + "[10.0.0.2]:22 ssh-ed25519 AAAAKEY2 host2\n") if err := os.WriteFile(path, original, 0o600); err != nil { t.Fatalf("write known_hosts: %v", err) } if err := ResetHostKey("10.0.0.1"); err != nil { t.Fatalf("ResetHostKey: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read known_hosts: %v", err) } result := string(data) if strings.Contains(result, "AAAAKEY1") { t.Errorf("target host key line not removed: %s", result) } if strings.Contains(result, "AAAAKEY1ALT") { t.Errorf("target host alt key line not removed: %s", result) } if !strings.Contains(result, "AAAAKEY2") { t.Errorf("other host's line was removed (should be intact): %s", result) } } // TestResetHostKey_NoMatchingLinesIsNoop verifies that ResetHostKey is // a no-op when no lines match (T02.8). func TestResetHostKey_NoMatchingLinesIsNoop(t *testing.T) { home := setupORCAHome(t) path := filepath.Join(home, "known_hosts") original := []byte("[10.0.0.2]:22 ssh-ed25519 AAAAKEY2 host2\n") if err := os.WriteFile(path, original, 0o600); err != nil { t.Fatalf("write known_hosts: %v", err) } if err := ResetHostKey("10.0.0.99"); err != nil { t.Fatalf("ResetHostKey: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read known_hosts: %v", err) } if string(data) != string(original) { t.Errorf("known_hosts changed on no-match: got %q, want %q", data, original) } } // TestResetHostKey_MissingFileIsNoop verifies ResetHostKey returns nil // when known_hosts does not exist (T02.8). func TestResetHostKey_MissingFileIsNoop(t *testing.T) { setupORCAHome(t) if err := ResetHostKey("10.0.0.1"); err != nil { t.Errorf("ResetHostKey on missing file should be no-op, got: %v", err) } } // TestResetHostKey_EmptyHostErrors verifies ResetHostKey rejects an // empty host (T02.8). func TestResetHostKey_EmptyHostErrors(t *testing.T) { if err := ResetHostKey(""); err == nil { t.Error("expected error for empty host, got nil") } } // bootstrapE2ESetup wires the real dialer against a fake SSH server so // the full HostKeyCallback path (pinned or TOFU) runs end-to-end through // BootstrapProxmox. Returns the host, port, and server (for fingerprint // computation). The known_hosts file is created empty in the temp // ORCA_HOME. func bootstrapE2ESetup(t *testing.T) (srv *fakeSSHServer, host, port string) { t.Helper() srv = newFakeSSHServer(t) t.Cleanup(srv.close) home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.WriteFile(filepath.Join(home, "known_hosts"), []byte{}, 0o600); err != nil { t.Fatalf("create known_hosts: %v", err) } orig := sshDialer t.Cleanup(func() { sshDialer = orig }) origRunner := sessionRunner t.Cleanup(func() { sessionRunner = origRunner }) sessionRunner = nil sshDialer = defaultSSHDialer{} host, port, _ = net.SplitHostPort(srv.addr()) return srv, host, port } // TestBootstrapE2E_PinnedFingerprintCorrect verifies that // --host-key-fingerprint with the correct pin (T02.10 case 1) succeeds // end-to-end and Result.HostKeyFingerprint equals the pinned value. func TestBootstrapE2E_PinnedFingerprintCorrect(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } pin := security.SSHFingerprintSHA256(hostKey) portNum, _ := strconv.Atoi(port) result, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, HostKeyFingerprint: pin, }) if err != nil { t.Fatalf("BootstrapProxmox with correct pin: %v", err) } if result.HostKeyFingerprint != pin { t.Errorf("Result.HostKeyFingerprint = %q, want %q (pinned value)", result.HostKeyFingerprint, pin) } } // TestBootstrapE2E_PinnedFingerprintWrong verifies that // --host-key-fingerprint with a wrong pin (T02.10 case 2) fails fast // with the REQ-058 mismatch error, before any SSH session commands run. func TestBootstrapE2E_PinnedFingerprintWrong(t *testing.T) { _, host, port := bootstrapE2ESetup(t) portNum, _ := strconv.Atoi(port) wrong := "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, HostKeyFingerprint: wrong, }) if err == nil { t.Fatal("expected error for wrong pin, got nil") } if !strings.Contains(err.Error(), "REQ-058") { t.Errorf("error should mention REQ-058, got: %v", err) } } // TestBootstrapE2E_TOFUFirstConnectCapturesKey verifies that with no // --host-key-fingerprint on a first connect (empty known_hosts) (T02.10 // case 3) the TOFU callback captures the key, writes known_hosts, and // bootstrap succeeds — exercised end-to-end through BootstrapProxmox. func TestBootstrapE2E_TOFUFirstConnectCapturesKey(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } portNum, _ := strconv.Atoi(port) home := os.Getenv("ORCA_HOME") knownHostsPath := filepath.Join(home, "known_hosts") before, _ := os.ReadFile(knownHostsPath) if len(before) != 0 { t.Fatalf("precondition: known_hosts not empty: %q", before) } result, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }) if err != nil { t.Fatalf("BootstrapProxmox first connect: %v", err) } data, err := os.ReadFile(knownHostsPath) if err != nil { t.Fatalf("read known_hosts: %v", err) } if len(data) == 0 { t.Fatal("known_hosts empty — TOFU did not capture the key end-to-end") } expectedFP := security.SSHFingerprintSHA256(hostKey) if result.HostKeyFingerprint != expectedFP { t.Errorf("Result.HostKeyFingerprint = %q, want %q", result.HostKeyFingerprint, expectedFP) } } // TestBootstrapE2E_TOFUSecondConnectMatches verifies that a second // connect (known_hosts already has the key from the first connect) // (T02.10 case 4) matches and succeeds end-to-end. func TestBootstrapE2E_TOFUSecondConnectMatches(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) portNum, _ := strconv.Atoi(port) for i := 0; i < 2; i++ { sessionRunner = nil if _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }); err != nil { t.Fatalf("bootstrap run %d: %v", i+1, err) } } _ = srv } // TestBootstrapE2E_TOFUMismatchFails verifies that when known_hosts has // a different key (T02.10 case 5) the second connect fails with a // mismatch (MITM detection) — end-to-end through BootstrapProxmox. func TestBootstrapE2E_TOFUMismatchFails(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } portNum, _ := strconv.Atoi(port) home := os.Getenv("ORCA_HOME") knownHostsPath := filepath.Join(home, "known_hosts") altPub, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("ed25519 gen: %v", err) } altKey, err := ssh.NewPublicKey(altPub) if err != nil { t.Fatalf("new pub: %v", err) } addr := host + ":" + port altLine := knownhosts.Line([]string{knownhosts.Normalize(addr)}, altKey) if err := os.WriteFile(knownHostsPath, []byte(altLine+"\n"), 0o600); err != nil { t.Fatalf("write known_hosts: %v", err) } _, err = BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }) if err == nil { t.Fatal("expected MITM/mismatch error, got nil") } if !strings.Contains(err.Error(), "ssh dial") { t.Errorf("error should mention ssh dial, got: %v", err) } } // TestBootstrapE2E_PrePopulatedKnownHostsMatches verifies the v0.6→v0.8 // migration path (T02.10 case 7): a known_hosts entry written by a prior // join (simulating a v0.6 install) is matched on second-connect without // re-capture, end-to-end through BootstrapProxmox. func TestBootstrapE2E_PrePopulatedKnownHostsMatches(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) hostKey := srv.hostPublicKey() if hostKey == nil { t.Fatal("server host key is nil") } portNum, _ := strconv.Atoi(port) home := os.Getenv("ORCA_HOME") knownHostsPath := filepath.Join(home, "known_hosts") addr := host + ":" + port preLine := knownhosts.Line([]string{knownhosts.Normalize(addr)}, hostKey) if err := os.WriteFile(knownHostsPath, []byte(preLine+"\n"), 0o600); err != nil { t.Fatalf("write known_hosts: %v", err) } result, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }) if err != nil { t.Fatalf("BootstrapProxmox on pre-populated known_hosts: %v", err) } expectedFP := security.SSHFingerprintSHA256(hostKey) if result.HostKeyFingerprint != expectedFP { t.Errorf("Result.HostKeyFingerprint = %q, want %q", result.HostKeyFingerprint, expectedFP) } } // TestBootstrapE2E_KeyResetThenRePin verifies T02.10 case 6: after // ResetHostKey removes the known_hosts entry, the next BootstrapProxmox // connect re-pins the key via TOFU and succeeds end-to-end. The reset // target is the known_hosts entry key (host:port, normalized), which // matches how the cli resolves the host from a proxmox node's address // for non-default ports. func TestBootstrapE2E_KeyResetThenRePin(t *testing.T) { srv, host, port := bootstrapE2ESetup(t) portNum, _ := strconv.Atoi(port) home := os.Getenv("ORCA_HOME") knownHostsPath := filepath.Join(home, "known_hosts") addr := host + ":" + port // First connect: TOFU captures + writes known_hosts. sessionRunner = nil if _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }); err != nil { t.Fatalf("first bootstrap: %v", err) } before, _ := os.ReadFile(knownHostsPath) if len(before) == 0 { t.Fatal("precondition: known_hosts empty after first connect") } // Reset: known_hosts entry removed. Pass the full addr (host:port) // so Normalize produces the same bracketed form the TOFU callback // wrote for a non-default port. if err := ResetHostKey(addr); err != nil { t.Fatalf("ResetHostKey: %v", err) } after, _ := os.ReadFile(knownHostsPath) if strings.Contains(string(after), knownhosts.Normalize(addr)) { t.Fatalf("known_hosts still contains host after reset: %q", after) } // Next connect re-pins via TOFU + succeeds. sessionRunner = nil if _, err := BootstrapProxmox(t.Context(), Options{ Host: host, Password: "pw", SSHPort: portNum, }); err != nil { t.Fatalf("re-pin bootstrap after reset: %v", err) } rePinned, _ := os.ReadFile(knownHostsPath) if !strings.Contains(string(rePinned), knownhosts.Normalize(addr)) { t.Fatalf("known_hosts not re-populated on next connect: %q", rePinned) } _ = srv }