package main import ( "crypto/x509" "encoding/pem" "errors" "fmt" "golang.org/x/crypto/ssh" "io/ioutil" "net" ) type SshClient struct { Config *ssh.ClientConfig Server string } func NewSshClient(user string, host string, port int, privateKeyPath string, privateKeyPassword string) (*SshClient, error) { // read private key file pemBytes, err := ioutil.ReadFile(privateKeyPath) if err != nil { return nil, fmt.Errorf("Reading private key file failed %v", err) } // create signer signer, err := signerFromPem(pemBytes, []byte(privateKeyPassword)) if err != nil { return nil, err } // build SSH client config config := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { // use OpenSSH's known_hosts file if you care about host validation return nil }, } client := &SshClient{ Config: config, Server: fmt.Sprintf("%v:%v", host, port), } return client, nil } // Opens a new SSH connection and runs the specified command // Returns the combined output of stdout and stderr func (s *SshClient) RunCommand(cmd string) (string, error) { // open connection conn, err := ssh.Dial("tcp", s.Server, s.Config) if err != nil { return "", fmt.Errorf("Dial to %v failed %v", s.Server, err) } defer conn.Close() // open session session, err := conn.NewSession() if err != nil { return "", fmt.Errorf("Create session for %v failed %v", s.Server, err) } defer session.Close() // run command and capture stdout/stderr output, err := session.CombinedOutput(cmd) return fmt.Sprintf("%s", output), err } func signerFromPem(pemBytes []byte, password []byte) (ssh.Signer, error) { // read pem block err := errors.New("Pem decode failed, no key found") pemBlock, _ := pem.Decode(pemBytes) if pemBlock == nil { return nil, err } // handle encrypted key if x509.IsEncryptedPEMBlock(pemBlock) { // decrypt PEM pemBlock.Bytes, err = x509.DecryptPEMBlock(pemBlock, []byte(password)) if err != nil { return nil, fmt.Errorf("Decrypting PEM block failed %v", err) } // get RSA, EC or DSA key key, err := parsePemBlock(pemBlock) if err != nil { return nil, err } // generate signer instance from key signer, err := ssh.NewSignerFromKey(key) if err != nil { return nil, fmt.Errorf("Creating signer from encrypted key failed %v", err) } return signer, nil } else { // generate signer instance from plain key signer, err := ssh.ParsePrivateKey(pemBytes) if err != nil { return nil, fmt.Errorf("Parsing plain private key failed %v", err) } return signer, nil } } func parsePemBlock(block *pem.Block) (interface{}, error) { switch block.Type { case "RSA PRIVATE KEY": key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("Parsing PKCS private key failed %v", err) } else { return key, nil } case "EC PRIVATE KEY": key, err := x509.ParseECPrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("Parsing EC private key failed %v", err) } else { return key, nil } case "DSA PRIVATE KEY": key, err := ssh.ParseDSAPrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("Parsing DSA private key failed %v", err) } else { return key, nil } default: return nil, fmt.Errorf("Parsing private key failed, unsupported key type %q", block.Type) } }