いろいろ備忘録日記

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

Goメモ-94 (go で SSH接続メモ (2))

概要

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

前回の続き。今回は秘密鍵にパスフレーズを付与している場合のコードです。

前回の記事は以下です。

devlights.hatenablog.com

サンプル

以下のサンプルですが、処理に必要な情報を環境変数から取得するようにしています。

動かす場合は以下を設定しておいてください。

  • $SSH_USER
    • SSH接続するユーザ名
  • $SSH_PASS
    • 秘密鍵のパスワード
  • $SSH_HOST
    • 接続先を xxx.xxx.xxx.xxx:ポート番号 の形式で。
  • $HOST_KEY
    • リモートサーバの公開鍵 (上の ssh-keyscan の結果)

SSH接続したら、cat /etc/os-release | head -n 2を実行して終わります。

package main

import (
    "io/ioutil"
    "os"
    "path/filepath"

    "github.com/devlights/gomy/output"
    "golang.org/x/crypto/ssh"
)

type (
    // returnCode -- 処理結果
    returnCode int
)

const (
    success                   returnCode = iota // 成功
    homedirNotFound                             // $HOME が展開出来なかった
    readErrSSHPrivateKey                        // 秘密鍵読み取り中にエラー
    parseErrSSHPrivateKey                       // 秘密鍵の解析中にエラー
    parseErrPublicKey                           // 公開鍵の解析中にエラー
    connErrSSHClient                            // SSH接続中にエラー
    canNotCreateNewSSHSession                   // SSHにてセッションを生成中にエラー
    execErrInSSHSession                         // SSHにてコマンドを実行中にエラー
)

const (
    command = "cat /etc/os-release | head -n 2"
)

// 環境変数より取得する情報
var (
    sshUser string // SSH ユーザ名
    sshPass string // 秘密鍵のパスワード
    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 == "" {
        output.Stderrl("[error]", "$SSH_USER が 設定されていません.")
    }

    if sshPass == "" {
        output.Stderrl("[error]", "$SSH_PASS が 設定されていません.")
    }

    if sshHost == "" {
        output.Stderrl("[error]", "$SSH_HOST が 設定されていません.")
    }

    if hostKey == "" {
        output.Stderrl("[error]", "$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 で パスワード付きの秘密鍵を作ったとする。
    // -------------------------------------------------------------

    // -------------------------------------------------------------
    // SSH 接続してコマンド実行
    //   - 鍵認証
    //   - リモートサーバの公開鍵を検証する
    // -------------------------------------------------------------

    // -------------------------------------------
    // $HOME/.ssh/id_rsa からデータ読み取り
    //
    homeDir, err := os.UserHomeDir()
    if err != nil {
        output.Stderrl("[error]", err)
        return homedirNotFound
    }

    sshPrivKeyFile := filepath.Join(homeDir, ".ssh/id_rsa")
    privKey, err := ioutil.ReadFile(sshPrivKeyFile)
    if err != nil {
        output.Stderrl("[error]", err)
        return readErrSSHPrivateKey
    }

    // -------------------------------------------
    // 秘密鍵を渡して Signer を取得
    //
    signer, err := ssh.ParsePrivateKeyWithPassphrase(privKey, []byte(sshPass))
    if err != nil {
        output.Stderrl("[error]", err)
        return parseErrSSHPrivateKey
    }

    // -------------------------------------------
    // リモートサーバ の 公開鍵 を得る
    //
    _, _, pubKey, _, _, err := ssh.ParseKnownHosts([]byte(hostKey))
    if err != nil {
        output.Stderrl("[error]", err)
        return parseErrPublicKey
    }

    // -------------------------------------------
    // 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 {
        output.Stderrl("[error]", err)
        return connErrSSHClient
    }

    // -------------------------------------------
    // セッションを開いて、コマンドを実行
    //
    sess, err := conn.NewSession()
    if err != nil {
        output.Stderrl("[error]", err)
        return canNotCreateNewSSHSession
    }
    defer sess.Close()

    // リモートサーバでのコマンド実行結果をローカルの標準出力と標準エラーへ流す
    sess.Stdout = os.Stdout
    sess.Stderr = os.Stderr

    if err = sess.Run(command); err != nil {
        output.Stderrl("[error]", err)
        return execErrInSSHSession
    }

    return 0
}

秘密鍵のパスフレーズが存在する場合は、 ssh.ParsePrivateKeyWithPassphrase(privKey, passPhase) を使います。

実行すると以下のようになります。

$ go run .
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