LoginSignup
39
41

More than 5 years have passed since last update.

多段SSH中のパスワード入力を自動化する (透過的なコマンド実行にも対応)

Last updated at Posted at 2015-07-10

はじめに

踏み台における公開鍵認証で十分なセキュリティが確保されていて、リモートがパスワード認証になる場合があります。今回は更に踏み台に下記のような制限が設けられているとします。

command="ssh ..." 踏み台接続後に自動的にリモートに接続する
no-port-forwarding ポートフォワーディングを無効化する

その状況下で今回実現したいのは下記のアクションです。

  • パスワードの自動入力
  • 透過的なコマンドの実行

【解決策1】 expectをフル活用する

expectコマンドはユーザの入力を仮想的に再現することが出来るツールです。自分で「ssh パスワード 自動化」とかでググって最初に辿り着いたのがこの方法だったので、更に利便性を高めるために色々手を加えてみました…

ソースコード

  • 標準の ssh コマンドをオーバーライドします。

  • .bashrc.zshrc に直接書けるように1つの関数で定義しています。但し、シェル非標準のプロセス置換機能を使っていることに注意してください。全てのシェルで動作が保証されるわけではありません。

  • 特殊な環境かどうかの判定は (~/.ssh/config で設定されている)特殊なホスト名がsshの引数群に正規表現でマッチするかどうかで判定しています。今回は簡易的に対象を1つのみに絞り、ハードコーディングすることにしています。

  • パスワードも同様に1つのみに絞り、ハードコーディングすることにしています。

