そんな curl で大丈夫か？認証情報を非対話で安全に入力する方法

# TL;DR

*   `curl` で HTTP(S) サーバに認証情報を非インタラクティブ(非対話的)に送信したい時がある。
*   引数に認証情報を渡せば非インタラクティブに使えるが、 `ps` コマンドで盗聴されたり意図せずに `.bash_history` に保存されてしまうことがある。
*   `read` コマンド、環境変数、ヒアストリング、プロセス置換、 Bash のビルトインコマンドを駆使して非インタラクティブでも安全に認証情報を入力する。

# 概要

`curl` コマンドは Basic/Digest 認証はもちろんの事、 GET/POST メソッドで認証情報を送信することが可能です。

`curl` コマンドは大きく分けて下記の3つの方法で認証情報を入力することができます。

*   インタラクティブに入力する
*   設定ファイルに記載して、それを読み込ませる
*   引数として渡す

認証情報を引数として渡すことは、プロセスツリー上に認証情報を平文で露出してしまうことになります。これは、そのコマンドを実行した Linux にログイン済みの第三者に認証情報を読み取られる可能性がありセキュリティ上危険です。また、手動でコマンドを入力した場合には、入力した認証情報が意図せずにヒストリとして `.bash_history` に保存されてしまうという問題もあります。
しかしながら、作業用の手順に含めたりする場合やシェルスクリプトを作成したりする場合など、コマンドに引数として渡す方が都合が良い場合が多くあります。
本稿では、実際に Linux にログインしている第三者が引数に含まれる認証情報を読み取れる事を示した上で、可能な限り利便性を損なわず、認証情報をファイルに保存せずによりセキュアに認証する方法になります。

# やりたい事

*   Linux クライアントで `curl` コマンドを使って、どこかのサーバに認証情報を送りたい。
*   認証情報は Linux クライアント上にファイルとして保存したくない。
*   認証情報が `.bash_history` に保存されるのも許せない。
*   Linux クライアントにログイン中の他ユーザに認証情報を盗聴されるのも許せない。
*   ショルダーハック防止の為に、入力した認証情報をエコーバックしたくない。
*   それでも `curl` コマンド実行時にインタラクティブに認証情報を聞いてくるのは嫌だ！  
    (何度も同じ認証情報を使う場合や、シェルスクリプトに `curl` コマンドを組み込む場合など。)

# 前提条件

## 環境

*   bash: `GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)`
*   curl: `curl 7.38.0 (x86_64-pc-linux-gnu)`

## 対象となる読者

*   bash を利用しているか、 bash のシェルスクリプトを実装しようとしている人
*   `curl` コマンドを使って、 HTTP(S) サーバに認証情報込みのデータを(特に GET/POST メソッドを使って)送りたい人
*   認証機能付き HTTP(S) サーバと連携するシェルスクリプトを書こうとしている人

## 対象とならない読者

*   `curl`? カール？なにそれおいしいの？な人
*   Linux クライアント ～ HTTP サーバ間での盗聴や改ざんについて知りたい人
*   ここまで読んで「いや、トークンをファイルに保存してそれを読み込ませればええやん。」って思った真人間

## 本稿で対応できる脅威

本稿にて対応できる脅威は下記の通りです。

*   ヒストリや `script` コマンド等による作業ログファイルからの認証情報漏洩
*   エコーバックによる認証情報のショルダーハック
*   `ps` コマンドによる認証情報の漏洩

## 本稿で対応できない脅威

本稿にて対応できない脅威は下記の通りです。

*   HTTP サーバとの通信を盗聴されることによる認証情報の漏洩  
    (ネットワークに平文で流れてたら防ぎようがない。。。)
*   Linux クライアントでの `root` 権限や、 `curl` コマンドを実行するアカウントの権限を用いた盗聴  
    (`root` 権限取られてたら多分どうしようもない。。。)

# 認証情報の入力

認証情報を `read` コマンドを利用して入力します。

