いろいろ備忘録日記

主に .NET とか Go とか Python絡みのメモを公開しています。

Goメモ-93 (go で SSH接続メモ (1))

概要

以下、自分用のメモです。忘れない内にメモメモ。

GoでSSH接続してコマンド実行して結果を受け取りたいなと思って、ちょっとサンプル作りました。

GoでSSH接続する場合、以下のライブラリを利用するのが多いみたいですね。

pkg.go.dev

ライブラリのインストール

いつもの如く、 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)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

pkg.go.dev

zaiste.net

medium.com


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com