シェルスクリプトで ssh 秘密鍵のパスワード入力を回避するときは ssh-agent を使うと楽


## 要約
* `ssh` 接続する際に、パスワード入力を補助してくれるツール `expect` と `ssh-agent` を試した。
* `expect`
    * Linuxのプロンプトを待ち受け、画面の出力に応じて文字を入力してくれる。
    * パスワード入力以外でも使える。
    * 時間かかる処理をするとタイムアウトしてしまう。
* `ssh-agent`
    * プロセスが鍵ファイルとパスワード情報を保持し入力が不要になる。

`ssh` パスワードの入力を省略したいだけなら`ssh-agent` をつかうと
タイムアウトや、画面の出力を考慮しなくてよいので楽でした。

#### 2020/07/13 追記：expect の代わりに SSH_ASKPASS を使うとより簡単にパスワード入力を回避できる
コメントで教えていただきました。
`expect` の問題点であった、タイムアウト等も回避できています。
単一スクリプトで簡単に実装するなら `SSH_ASKPASS`
複数スクリプトや複数のサーバをまたいで設定したいなら`ssh-agent`と使い分けるといいと思います。

参考)
[expectやsshpassを使わずにシェルでSSHパスワード認証を自動化する](https://qiita.com/wadahiro/items/977e4f820b4451a2e5e0)


## 参考にした諸先生方の記事
[Linuxの対話がめんどくさい?そんな時こそ自動化だ！-expect編-](https://qiita.com/ine1127/items/cd6bc91174635016db9b)
[ssh-agentの使い方](https://qiita.com/isaoshimizu/items/84ac5a0b1d42b9d355cf)


## 経緯
私は普段プログラマとして働いていますが、サーバでコマンド発行する作業も少なくありません。
特にrpmパッケージのビルド作業には、複数サーバへのログインとコマンド発行が必要です。

***定型作業やりたくない、いちいちコマンド覚えられへんし、めんどくさい…***

そう思い自動化しました。
つまづいて結構時間かかったので記録を残しておきます。


***ここまで調べて、時間かけてスクリプトを書くぐらいならパスワード入力する方が早いんじゃ…***

そう思った人もいらっしゃるでしょう、しかし私はプログラマなのです。

> プログラマという人種は、楽をするためであれば、どんな苦労も厭わない。

かつてビルゲイツが言ったとか言わないとか。
なので仕方ないのです。


## Shell スクリプトを書いて自動化する

### 環境
CentOS 8.0

### 実装
早速発行してるコマンドを並べたシェルスクリプトを作ってみました。

```shell
#!/bin/bash
IP_A='10.0.0.1'
IP_B='10.0.0.2'
USER='login_user_name'
SSH_KEY='./private.key'

ssh ${USER}@${IP_A} -i ${SSH_KEY} sudo buildCommand.sh
ssh ${USER}@${IP_B} -i ${SSH_KEY} sudo buildCommand.sh
```

実行すると

```
# sh test.sh
Enter passphrase for key './private.key': 
Enter passphrase for key './private.key': 
```

当然のことながら、都度 `ssh` パスワードを聞かれる。
***これは自動やない…***
入力の自動化ってどうすんのかなとおもってググると `expect` が使えそうだった。



## expect に挑戦

`expect` はLinuxの対話処理を自動化してくれるコマンドです。
最小構成では入っていないので、パッケージのインストールが必要です。

### 早速実装
```shell
#!/bin/bash
IP_A='10.0.0.1'
IP_B='10.0.0.2'
USER='login_user_name'
SSH_KEY='./private.key'
SSH_PW='P@ssW0rd'

function runExpect () {
    expect -c "
spawn env LANG=C ${*}                                 // コマンドの実行
expect \"Enter passphrase for key \'${SSH_KEY}\':\"   // 待ち受ける出力
send \"${SSH_PW}\n\"                                  // マッチしたときに入力する内容
expect \"$\"
"
}

runExpect ssh ${USER}@${IP_A} -i ${KEYPATH} sudo buildCommand.sh
runExpect ssh ${USER}@${IP_B} -i ${KEYPATH} sudo buildCommand.sh
```

実行すると、簡単な `ls` コマンドなどでは結果を返してくれるのですが、
本来実行したいビルドコマンドを実行すると途中で止まってしまう…

***「動けexpect！なぜ動かん!!」***

あれこれ試しましたが、終ぞ意図した動きをしてくれませんでした…


#### 注意：タイムアウト秒数伸ばせないの？
`timeout` コマンドを発行すると、タイムアウト時間を指定できるようでしたが、
ビルドするファイルごとに実行時間も異なるため、
タイムアウト時間を指定したくなく、使いませんでした。


## ssh-agent に切り替え
途方に暮れ、会社の先輩に相談すると 
「 `ssh` のパスワード入力を省略したいだけなら、`ssh-agent` 使ったら？」と教えてもらいました。

***「ssh-agent！！！」***

`ssh-agent` は`ssh`のパスワード情報を保持してくれるプロセスです。
最小構成でもインストールされています。

### 使い方
以下のコマンドを発行すると、`ssh-agent` プロセスが立ち上がります。

```shell
eval `ssh-agent`
```

プロセス起動後、鍵ファイルを登録します。

```shell
ssh-add ./private.key 
```
パスフレーズを聞かれるので、入力するとその後
`ssh`コマンド実行時に鍵ファイルやパスワードの入力が不要になります。
  
  
使用後は以下のコマンドでプロセスを停止すれば、再度鍵情報が必要になります。

```shell
eval `ssh-agent -k`
```

### スクリプトも少しだけ修正
```shell
#!/bin/bash
IP_A='10.0.0.1'
IP_B='10.0.0.2'
USER='login_user_name'

ssh ${USER}@${IP_A} sudo buildCommand.sh
ssh ${USER}@${IP_B} sudo buildCommand.sh
```

実行すると無事ビルドが成功しました。
`ssh` に加えて `scp` もパスワードの入力が不要です。

~~なんや expect 要らんやんけ~~

#### 注意：なんで eval つけてんの？
`ssh-agent` コマンドを発行すると以下のように、環境変数を設定するためのコマンドを出力します。

```
# ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-JfJ0l4VQtr3E/agent.27715; export SSH_AUTH_SOCK;
SSH_AGENT_PID=27716; export SSH_AGENT_PID;
echo Agent pid 27716;
```
出力されたコマンドをシェルで実行するために実行結果をevalに渡しています。


#### 注意：これ誰でもsshパスなしにならん？
パスなしで実行するためには環境変数 `SSH_AUTH_SOCK` にソケットファイルパスを指定する必要があります。
ソケットファイルは実行ユーザの権限で作成されるため、一般権限同士で参照することはできません。
ソケットファイルパスとプロセスIDを読まれない限りは安全です。


# 蛇足

sshパスなしでガンガンコマンドを発行していたのですが、
毎回プロセス立ち上げて、最初のパスワード入力するのも面倒になってきました。
とはいえプロセス立ち上げっぱなしは、心配…

***よし自動化したろ！***

自動化しようと思いシェルスクリプトを書き始めましたが、
再度、鍵ファイルのパスワード入力を自動化しないといけなくなりました。
そう `expect` の出番です。

主人公のピンチにかつてのライバルが助けに来る胸熱展開！


### 完成版

```shell
#!/bin/bash
IP_A='10.0.0.1'
IP_B='10.0.0.2'
USER='login_user_name'
SSH_KEY='./private.key'
SSH_PW='P@ssW0rd'

eval `ssh-agent`

expect -c "
spawn env LANG=C ssh-add ${SSH_KEY}
expect \"Enter passphrase for \'${SSH_KEY}\':\"
send \"${SSH_PW}\n\"
expect \"$\"
"

ssh ${USER}@${IP_A} sudo buildCommand.sh
ssh ${USER}@${IP_B} sudo buildCommand.sh

eval `ssh-agent -k`
```

***成し遂げたぜ！*** 


#### 注意：パスワードシェル平文で入ってるし、セキュリティ下がってない？

まぁほら、ね？
※ パスワードを平文でシェルスクリプトに書いているので危険です、ご利用は計画的に。

#### 2019/12/05 追記：expect 使うくらいなら、大人しくsshキーのパスフレーズを解除したらどうでしょう 

（ 確かに！問題を解決することに固執するあまり、本来の目的を忘れていた。 ）
（ しかし、それではこの ”蛇足” の存在意義が......！！ ）

パスフレーズを解除した鍵ファイルは、だれでもパスなしで利用できてしまいます、
こちらもご利用は計画的に。

> この指摘は先輩からいただきました、レビューいただきありがとうございました。

# 結論
* `ssh` 鍵情報を自動入力するには `ssh-agent` が使いやすくてよい
* `expect` ももちろん有用なケースがある、特徴を理解して使い分けるのが大事
* 自動化とセキュリティホールは紙一重、気を付けよう！