下記の方法で認証情報を入力し、それぞれの環境変数を利用するだけで認証情報が `.bash_history` ファイルや `script` コマンドのログに保存されてしまうことを防ぐことができます。詳細は[Linux 作業手順書からべた書きパスワードをなくすシンプルなアイディア](https://qiita.com/ktooi/items/ba156bb1848c31f04949)をご参照ください。

```shell-session
$ read -p "Please enter your name: " __user
Please enter your name: ktooi
$ read -sp "Please enter your password: " __pass; echo
Please enter your password: 
$ read -sp "Please enter your token: " __token; echo
Please enter your token: 
```

上記のコマンドで、3つの環境変数が設定されます。
それぞれの環境変数名と、その意味、本稿で例示する値は下記の表を参照してください。

|環境変数|意味|例示する値|備考|
|---|---|---|---|
|`${__user}`|ログインするユーザ名です。|`ktooi`|本稿ではセキュアな情報としては扱いません。`${__pass}` と併用します。|
|`${__pass}`|ログインするユーザのパスワードです。|`CanUC!t?`|セキュアな情報として扱います。 `${__user}` と併用します。|
|`${__token}`|アプリケーションから払い出されたトークンです。|`CanUCMyToken?`|セキュアな情報として扱います。単体で完結するクレデンシャル情報です。|

セキュアな情報として扱う `${__pass}`, `${__token}` が、 `ps` コマンドに出力されたり `.bash_history` に保存されないようにします。

# 認証情報を盗聴(して|されて)みる

## 危険な Basic/Digest 認証

Basic/Digest 認証での認証情報盗聴を実験してみます。

`curl` コマンドでは、 Basic/Digest 認証を実施する時に `-u` オプションを利用します。
`-u` オプションの値は `ユーザ名:パスワード` の形式で指定しますが、実は `curl` コマンドは `ps` コマンドを実行しても `-u` オプションに指定された値を隠蔽するように実装されています。

試してみましょう。ターミナルを2つ起動して、両方とも `curl` コマンドを実行するホストにログインしてください。

片方は下記のコマンドを実行します。こちらは**盗聴する側**のターミナルになります。

```shell-session
$ while true; do ps aww | fgrep curl && break; done
```
このコマンドは `ps` コマンドの結果に `curl` が出現するまで何も表示せずにプロンプトを返しません。
`curl` が出現した場合は、引数も含めたコマンドの文字列を表示して終了します。

それではもう片方のターミナルで下記のコマンドを実行してみます。こちらは**盗聴される側**のターミナルになります。

```shell-session
$ curl --digest -u ${__user}:${__pass} https://192.0.2.1/
```
**盗聴される側**のターミナルでコマンドを実行した直後に、**盗聴する側**のターミナルに下記のような出力があるはずです。

```shell-session
$ while true; do ps aww | fgrep curl && break; done
21787 pts/1    S+     0:00 curl --digest -u                https://192.0.2.1/
```
このように、 `ps` コマンドの出力をみると、 `curl` コマンドの `-u` オプションの中身は半角スペースで埋められており、そこに記載されている認証情報を読み取ることができません。

という事で、 Basic/Digest 認証はこれでオッケーです。


（　＾ω＾）・・・

本当にオッケーなのでしょうか・・・？

実は、よく見てみると `-u` オプションは半角スペースで埋められていますが、その文字数までは隠蔽されていません。このことから、ユーザ名とパスワードの長さを推測することができます。
また、そもそも `-u` オプションを半角スペースで埋める処理は `curl` コマンドの処理によるものであり、コマンドの実行から `-u` オプションを半角スペースで埋める処理を実行するまでの僅かな時間ではありますが、ユーザ名とパスワードが露出するタイミングがあります。

先ほどの**盗聴する側**と**盗聴される側**のコマンドを何度も実行すると、そのうち**盗聴する側**に下記のような結果を得られるはずです。

```shell-session
$ while true; do ps aww | fgrep curl && break; done
21787 pts/1    S+     0:00 curl --digest -u                https://192.0.2.1/
(...snip...)
$ while true; do ps aww | fgrep curl && break; done
22464 pts/1    R+     0:00 curl --digest -u ktooi:CanUC!t? https://192.0.2.1/
```

何度か繰り返すうちにユーザ名とパスワードを盗聴することに成功しました。
コマンドを実行したときは `${__user}`, `${__pass}` と環境変数を利用していますが、これはコマンド実行時にシェルによって展開される為、プロセスツリー上では環境変数に設定した値が表示されてしまいます。

このことから、 `-u` オプションを利用して、コマンドラインにユーザ名とパスワードを指定するのは安全とは言い切れません。
なお、 `-u` オプションにユーザ名やパスワードを指定しない場合は、インタラクティブに問い合わせてきます。こちらは認証情報が `ps` コマンド等で露出しないので比較的安全です(が、本稿のやりたい事から外れるので詳細は省略します)。

## 危険な GET/POST メソッド認証

GET/POST メソッドでの認証情報盗聴を実験してみます。ここでは `-X` オプションと `--data-urlencode` (または `--data`)オプションを利用する方法で実験します。

試してみましょう。[危険な Basic/Digest 認証](#危険な-basicdigest-認証)の時と同じようにターミナルを2つ起動して、両方とも `curl` コマンドを実行するホストにログインしてください。

片方は下記のコマンドを実行します。こちらは**盗聴する側**のターミナルになります。

```shell-session
$ while true; do ps aww | fgrep curl && break; done
```
このコマンドは[危険な Basic/Digest 認証](#危険な-basicdigest-認証)の時と同様で、 `ps` コマンドの結果に `curl` が出現するまで何も表示せずにプロンプトを返しません。
`curl` が出現した場合は、引数も含めたコマンドの文字列を表示して終了します。

それではもう片方のターミナルで下記のコマンドを実行してみます。こちらは**盗聴される側**のターミナルになります。

```shell-session
$ curl -X GET --data-urlencode "user=${__user}" --data-urlencode "password=${__pass}" https://192.0.2.1/
```
**盗聴される側**のターミナルでコマンドを実行した直後に、**盗聴する側**のターミナルに下記のような出力があるはずです。

```shell-session
$ while true; do ps aww | fgrep curl && break; done
24455 pts/0    S+     0:00 curl -X GET --data-urlencode user=ktooi --data-urlencode password=CanUC!t? https://192.0.2.1/
```
このように普通に認証情報が表示されてしまいました。もちろん、 `GET` を `POST` に変更しても認証情報が露出してしまうことには変わりありません。

# 安全に認証情報を渡す

## 安全な Basic/Digest 認証

※ コマンドの例示はすべて Digest 認証です。 Basic 認証にする場合は、 `--digest` オプションを削除してください。

[危険な Basic/Digest 認証](#危険な-basicdigest-認証)のやり方では `ps` コマンドによりログイン済みのユーザからは容易に盗聴可能でした。それでは、ログイン済みのユーザに盗聴されないように、それでいて非インタラクティブに認証情報を入力するにはどのようにすればよいのでしょうか？

ここでは `-K` オプションとヒアストリング又はプロセス置換を利用して対処します。
`-K` オプションは、コマンド実行時の挙動を引数からではなく、指定されたファイルパスから読み込むオプションです。ファイルパスに `-` を指定すると標準入力から読み込みます。

ここでヒアストリングを利用して認証情報を渡してみます。　

```shell-session
$ curl --digest -K- https://192.0.2.1/ <<< "-u ${__user}:${__pass}"
```
この時、 `ps` コマンドでオプションを確認すると次のように表示されます。

```
27345 pts/0    S+     0:00 curl --digest -K- https://192.0.2.1/
```
引数はそのまま表示されていますが、ヒアストリングで渡した `-u ${__user}:${__pass}` は表示されておらず、認証情報が露出していないことを確認できます。

ヒアストリングを利用する方法以外にも、ビルトイン `echo` とプロセス置換を利用して認証情報を渡す方法も考えられます。

```shell-session
$ curl --digest -K<(builtin echo "-u ${__user}:${__pass}") https://192.0.2.1/
```
この時、 `ps` コマンドでオプションを確認すると次のように表示されます。

```
27728 pts/0    S+     0:00 curl --digest -K/dev/fd/63 https://192.0.2.1/
```
プロセス置換で記載した部分(`<(builtin echo "-u ${__user}:${__pass}")`)が `/dev/fd/63` に置き換えられており、認証情報を盗聴することができなくなっています。
なお、プロセス置換で記載した箇所でも、通常のコマンドだと新たにプロセスが生成されてしまい `ps` コマンドに引数が露出してしまいます。この為、新たにプロセスを生成する必要のないビルトインな `echo` を利用しています。[^1]

[^1]: 明示的に `builtin` を付与する必要はないかもしれませんが、確実にビルトインな `echo` が利用されるようにあえて付与しています。

## 安全な GET/POST メソッド認証

※ コマンドの例示はすべて GET メソッドでの認証情報送信です。 POST メソッドで認証情報を送信する場合は、 `-X GET` を `-X POST` に置き換えてください。

[危険な GET/POST メソッド認証](#危険な-getpost-メソッド認証)の方法では `ps` コマンドによる認証情報の盗聴が可能でした。

しかしながら、 GET/POST メソッドを用いた認証情報の送信でも、[安全な Basic/Digest 認証](#安全な-basicdigest-認証)と同様にヒアストリングやプロセス置換を利用することで `ps` コマンドを利用した認証情報の盗聴を防ぐことができます。

下記はプロセス置換を利用した例です。

```shell-session
$ curl -X GET --data-urlencode user@<(builtin echo -n "${__user}") --data-urlencode password@<(builtin echo -n "${__pass}") https://192.0.2.1/
```
`--data-urlencode` オプションを利用しているので渡すパラメータは URL エンコードされます。 `--data-urlencode` の値には、 `name=content` というフォーマットの他に `name@filename` というフォーマットを指定することができます。プロセス置換はファイルパスとして解釈されるので、 `name@filename` というフォーマットを利用しています。

このコマンドを、 `ps` コマンドで見てみると次のように表示されます。

```
  459 pts/0    S+     0:00 curl -X GET --data-urlencode user@/dev/fd/63 --data-urlencode password@/dev/fd/62 https://192.0.2.1/
```
プロセス置換で書いた部分がそれぞれ `/dev/fd/63`, `/dev/fd/62` となっており、この表示から認証情報を盗聴することができなくなっているのが確認できます。
また、ユーザ名を見られても問題ない場合は次のように実行することができます。

```shell-session
$ curl -X GET --data-urlencode "user=${__user}" --data-urlencode password@<(builtin echo -n "${__pass}") https://192.0.2.1/
```
このコマンドを、 `ps` コマンドで見てみると次のように表示されます。

```
  480 pts/0    S+     0:00 curl -X GET --data-urlencode user=ktooi --data-urlencode password@/dev/fd/63 https://192.0.2.1/
```

URL エンコードをされては困る場合には `--data` オプションを利用します。
但し、こちらの値は `name@filename` フォーマットを利用することができません。この為プロセス置換とは相性が悪くなっています。[安全な Basic/Digest 認証](#安全な-basicdigest-認証)の時のように、 `-K` オプションを利用して認証情報を渡しましょう。

```shell-session
$ builtin echo "--data \"user=${__user}&password=${__pass}\"" | curl -X GET -K- https://192.0.2.1/
```
`--data` オプションは、複数のパラメータを送信する場合には値を `&` で区切るか、パラメータの数だけ `--data` オプションを記載します。

ユーザ名がプロセスツリー上に見えて問題ない場合は、次のように実行することができます。

```shell-session
$ builtin echo "--data \"password=${__pass}\"" | curl -X GET -K- --data "user=${__user}" https://192.0.2.1/
```

## 安全なヘッダ認証

GET/POST メソッドでは `--data` の値をファイルから読み込ませる機能がありましたが、ヘッダ情報を指定する `-H` オプションにはそのような機能はありません。なので、ここでは `-K` オプションを利用します。

```shell-session
$ curl -K- https://192.0.2.1/ <<< "-H 'Authorization: Bearer ${__token}'"
```
ちなみに上記は [GitLab as an OAuth2 provider | GitLab](https://docs.gitlab.com/ee/api/oauth2.html#access-gitlab-api-with-access-token) に記載されている、 GitLab へのトークン送信方法です。ヘッダー名等は送信先のシステムに合わせて変更してください。

# まとめ

引数に認証情報を渡す方法では、プロセスの生成～終了までの間に認証情報がプロセスツリー上に露出してしまう事を示しました。これはその Linux にログインしている第三者に認証情報を盗聴される恐れがあります。

それを防いで安全に認証情報を送信する方法のまとめです。~~面倒だったので~~ここまでで例示していなかったコマンドも含めていますが、考え方は共通しているので詳細は省略します。っていうか省略してもそこそこ長いです。。。

*   Basic/Digest 認証を利用 or GET/POST メソッドで認証情報を渡す場合
    1.  認証情報を入力

        ```shell-session
        $ read -p "Please enter your name: " __user
        Please enter your name: # ユーザ名を入力
        $ read -sp "Please enter your password: " __pass; echo
        Please enter your password: # パスワードを入力
        ```
    2.  `curl` コマンドで認証する
        *   Basic 認証を使う場合

            ```shell-session
            $ # ビルトイン echo
            $ builtin echo "-u ${__user}:${__pass}" | curl -K- https://192.0.2.1/
            $ # ヒアストリング
            $ curl -K- https://192.0.2.1/ <<< "-u ${__user}:${__pass}"
            $ # プロセス置換
            $ curl -K<(builtin echo "-u ${__user}:${__pass}") https://192.0.2.1/
            ```
        *   Digest 認証を使う場合

            ```shell-session
            $ # ビルトイン echo
            $ builtin echo "-u ${__user}:${__pass}" | curl --digest -K- https://192.0.2.1/ <<< "-u ${__user}:${__pass}"
            $ # ヒアストリング
            $ curl --digest -K- https://192.0.2.1/ <<< "-u ${__user}:${__pass}"
            $ # プロセス置換
            $ curl --digest -K<(builtin echo "-u ${__user}:${__pass}") https://192.0.2.1/
            ```
        *   GET メソッドで認証情報を URL エンコードして渡す場合

            **プロセスツリー上にユーザ名が見えても問題ない場合**

            ```shell-session
            $ # ビルトイン echo
            $ builtin echo -n "${__pass}" | curl -X GET --data-urlencode "user=${__user}" --data-urlencode password@- https://192.0.2.1/
            $ # ヒアストリング
            $ curl -X GET --data-urlencode "user=${__user}" --data-urlencode password@- https://192.0.2.1/ <<< "${__pass}"
            $ # プロセス置換
            $ curl -X GET --data-urlencode "user=${__user}" --data-urlencode password@<(builtin echo -n "${__pass}") https://192.0.2.1/
            ```
            **プロセスツリー上からユーザ名も隠蔽したい場合**

            ```shell-session
            $ # プロセス置換
            $ curl -X GET --data-urlencode user@<(builtin echo -n "${__user}") --data-urlencode password@<(builtin echo -n "${__pass}") https://192.0.2.1/
            ```
        *   GET メソッドで認証情報を URL エンコードしないで渡す場合

            **プロセスツリー上にユーザ名が見えても問題ない場合**

            ```shell-session
            $ # ビルトイン echo
            $ builtin echo "--data \"password=${__pass}\"" | curl -X GET -K- --data "user=${__user}" https://192.0.2.1/
            $ # ヒアストリング
            $ curl -X GET -K- --data "user=${__user}" https://192.0.2.1/ <<< "--data \"password=${__pass}\""
            ```
            **プロセスツリー上からユーザ名も隠蔽したい場合**

            ```shell-session
            $ # ビルトイン echo
            $ builtin echo "--data \"user=${__user}&password=${__pass}\"" | curl -X GET -K- https://192.0.2.1/
            $ # ヒアストリング
            $ curl -X GET -K- https://192.0.2.1/ <<< --data "--data \"user=${__user}&password=${__pass}\""
            ```
*   ヘッダで認証情報を渡す場合
    1.  認証情報を入力

        ```shell-session
        $ read -sp "Please enter your token: " __token; echo
        Please enter your token: 
        ```
    2.  `curl` コマンドで認証する

        ```shell-session
        $ # ビルトイン echo
        $ builtin echo "-H 'Authorization: Bearer ${__token}'" | curl -K- https://192.0.2.1/
        $ # ヒアストリング
        $ curl -K- https://192.0.2.1/ <<< "-H 'Authorization: Bearer ${__token}'"
        ```

おしまい。