package sshmanager import ( "bytes" "context" "fmt" "log/slog" "net" "os" "path/filepath" "time" "golang.org/x/crypto/ssh" ) type DeployKey struct { LocalPath string RemotePath string Mode uint32 } type DeployResult struct { Success bool Messages []string Errors []string } func DeployKeysToMachine(ctx context.Context, serverKeyPath string, host string, port int, user string, keys []DeployKey, knownHostsHosts []string) (*DeployResult, error) { result := &DeployResult{Success: true, Messages: []string{}, Errors: []string{}} addr := fmt.Sprintf("%s:%d", host, port) keyData, err := os.ReadFile(serverKeyPath) if err != nil { return nil, fmt.Errorf("reading server key: %w", err) } signer, err := ssh.ParsePrivateKey(keyData) if err != nil { return nil, fmt.Errorf("parsing server key: %w", err) } hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil } cfg := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, HostKeyCallback: hostKeyCallback, Timeout: 10 * time.Second, } ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() conn, err := ssh.Dial("tcp", addr, cfg) if err != nil { slog.Warn("deploy keys: SSH dial failed", "host", addr, "error", err) return &DeployResult{Success: false, Messages: []string{}, Errors: []string{fmt.Sprintf("connecting to %s: %v", addr, err)}}, nil } defer conn.Close() remoteSSHDir := "/var/lib/syncserver/ssh" remoteKeysDir := filepath.Join(remoteSSHDir, "keys") session, err := conn.NewSession() if err != nil { return nil, fmt.Errorf("creating session: %w", err) } defer session.Close() var stdout, stderr bytes.Buffer session.Stdout = &stdout session.Stderr = &stderr if err := session.Run(fmt.Sprintf("mkdir -p %s && chmod 700 %s", remoteKeysDir, remoteKeysDir)); err != nil { return nil, fmt.Errorf("creating remote ssh dir: %s %w", stderr.String(), err) } result.Messages = append(result.Messages, fmt.Sprintf("Created %s on %s", remoteKeysDir, host)) for _, k := range keys { keyContent, err := os.ReadFile(k.LocalPath) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("reading local key %s: %v", k.LocalPath, err)) result.Success = false continue } mode := k.Mode if mode == 0 { mode = 0600 } sess2, err := conn.NewSession() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("session for key upload: %v", err)) result.Success = false continue } defer sess2.Close() sess2.Stdout = &stdout sess2.Stderr = &stderr slog.Debug("deploy: uploading key", "local", k.LocalPath, "remote", k.RemotePath, "host", host) cmd := fmt.Sprintf("cat > %s && chmod 0%o %s", k.RemotePath, mode, k.RemotePath) stdin, err := sess2.StdinPipe() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("stdin pipe for %s: %v", k.RemotePath, err)) result.Success = false sess2.Close() continue } if err := sess2.Start(cmd); err != nil { result.Errors = append(result.Errors, fmt.Sprintf("starting command for %s: %v", k.RemotePath, err)) result.Success = false sess2.Close() continue } _, err = stdin.Write(keyContent) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("writing key %s: %v", k.RemotePath, err)) result.Success = false stdin.Close() sess2.Close() continue } stdin.Close() if err := sess2.Wait(); err != nil { result.Errors = append(result.Errors, fmt.Sprintf("uploading %s: %v (stderr: %s)", k.RemotePath, err, stderr.String())) result.Success = false continue } result.Messages = append(result.Messages, fmt.Sprintf("Uploaded %s to %s:%s", filepath.Base(k.LocalPath), host, k.RemotePath)) } if len(knownHostsHosts) > 0 { for _, khHost := range knownHostsHosts { session2, err := conn.NewSession() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("session for ssh-keyscan %s: %v", khHost, err)) continue } session2.Stdout = &stdout session2.Stderr = &stderr err = session2.Run(fmt.Sprintf("ssh-keyscan -H -p %s 2>/dev/null >> %s/known_hosts", khHost, remoteSSHDir)) session2.Close() if err != nil { slog.Warn("deploy: ssh-keyscan failed", "host", khHost, "error", err) result.Errors = append(result.Errors, fmt.Sprintf("ssh-keyscan %s: %v (stderr: %s)", khHost, err, stderr.String())) } else { result.Messages = append(result.Messages, fmt.Sprintf("Populated known_hosts with %s", khHost)) } } } slog.Info("deploy keys result", "host", host, "success", result.Success, "messages", len(result.Messages), "errors", len(result.Errors)) return result, nil }