package sshmanager import ( "bytes" "context" "fmt" "net" "os" "path/filepath" "strings" "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, serverPubKey string, host string, port int, user string, keys []DeployKey, knownHostsHost string, addServerPubKey bool) (*DeployResult, error) { result := &DeployResult{Success: true} 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 { return nil, fmt.Errorf("connecting to %s: %w", addr, err) } 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 cmd := fmt.Sprintf("cat > %s && chmod 0%o %s", k.RemotePath, mode, k.RemotePath) 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 continue } 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 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 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 knownHostsHost != "" { session2, err := conn.NewSession() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("session for ssh-keyscan: %v", err)) result.Success = false } else { session2.Stdout = &stdout session2.Stderr = &stderr err := session2.Run(fmt.Sprintf("ssh-keyscan -H -p %d %s 2>/dev/null >> %s/known_hosts", port, knownHostsHost, remoteSSHDir)) session2.Close() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("ssh-keyscan %s: %v (stderr: %s)", knownHostsHost, err, stderr.String())) result.Success = false } else { result.Messages = append(result.Messages, fmt.Sprintf("Populated known_hosts with %s:%d", knownHostsHost, port)) } } } if addServerPubKey && serverPubKey != "" { session3, err := conn.NewSession() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("session for authorized_keys: %v", err)) result.Success = false } else { session3.Stdout = &stdout session3.Stderr = &stderr pubKeyClean := strings.TrimSpace(serverPubKey) err := session3.Run(fmt.Sprintf("mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '%s' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys", pubKeyClean)) session3.Close() if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("adding to authorized_keys: %v (stderr: %s)", err, stderr.String())) result.Success = false } else { result.Messages = append(result.Messages, fmt.Sprintf("Added server public key to %s@%s:~/.ssh/authorized_keys", user, host)) } } } return result, nil }