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

# はじめに

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


<table><tr><th width="200"><code>command="ssh ..."</code></th><th>踏み台接続後に自動的にリモートに接続する</th></tr><tr><th width="200"><code>no-port-forwarding</code></th><td>ポートフォワーディングを無効化する</td></tr></table>

- [authorized_keys ファイルについて調べてみたら楽しかった．](http://takuya-1st.hatenablog.jp/entry/20110813/1313252992)

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

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

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

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

## ソースコード

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

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

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

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

```bash
# 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` を実行するだけです。パスワードが自動入力されます。

```shell-session
local@localhost:~$ ssh FUMIDAI
```

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

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

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

```shell-session
local@localhost:~$ ssh FUMIDAI ls -A
```

```shell-session
local@localhost:~$ ssh FUMIDAI brew upgrade --all
```

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

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

```bash:3500バイト程度ならスリープのみでOK
local@localhost:~$ (sleep 3 && cat example.txt) | ssh FUMIDAI 'cat > /tmp/example.txt'
```

```bash:3500バイトを超えるならPipeViewerを使ってフロー制御
local@localhost:~$ (sleep 3 && cat example.txt) | pv -L 3500 | ssh FUMIDAI 'cat > /tmp/example.txt'
```

```bash:バイナリデータを含む場合は更にBASE64エンコードが必要
local@localhost:~$ (sleep 3 && tar -zcf - .) | base64 | pv -L 3500 | ssh FUMIDAI 'base64 -d | tar -xf - -C /tmp'
```

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

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

- [ssh-agentの使い方](http://qiita.com/isaoshimizu/items/84ac5a0b1d42b9d355cf)

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

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

```bash:~/.bashrc&#32;~/.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`が必要です。
- **`>` と `>>` を間違えないように！** (既に登録されているものが消えてしまう)

```shell-session
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` を付加する

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

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

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

# 追記

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

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

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