Linux 作業手順書からべた書きパスワードをなくすシンプルなアイディア

※ 2018/07/21 追記。

お読みいただきましてありがとうございます！

すみません、実はタイトルの割には「現在回しているイケてない運用をちょっとした工夫でサクッと改善する」ことを主眼としており、セキュリティのベストプラクティスではありません。。。
本稿の例で挙げているコマンドのリスクにつきましては、 [注意](#注意) の項を追加しましたのでご参照ください。

公開当初は「10いいねくらいもらえたら嬉しいなー」と思っていた程度だったのですが、まさかここまでたくさんの方に読んでいただけるとは。。。
もっと内容を精査しておくべきだったと反省しております。。。

いいねやコメント等反応頂けるのはとても励みになります。ありがとうございます！

# TL;DR

怖いですよね、セキュリティインシデント。

インフラ系でお仕事をしていると、 Linux にログインして操作する手順書を作る事が多くなります。手順書の中には認証が必要なコマンドを含めなければならない場合もあり、そういった時にはパスワードといったクレデンシャル情報の取り扱いを考える必要性が出てきます。そのような時に最もやってはいけないことは **クレデンシャル情報を手順書にべた書きする** という行為です。
このような行為は

*   手順書の機密性を不必要に上昇させてしまう
*   クレデンシャル情報の更新(例えばアカウントのパスワード変更)の際に漏れなく手順書のべた書き箇所を修正する手間が発生する

という問題をはらむことになります。

そこでよくあるのが、下記のようにコマンドの一部を作業実施時に作業者により読み替えさせる手法です。

```shell-session
$ curl -u user:{パスワード} http://example.com
※ {パスワード} は XXX を参照して読み替えてください。
```

この手法は手順書の機密性を不必要に上昇させませんし、クレデンシャル情報の参照先を統一しておけばクレデンシャル情報の更新時に参照先を修正するだけでよいので、上記の問題をどちらも解決できます。
しかし、これはベストな手法でしょうか？答えは **No** です。

それでは、どういった手法を使えばよいのか？というのが本稿の趣旨です。

# 何が問題か？

読み替え手法には `.bash_history` にクレデンシャル情報が残ってしまう問題があります。
どういうことか見ていきましょう。

## `.bash_history` にクレデンシャル情報が残ってしまう問題

`curl -u user:{パスワード} http://example.com` のように、読み替え手法で手順書を記載しても、実際にコマンドを打つ時にはパスワードをダイレクトに入力する必要があります。

`.bash_history` にどのように記録されるか、実際に読み替え手法でコマンドを入力してみてみましょう。
下記は `{パスワード}` を `password` に読み替えた場合の例です。

```shell-session
$ curl -u user:password http://example.com
```

いったんログアウトしてから再度ログインして `~/.bash_history` を見てみると次のように記録されています。

```~/.bash_history
curl -u user:password http://example.com
```

はい、当たり前ですね。普通に `password` という文字列が含まれた状態で記録されてしまっています。

実際のところ、普通は `.bash_history` はオーナーにしか読み書き権限がないので第三者に読み取られる可能性は低いのですが、クレデンシャル情報が平文で保存されてしまうのはいざというときに問題になりかねません。

## コマンドをコピペしにくい問題

これは個人的に感じる問題です。

前出の例の `curl` コマンドであれば、

1.  `curl -u user:` までをコピペ
2.  `{パスワード}` に入れるべき実際のパスワードを入力
3.  パスワード以降の ` http://example.com` をコピペ(`http://example.com` の前の半角スペースも含む)

という手順を踏む必要があります。これは意外とストレスです。
更に、たった3ステップではありますが、同じことをいろいろな人間にやらせると必ずミスる人間がでてきます。[^1]

[^1]: 症状には個人差があります。

# ではどのようにするべきか？

次のように `read` コマンドを利用すれば解決できます。

```shell-session
$ read -sp "Please input your password: " __pass; echo
※ パスワード は XXX を参照して入力してください。
$ curl -u "user:${__pass}" http://example.com
```
シェルスクリプトでパスワードの入力を求めるときに `read` コマンドを使ったことのある方は多いかもしれません。そうです、パスワードを入力するためにその `read` コマンドを手順書に記載するのです。

※ これは bash の `read` の場合です。 zsh のでは動作しません。 zsh の場合は [zshでも作業手順書からべた書きパスワードをなくしたい！！](https://qiita.com/serinuntius/items/58607f82aaaa349ca3c7) をご参照ください(@serinuntius さんありがとうございます！)。

## `read` コマンドについて軽くおさらい

`read` コマンドを実行すると、キーボードからの入力待ち状態になります。オプション等の意味や効果は次の通り。

*   `-p` オプションでメッセージが指定されていれば、入力待ち状態になる前にそのメッセージを表示します。上記の例ですと `Please input your password: ` を表示します。
*   `-s` オプションが指定されていれば、キーボードから入力した内容がエコーバックされません。[^2] Linux ログイン時にパスワードを入力しても何も画面に表示されないのと同じです。
*   `__pass` は入力された文字列を代入する変数名です。
*   最後の `; echo` は改行を出力します。 `read` コマンドは最後に改行を出力しないので、このようにしておかないとプロンプトが現在の行に出力されてしまい不格好になってしまいます。

[^2]: エコーバックされないことでショルダーハック対策となり、 `script` コマンド等で作業ログを取っているような場合にもクレデンシャル情報が記録されないメリットがあります。

実際に `read` コマンドを実行すると次のようになります。入力した値を `echo` コマンドで表示させています。

```shell-session
$ read -sp "Please input your password: " __pass; echo
Please input your password: 
$ echo "$__pass"
mypassword
```

## `.bash_history` にクレデンシャル情報が残らない

`.bash_history` にどのように記録されるか、 `read` コマンド手法のコマンドを入力してみてみましょう。

```shell-session
$ read -sp "Please input your password: " __pass; echo
$ curl -u "user:${__pass}" http://example.com
```

いったんログアウトしてから再度ログインして `~/.bash_history` を見てみると次のように記録されています。

```~/.bash_history
read -sp "Please input your password: " __pass; echo
curl -u "user:${__pass}" http://example.com
```

`read` コマンドと `curl` コマンドの履歴が残りますが、パスワード部分に指定した環境変数が環境変数のまま記録されており、実際に入力されたパスワードが何なのかはコマンド履歴から隠蔽することに成功しています。

`read` コマンド手法はクレデンシャル情報が `.bash_history` に記録されるのを見事に防いでいますね。

## コマンドのコピペが楽

`read` コマンド手法ではどのようにコマンドをコピペするのでしょうか。

```shell-session
$ read -sp "Please input your password: " __pass; echo
※ パスワード は XXX を参照して入力してください。
$ curl -u "user:${__pass}" http://example.com
```

この手順の場合、実際にコマンドを打つ時の流れは

1.  `read -sp "Please input your password: " __pass; echo` を1行まるっとコピペ。
2.  パスワードを入力。
3.  `curl -u "user:${__pass}" http://example.com` を1行まるっとコピペ。

となります。先に出した例と同じ3ステップですね。読み替え手法と比べて何が良くなったのでしょうか？

...答えはコピペの粒度です。先に出した例だと、1行の中で途中まで/途中からコピペする必要があったのに対して、こちらの例だと1行まるっとコピペするだけになり、ヒューマンエラーの低減を期待できます。[^3]

[^3]: あくまで個人の感想で、効果には個人差があります。

# 注意

## 例示している方法はセキュリティ的には完璧ではない

`read` コマンド手法であれば、クレデンシャル情報がファイルに保存されてしまうことを防ぐことができます。
しかし、本稿で例示しているような「クレデンシャル情報を引数でコマンドに渡す」というやり方では、サーバにログイン済みの第三者にクレデンシャル情報を盗み見られてしまうリスクがあります。

どういうことか、本稿で何度も例示している下記のコマンドについて考えてみます。

```shell-session
$ read -sp "Please input your password: " __pass; echo
$ curl -u "user:${__pass}" http://example.com
```
この場合、実際に `curl` コマンドを実行する時には環境変数等がシェル展開され下記のコマンドが実行されます。

```
curl -u user:password http://example.com
```
このコマンドが実行されている間に、サーバにログイン済みの第三者が `ps` コマンドを叩いたらどうなるでしょうか？
引数部分に(シェル展開後の)クレデンシャル情報を含むプロセスのリストが表示されてしまうはずです。

このように、引数でクレデンシャル情報を渡すという事自体にセキュリティ的なリスクがあります。

## ただ、そのリスクを軽減しているコマンドもある

但し、 `ps` コマンドからクレデンシャル情報を隠匿するように実装されているコマンドもあります。
実は `curl` コマンドも引数からクレデンシャル情報を隠匿するように実装されており、コマンド実行中に `ps` コマンドを叩いても下記のようにクレデンシャル情報を隠匿しています。

```shell-session
$ curl -u user:password http://192.0.2.1/ &
[1] 22032
$ ps 22032
  PID TTY      STAT   TIME COMMAND
22032 pts/0    S      0:00 curl -u               http://192.0.2.1/
```
ただ、これも完ぺきではありません。
なぜなら、 **クレデンシャル情報を隠匿するのはプロセス内の処理であり、プロセス起動～隠匿処理が完了するまでの間はクレデンシャル情報を盗み見られてしまう恐れがある** 為です。

あくまでリスクの **軽減(≠解消)** と考えるべきです。

## よりセキュアな方法は？

このようなリスクを回避または軽減するためには、

*   後述する `mysql` コマンドのようにコマンドの機能でインタラクティブにパスワードを入力する(多分たいていの場合は `read` コマンド手法は不可)
*   パスワードを環境変数で直接受け取れるコマンドであれば、下記の例のように環境変数で渡す

    ```shell-session
    $ read -sp "Please input your password: " pass; echo
    $ pass="$pass" COMMAND
    ```

といった方法が考えられます。
これらの方法であれば `ps` コマンドでの盗聴は不可能となり、よりセキュアになります。

実際に利用するコマンドが、

*   どのような手段でクレデンシャル情報を渡せるのか確認し
*   ご自身の環境のセキュリティリスク(サーバ上にクレデンシャル情報を閲覧してはならないユーザのログインが許可されているか等)を考慮した上で

セキュリティリスクと作業工数等のバランスをとって頂ければと思います。

# その他

## パスワードの繰り返し入力が不要で楽

これはセキュリティ的なメリットではありませんが、作業時間を短縮できるメリットがあります。

パスワードを環境変数に格納するので、同じパスワードを何度も使う場合に都度入力する必要がありません。例えば次のように使えます。

```shell-session
$ read -sp "Please input your password: " __pass; echo
Please input your password: 
$ curl -u "user:${__pass}" http://example.com/file01 -o file01
$ curl -u "user:${__pass}" http://example.com/file02 -o file02
$ curl -u "user:${__pass}" http://example.com/file03 -o file03
```

パスワードは3か所で使っていますが、入力しているのは `read` コマンド実行時の一度のみです。

## `read` コマンドを使わなくても大丈夫なコマンドもある

例えば `mysql` コマンドです。 `mysql` コマンドは `-p` オプションでパスワードを指定することができます。

```shell-session
$ mysql -uroot -pmysqlpassword
```
上記のようにコマンドラインでパスワードを指定する事もできますが、 `-p` オプションの後にパスワードを指定しないとインタラクティブにパスワードを聞いてきます。

```shell-session
$ mysql -uroot -p
Enter password:
```
もちろんパスワードのエコーバックもありません。

インタラクティブにパスワードを問い合わせてくるコマンドは、ほとんどの場合パスワードに間違いがあれば直ぐに認証エラーを返してくれ、入力ミスに気づきやすいのでそのまま使った方が良いことが多いです。

# まとめ

*   パスワード等のクレデンシャル情報を別に安全に管理し、手順書はそれを参照させる
*   パスワード等のクレデンシャル情報をインタラクティブに問い合わせてくるコマンドであれば、それを利用する
*   コマンド実行時にオプションや引数で指定しなければならないコマンドであれば、 `read` コマンドを利用する

# 参考

*   [curlを使ってベーシック認証→ユーザーログイン→ファイルダウンロード](https://qiita.com/sukakako/items/9273fa05948af43ab39d)