diff --git a/internal/api/handlers_machines.go b/internal/api/handlers_machines.go index a7cba93..2288e2f 100644 --- a/internal/api/handlers_machines.go +++ b/internal/api/handlers_machines.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/syncserver/internal/config" + "golang.org/x/crypto/ssh" "github.com/syncserver/internal/models" "github.com/syncserver/internal/sshmanager" "github.com/syncserver/internal/syncengine" @@ -355,29 +356,63 @@ func (h *MachineHandler) ApproveFingerprint(w http.ResponseWriter, r *http.Reque var req struct { Fingerprint string `json:"fingerprint"` + HostKey string `json:"host_key"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - if req.Fingerprint == "" { - writeError(w, http.StatusBadRequest, "fingerprint is required") + json.NewDecoder(r.Body).Decode(&req) + + sshDir := filepath.Join("/var/lib/syncserver", "ssh") + knownHostsPath, err := sshmanager.EnsureKnownHosts(sshDir) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to ensure known_hosts") return } - if err := repo.UpdateFingerprint(id, true, req.Fingerprint); err != nil { + privKeyPath := filepath.Join("/var/lib/syncserver", "ssh", "id_ed25519") + if m.SSHKeyID != nil { + sshKeyRepo := models.NewSSHKeyRepository(h.db) + sshKey, err := sshKeyRepo.GetByID(*m.SSHKeyID) + if err == nil && sshKey.PrivateKeyPath != "" { + privKeyPath = sshKey.PrivateKeyPath + } + } + + var fingerprint, pubKeyLine string + + if req.HostKey != "" { + pubKeyLine = req.HostKey + } else { + conn, fp, pubKey, err := sshmanager.ConnectForApproval( + context.Background(), m.Host, m.Port, m.SSHUser, + privKeyPath, knownHostsPath, + ) + if err != nil { + writeError(w, http.StatusInternalServerError, fmt.Sprintf("could not retrieve host key: %v", err)) + return + } + conn.Close() + fingerprint = fp + pubKeyLine = string(ssh.MarshalAuthorizedKey(pubKey)) + } + + if fingerprint == "" && req.Fingerprint != "" { + fingerprint = req.Fingerprint + } + + if pubKeyLine != "" { + if err := sshmanager.AddKnownHost(sshDir, m.Host, m.Port, []byte(pubKeyLine)); err != nil { + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to add known_host entry: %v", err)) + return + } + } + + if err := repo.UpdateFingerprint(id, true, fingerprint); err != nil { slog.Error("failed to update fingerprint", "id", id, "error", err) writeError(w, http.StatusInternalServerError, "failed to update fingerprint") return } - sshDir := filepath.Join("/var/lib/syncserver", "ssh") - if err := sshmanager.AddKnownHost(sshDir, m.Host, m.Port, []byte(req.Fingerprint)); err != nil { - slog.Warn("failed to add known_host entry", "host", m.Host, "error", err) - } - m.FingerprintConfirmed = true - m.HostKeyFingerprint = &req.Fingerprint + m.HostKeyFingerprint = &fingerprint writeJSON(w, machineToResp(*m)) } diff --git a/internal/sshmanager/shutdown.go b/internal/sshmanager/shutdown.go index d049ce4..72a99d2 100644 --- a/internal/sshmanager/shutdown.go +++ b/internal/sshmanager/shutdown.go @@ -14,7 +14,7 @@ type ShutdownResult struct { } func RunRemoteCommand(ctx context.Context, host string, port int, user, privKeyPath, knownHostsPath string, strictHostKeyChecking bool, command string) (*ShutdownResult, error) { - conn, _, err := dialSSH(ctx, host, port, user, privKeyPath, knownHostsPath, strictHostKeyChecking) + conn, _, _, err := dialSSH(ctx, host, port, user, privKeyPath, knownHostsPath, strictHostKeyChecking) if err != nil { return &ShutdownResult{Success: false, Error: err.Error()}, nil } diff --git a/internal/sshmanager/testconn.go b/internal/sshmanager/testconn.go index 179091c..3b2effc 100644 --- a/internal/sshmanager/testconn.go +++ b/internal/sshmanager/testconn.go @@ -22,26 +22,28 @@ type ConnResult struct { Fingerprint string } -func dialSSH(ctx context.Context, host string, port int, user, privKeyPath, knownHostsPath string, strictHostKeyChecking bool) (*ssh.Client, string, error) { +func dialSSH(ctx context.Context, host string, port int, user, privKeyPath, knownHostsPath string, strictHostKeyChecking bool) (*ssh.Client, string, ssh.PublicKey, error) { addr := fmt.Sprintf("%s:%d", host, port) auths := []ssh.AuthMethod{} if privKeyPath != "" { key, err := os.ReadFile(privKeyPath) if err != nil { - return nil, "", fmt.Errorf("reading private key: %w", err) + return nil, "", nil, fmt.Errorf("reading private key: %w", err) } signer, err := ssh.ParsePrivateKey(key) if err != nil { - return nil, "", fmt.Errorf("parsing private key: %w", err) + return nil, "", nil, fmt.Errorf("parsing private key: %w", err) } auths = append(auths, ssh.PublicKeys(signer)) } var capturedFingerprint string + var capturedPubKey ssh.PublicKey hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error { h := sha256.Sum256(key.Marshal()) capturedFingerprint = "SHA256:" + base64.RawStdEncoding.EncodeToString(h[:]) + capturedPubKey = key if strictHostKeyChecking && knownHostsPath != "" { kh, err := GetKnownHost(filepath.Dir(knownHostsPath), host, port) if err != nil { @@ -71,15 +73,15 @@ func dialSSH(ctx context.Context, host string, port int, user, privKeyPath, know conn, err := ssh.Dial("tcp", addr, cfg) if err != nil { if strings.Contains(err.Error(), "known_hosts") || strings.Contains(err.Error(), "host key") { - return nil, capturedFingerprint, fmt.Errorf("host key verification failed: %v", err) + return nil, capturedFingerprint, capturedPubKey, fmt.Errorf("host key verification failed: %v", err) } - return nil, capturedFingerprint, fmt.Errorf("connection failed: %v", err) + return nil, capturedFingerprint, capturedPubKey, fmt.Errorf("connection failed: %v", err) } - return conn, capturedFingerprint, nil + return conn, capturedFingerprint, capturedPubKey, nil } func TestSSHConnection(ctx context.Context, host string, port int, user, privKeyPath, knownHostsPath string, strictHostKeyChecking bool) (*ConnResult, error) { - conn, fingerprint, err := dialSSH(ctx, host, port, user, privKeyPath, knownHostsPath, strictHostKeyChecking) + conn, fingerprint, _, err := dialSSH(ctx, host, port, user, privKeyPath, knownHostsPath, strictHostKeyChecking) if err != nil { return &ConnResult{ Success: false, @@ -113,3 +115,7 @@ func TestSSHConnection(ctx context.Context, host string, port int, user, privKey Fingerprint: fingerprint, }, nil } + +func ConnectForApproval(ctx context.Context, host string, port int, user, privKeyPath, knownHostsPath string) (*ssh.Client, string, ssh.PublicKey, error) { + return dialSSH(ctx, host, port, user, privKeyPath, knownHostsPath, false) +}