概要
以下、自分用のメモです。忘れない内にメモメモ。
GoでSSH接続してコマンド実行して結果を受け取りたいなと思って、ちょっとサンプル作りました。
GoでSSH接続する場合、以下のライブラリを利用するのが多いみたいですね。
ライブラリのインストール
いつもの如く、 go get
で。
$ go get -u -v golang.org/x/crypto/ssh $
SSH接続する前準備
今回は、パスワード認証と鍵認証の両方のサンプル作りたいので、鍵認証用の準備。
大抵、最初に一回やってしまって、その後時間が経過して忘れてしまうことが多いので、ついでにここにメモ。
キー生成
鍵のパスワードを入れるプロンプトで表示されるけど、今回はパス無しで作る。 パスワード付きの場合のサンプルは後日。
$ ssh-keygen -b 4096 Generating public/private rsa key pair. Enter file in which to save the key (/home/xxxx/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/xxxx/.ssh/id_rsa Your public key has been saved in /home/xxxx/.ssh/id_rsa.pub ...snip...
キーをリモートにコピー
$ ssh-copy-id user@remote-host /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/xxxx/.ssh/id_rsa.pub" /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'user@remote-host'" and check to make sure that only the key(s) you wanted were added.
鍵認証でログインできるか確認
$ ssh user@remote-host $
リモートサーバの公開鍵を確認
.ssh/known_hosts
見てもいいと思うけど、一応コマンドも記載。
Goのコード内で、公開鍵を検証する場合はこの値を利用します。
$ ssh-keyscan -4 -t ecdsa remote-host xxx.xxx.xxx.xxx ecdsa-sha2-nistp256 ABCDE.....
サンプル
以下の4パターンについて、サンプル書いています。
秘密鍵にパスフレーズをつけている場合のサンプルは次で。
以下のサンプルですが、処理に必要な情報を環境変数から取得するようにしています。
動かす場合は以下を設定しておいてください。
- $SSH_USER
- SSH接続するユーザ名
- $SSH_PASS
- SSH接続するパスワード
- $SSH_HOST
- 接続先を
xxx.xxx.xxx.xxx:ポート番号
の形式で。
- 接続先を
- $HOST_KEY
- リモートサーバの公開鍵 (上の ssh-keyscan の結果)
各関数は、SSH接続したら、cat /etc/os-release | head -n 2
を実行して終わります。
package main import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "golang.org/x/crypto/ssh" ) type ( // returnCode -- 処理結果 returnCode int ) const ( success returnCode = iota // 成功 homedirNotFound // $HOME が展開出来なかった readErrSSHPrivateKey // 秘密鍵読み取り中にエラー parseErrSSHPrivateKey // 秘密鍵の解析中にエラー connErrSSHClient // SSH接続中にエラー canNotCreateNewSSHSession // SSHにてセッションを生成中にエラー execErrInSSHSession // SSHにてコマンドを実行中にエラー ) const ( command = "cat /etc/os-release | head -n 2" ) // 環境変数より取得する情報 var ( sshUser string // SSH ユーザ名 sshPass string // SSH パスワード sshHost string // SSH リモートホスト (xxx.xxx.xxx.xxx:port) hostKey string // リモートホストの公開鍵 (ssh-keyscan の 結果)(e.g: xxx.xxx.xxx.xxx ecdsa-sha2-nistp256 xxxxxxxxxxx) ) func init() { sshUser = os.ExpandEnv("$SSH_USER") sshPass = os.ExpandEnv("$SSH_PASS") sshHost = os.ExpandEnv("$SSH_HOST") hostKey = os.ExpandEnv("$HOST_KEY") } func main() { if sshUser == "" { log.Fatal("$SSH_USER が 設定されていません.") } if sshPass == "" { log.Fatal("$SSH_PASS が 設定されていません.") } if sshHost == "" { log.Fatal("$SSH_HOST が 設定されていません.") } if hostKey == "" { log.Fatal("$HOST_KEY が 設定されていません.") } os.Exit(int(run())) } func run() returnCode { // ------------------------------------------------------------- // GO で ssh を扱う場合 golang.org/x/crypto/ssh を使う // // 標準パッケージには入っていないので利用する場合は go get する. // $ go get -v -u golang.org/x/crypto/ssh // // SSH で接続する場合、大きく分けて // 1. パスワード認証 // 2. 鍵認証 // の2つがある。 // // また、リモートサーバの 公開鍵 を // 1. 検証する // 2. 検証しない // の2つがある。 // // なお、今回は ssh-keygen で パスワード無しの秘密鍵を作ったとする。 // 秘密鍵にパスワードを付与している場合の処理は別のサンプルにて。 // // 以下、それぞれのケースで同じことをしている。 // ------------------------------------------------------------- // ------------------------------------------------------------- // (1) SSH 接続してコマンド実行 // - パスワード認証 // - リモートサーバの公開鍵を検証しない // ------------------------------------------------------------- fmt.Println("RUN (1)") if ret := sshWithPasswordWithInsecureHostKey(); ret != success { return ret } // ------------------------------------------------------------- // (2) SSH 接続してコマンド実行 // - パスワード認証 // - リモートサーバの公開鍵を検証する // ------------------------------------------------------------- fmt.Println("RUN (2)") if ret := sshWithPasswordWithFixedHostKey(); ret != success { return ret } // ------------------------------------------------------------- // (3) SSH 接続してコマンド実行 // - 鍵認証 // - リモートサーバの公開鍵を検証しない // ------------------------------------------------------------- fmt.Println("RUN (3)") if ret := sshWithKeyFileWithInsecureHostKey(); ret != success { return ret } // ------------------------------------------------------------- // (4) SSH 接続してコマンド実行 // - 鍵認証 // - リモートサーバの公開鍵を検証する // ------------------------------------------------------------- fmt.Println("RUN (4)") if ret := sshWithKeyFileWithFixedHostKey(); ret != success { return ret } return 0 } // (1) SSH 接続してコマンド実行 // - パスワード認証 // - リモートサーバの公開鍵を検証しない func sshWithPasswordWithInsecureHostKey() returnCode { // ------------------------------------------- // SSH の 接続設定 を構築 // config := &ssh.ClientConfig{ // SSH ユーザ名 User: sshUser, // 認証方式 Auth: []ssh.AuthMethod{ // パスワード認証 ssh.Password(sshPass), }, // リモートサーバの公開鍵を検証しない HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // ------------------------------------------- // SSH で 接続 // conn, err := ssh.Dial("tcp", sshHost, config) if err != nil { log.Println(err) return connErrSSHClient } // ------------------------------------------- // セッションを開いて、コマンドを実行 // sess, err := conn.NewSession() if err != nil { log.Println(err) return canNotCreateNewSSHSession } defer sess.Close() // リモートサーバでのコマンド実行結果をローカルの標準出力と標準エラーへ流す sess.Stdout = os.Stdout sess.Stderr = os.Stderr if err = sess.Run(command); err != nil { log.Println(err) return execErrInSSHSession } return success } // (2) SSH 接続してコマンド実行 // - パスワード認証 // - リモートサーバの公開鍵を検証する func sshWithPasswordWithFixedHostKey() returnCode { // ------------------------------------------- // リモートサーバ の 公開鍵 を得る // _, _, pubKey, _, _, _ := ssh.ParseKnownHosts([]byte(hostKey)) // ------------------------------------------- // SSH の 接続設定 を構築 // config := &ssh.ClientConfig{ // SSH ユーザ名 User: sshUser, // 認証方式 Auth: []ssh.AuthMethod{ // パスワード認証 ssh.Password(sshPass), }, // リモートサーバの公開鍵を検証 HostKeyCallback: ssh.FixedHostKey(pubKey), } // ------------------------------------------- // SSH で 接続 // conn, err := ssh.Dial("tcp", sshHost, config) if err != nil { log.Println(err) return connErrSSHClient } // ------------------------------------------- // セッションを開いて、コマンドを実行 // sess, err := conn.NewSession() if err != nil { log.Println(err) return canNotCreateNewSSHSession } defer sess.Close() // リモートサーバでのコマンド実行結果をローカルの標準出力と標準エラーへ流す sess.Stdout = os.Stdout sess.Stderr = os.Stderr if err = sess.Run(command); err != nil { log.Println(err) return execErrInSSHSession } return success } // (3) SSH 接続してコマンド実行 // - 鍵認証 // - リモートサーバの公開鍵を検証しない func sshWithKeyFileWithInsecureHostKey() returnCode { // ------------------------------------------- // $HOME/.ssh/id_rsa からデータ読み取り // homeDir, err := os.UserHomeDir() if err != nil { log.Println(err) return homedirNotFound } sshPrivKeyFile := filepath.Join(homeDir, ".ssh/id_rsa") privKey, err := ioutil.ReadFile(sshPrivKeyFile) if err != nil { log.Println(err) return readErrSSHPrivateKey } // ------------------------------------------- // 秘密鍵を渡して Signer を取得 // signer, err := ssh.ParsePrivateKey(privKey) if err != nil { log.Println(err) return parseErrSSHPrivateKey } // ------------------------------------------- // SSH の 接続設定 を構築 // config := &ssh.ClientConfig{ // SSH ユーザ名 User: sshUser, // 認証方式 Auth: []ssh.AuthMethod{ // 鍵認証 ssh.PublicKeys(signer), }, // リモートサーバの公開鍵を検証しない HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // ------------------------------------------- // SSH で 接続 // conn, err := ssh.Dial("tcp", sshHost, config) if err != nil { log.Println(err) return connErrSSHClient } // ------------------------------------------- // セッションを開いて、コマンドを実行 // sess, err := conn.NewSession() if err != nil { log.Println(err) return canNotCreateNewSSHSession } defer sess.Close() // リモートサーバでのコマンド実行結果をローカルの標準出力と標準エラーへ流す sess.Stdout = os.Stdout sess.Stderr = os.Stderr if err = sess.Run(command); err != nil { log.Println(err) return execErrInSSHSession } return success } // (4) SSH 接続してコマンド実行 // - 鍵認証 // - リモートサーバの公開鍵を検証する func sshWithKeyFileWithFixedHostKey() returnCode { // ------------------------------------------- // $HOME/.ssh/id_rsa からデータ読み取り // homeDir, err := os.UserHomeDir() if err != nil { log.Println(err) return homedirNotFound } sshPrivKeyFile := filepath.Join(homeDir, ".ssh/id_rsa") privKey, err := ioutil.ReadFile(sshPrivKeyFile) if err != nil { log.Println(err) return readErrSSHPrivateKey } // ------------------------------------------- // 秘密鍵を渡して Signer を取得 // signer, err := ssh.ParsePrivateKey(privKey) if err != nil { log.Println(err) return parseErrSSHPrivateKey } // ------------------------------------------- // リモートサーバ の 公開鍵 を得る // _, _, pubKey, _, _, _ := ssh.ParseKnownHosts([]byte(hostKey)) // ------------------------------------------- // SSH の 接続設定 を構築 // config := &ssh.ClientConfig{ // SSH ユーザ名 User: sshUser, // 認証方式 Auth: []ssh.AuthMethod{ // 鍵認証 ssh.PublicKeys(signer), }, // リモートサーバの公開鍵を検証 HostKeyCallback: ssh.FixedHostKey(pubKey), } // ------------------------------------------- // SSH で 接続 // conn, err := ssh.Dial("tcp", sshHost, config) if err != nil { log.Println(err) return connErrSSHClient } // ------------------------------------------- // セッションを開いて、コマンドを実行 // sess, err := conn.NewSession() if err != nil { log.Println(err) return canNotCreateNewSSHSession } defer sess.Close() // リモートサーバでのコマンド実行結果をローカルの標準出力と標準エラーへ流す sess.Stdout = os.Stdout sess.Stderr = os.Stderr if err = sess.Run(command); err != nil { log.Println(err) return execErrInSSHSession } return success }
基本的に、すごくモジュラーな作りになっていて、最終的に ssh.ClientConfig
を作ってしまえば、後はどの形式でも同じです。
パスワード認証の場合は, AuthMethodに ssh.Password(password)
を、鍵認証の場合は ssh.PublicKeys(signer)
を渡します。
公開鍵の検証は、検証しない場合は ssh.InsecureIgnoreHostKey()
を、検証する場合は ssh.FixedHostKey(pubKey)
を渡します。
秘密鍵のパスフレーズが存在する場合は、ssh.ParsePrivateKey(privKey)
の部分を ssh.ParsePrivateKeyWithPassphrase(privKey, passPhase)
に変えます。
実行すると以下のようになります。
$ go run . RUN (1) NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" RUN (2) NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" RUN (3) NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)" RUN (4) NAME="Ubuntu" VERSION="20.04 LTS (Focal Fossa)"
参考情報

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者:Alan A.A. Donovan,Brian W. Kernighan
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)

Go Programming for Network Operations: A Golang Network Automation Handbook (English Edition)
- 作者:McAllen, Tom
- 発売日: 2018/10/21
- メディア: Kindle版
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場