# sshコマンドをオーバーライドする
ssh() {

    # 設定
    local password="PASSWORD"
    local specialhost="FUMIDAI"

    if [[ "$@" =~ $specialhost ]]; then # 処理を適用する場合

        # sshの引数とサブコマンドの引数を分離する
        local sshargs
        sshargs=("ssh" "-t")
        local reach=false
        while (( $# > 0 )); do
            if [ "--" = "$1" ]; then
                shift
                break
            elif [[ "$1" =~ ^-[bcDEeFIiLlmOopQRSWw]+$ ]]; then
                sshargs=("${sshargs[@]}" "$1")
                shift
                if (( $# > 0 )); then
                    sshargs=("${sshargs[@]}" "$1")
                    shift
                fi
            elif [[ "$1" =~ ^-.*$ ]]; then
                sshargs=("${sshargs[@]}" "$1")
                shift
            elif ! $reach; then
                sshargs=("${sshargs[@]}" "$1")
                reach=true
                shift
            else
                break
            fi
        done

        if (( $# == 0 )); then # サブコマンドがない場合は通常通りシェルを起動

            # Expectでパスワードを自動入力
            expect <(echo '
                set timeout -1
                set password [lindex $argv 0]
                set sshargs [lrange $argv 1 end]
                proc are_you_sure {} {
                    send [gets stdin]
                    send "\n"
                    expect "password:" password_input "Please type" are_you_sure
                }
                proc password_input {} {
                    global password
                    send $password
                    send "\n"
                    expect "password:" password_input "Last login:" interact
                }
                eval spawn -noecho command $sshargs
                expect "Are you sure" are_you_sure "password:" password_input "Last login:" interact
            ') $password "${sshargs[@]}"

        else #サブコマンドがある場合は標準出力のゴミを取り除く

            # BASE6464デコードにはオプション2通りあるのでタイプを確認
            echo "" | base64 -D &>/dev/null
            local base64=$?
            [ "$base64" = "0" ] && base64="-D" || base64="-d"

            # バウンダリ生成
            local boundary=$(mktemp -u boundary---------XXXXXXXXXXXXXXXXXXXXXXXXXX)

            # Expectでパスワードを自動入力
            expect <(echo '
                set timeout -1
                set password [lindex $argv 0]
                set boundary [lindex $argv 2]
                set bashargs [lrange $argv 3 [expr 3 + [lindex $argv 1] - 1]]
                set sshargs [lrange $argv [expr 3 + [lindex $argv 1]] end]
                set boundary_count 0
                proc are_you_sure {} {
                    send [gets stdin]
                    send "\n"
                    expect "password:" password_input "Please type" are_you_sure
                }
                proc password_input {} {
                    global password
                    send $password
                    send "\n"
                    expect "password:" password_input "Last login:" command_input
                }
                proc command_input {} {
                    global boundary
                    global bashargs
                    global eb
                    expect_background "$boundary---" observe_boundary
                    set eb "echo \"$boundary\"\"---\""
                    set en "echo \"\""
                    set args [join $bashargs]
                    send "$eb && ( $args | base64 ) 2>/dev/null && $eb && $en && $en\n"
                    interact
                }
                proc observe_boundary {} {
                    global boundary
                    global boundary_count
                    if { $boundary_count < 1 } {
                        incr boundary_count
                        expect_background "$boundary---" observe_boundary
                    } else {
                        exit 0
                    }
                }
                eval spawn -noecho command $sshargs
                expect "Are you sure" are_you_sure "password:" password_input "Last login:" command_input
            ') $password $# $boundary "$@" "${sshargs[@]}" |

            # バウンダリで囲まれた部分だけを抽出
            awk '
                BEGIN {
                    count=0
                    skip=0
                }
                $0 ~ "'$boundary'---" {
                    ++count
                    skip=1
                }
                count==1 {
                    if (skip != 1) {
                        print $0
                    } else {
                        skip=0
                    }
                }
            ' |

            # BASE64デコードする
            base64 $base64

        fi

    else; # 処理を適用しない場合

        # 本来のシェルコマンドをそのまま実行
        command ssh "$@"

    fi

}

利用例

リモートでシェルを起動する

特に意識せず普段通り ssh を実行するだけです。パスワードが自動入力されます。

local@localhost:~$ ssh FUMIDAI

リモートでコマンドを実行する

ローカルとのデータ通信を必要としない場合

これも特に気に留めることはありません。set timeout -1 を最初に実行してあるので nohup もいらないと思います。

local@localhost:~$ ssh FUMIDAI ls -A
local@localhost:~$ ssh FUMIDAI brew upgrade --all

ローカルとのデータ通信を必要とする場合

要注意です。とんでもなく鈍速でしか通信できないので実用性はあまりありません…というかデータの欠落が激しいので…

3500バイト程度ならスリープのみでOK
local@localhost:~$ (sleep 3 && cat example.txt) | ssh FUMIDAI 'cat > /tmp/example.txt'
3500バイトを超えるならPipeViewerを使ってフロー制御
local@localhost:~$ (sleep 3 && cat example.txt) | pv -L 3500 | ssh FUMIDAI 'cat > /tmp/example.txt'
バイナリデータを含む場合は更にBASE64エンコードが必要
local@localhost:~$ (sleep 3 && tar -zcf - .) | base64 | pv -L 3500 | ssh FUMIDAI 'base64 -d | tar -xf - -C /tmp'

【解決策2】 ssh-agent の AgentForwarding を利用する

後から便利な方法を教えていただいたのでこちらもまとめておきます。パスワード認証を本質的にスキップすることができ、間にexpectを挟む必要が無くなります!

1. ローカルのssh-agentに秘密鍵を登録する

踏み台用と同じ秘密鍵をssh-agentに登録します。ssh-agentはファイルではなくメモリ上のプロセスなので再起動ごとに行う必要がありますが、この処理は自動化出来ます。

~/.bashrc ~/.zshrc
if [ -z `pgrep ssh-agent` ]; then 
    eval `ssh-agent` 1>/dev/null &&
    ssh-add ~/.ssh/id_rsa 2>/dev/null
fi

2. リモートのauthorized_keysに公開鍵を登録する

一度パスワード認証で入っておき、~/.ssh/authorized_keys に上記の秘密鍵に対応する公開鍵を追記します。こちらはファイルなので1回きりで済みます。

  • ~/.ssh はパーミッション0700が必要です。
  • ~/.ssh/authorized_keys はパーミッション0600が必要です。
  • >>> を間違えないように! (既に登録されているものが消えてしまう)
local@localhost:~$ cat ~/.ssh/id_rsa.pub
出てきたやつをコピー
local@localhost:~$ ssh FUMIDAI
パスワード入力してログイン
example@example.com:~$ chmod 0700 ~/.ssh
example@example.com:~$ chmod 0600 ~/.ssh/authorized_keys
example@example.com:~$ cat >> ~/.ssh/authorized_keys
ペースト ⇛ 改行(必須!) ⇛ Ctrl-D
example@example.com:~$ logout

3. ssh 利用時にオプションに -A を付加する

local@localhost:~$ ssh -A FUMIDAI
パスワード無しに一発ログイン完了!
example@example.com:~$ 

毎回このオプションを付けるのが面倒、またsshではなくscpで使いたい場合なども考慮して、~/.ssh/configForwardAgent yes を書いておいたほうがいいかもしれません。

~/.ssh/config
Host FUMIDAI
    HostName example.com
    ForwardAgent yes

追記

やっぱりssh-agentダメだった

  • scp だとフォワーディングしてくれない…何故だ…
  • ssh でログインした後に実行して欲しいサブコマンド渡しても実行が始まらない…しかも標準出力にログインメッセージとか出てきてるし…これもうわかんねぇな…

まだ最初のやつのほうがマシですね。

39
41
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
41