パスワードを安全に入力してCLIからMySQLに接続したい

`mysql` や `mysqldump` コマンドを使うとき、パスワードをどう入力すればよいのか悩みました。それぞれの問題点などを挙げつつ、いくつか方法をまとめておきます。



# 前提条件

```sh
$ mysql --version
mysql  Ver 9.1.0 for Linux on aarch64 (MySQL Community Server - GPL)
```

私の環境では Docker Compose で下記のMySQLコンテナを立ち上げ、そこに接続しています。特に断りがなければ `docker exec -it my-database` を省略して記述します。

```yml:docker-compose.yml
services:
  mysql:
    image: mysql:9.1
    container_name: docker-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: my-database
      MYSQL_USER: user
      MYSQL_PASSWORD: password
```

今回は最新版の [MySQL 9.1](https://dev.mysql.com/doc/relnotes/mysql/9.1/en/) で検証していますが、[8.0](https://dev.mysql.com/doc/relnotes/mysql/8.0/en/) でも同一の挙動を確認しています。

# 方法1: パスワードをコマンドラインに入力

```sh
$ mysql -u user -ppassword
```

最も簡単でわかりやすくスクリプトとしても実行しやすい方法です。
無論、パスワードが直接見えており安全ではありません。実行時に MySQL からも警告されます。

```
mysql: [Warning] Using a password on the command line interface can be insecure.
```




# 方法2: インタラクティブにパスワード入力

```sh
$ mysql -u user -p
Enter password:
```

`-p` または `--password` だけを指定するとパスワード入力が求められます。手入力を行う際はこの方法が最も使われていると思われます。コマンドライン上にパスワードは入力されていないので警告も受けません。

しかしこの方法では `mysqldump` の結果をパイプで出力するのには使えません。何故なら、パスワード入力を促す `Enter password:` が標準出力で表示されるためです。しかも、仮にパイプで繋いでしまうと `Enter password:` でさえもパイプに吸い込まれてしまうので Ctrl+D を押すまでこのプロンプトも表示されません。

```sh
$ mysqldump -u user -p my-database | cat
Enter password:
-- MySQL dump 10.13  Distrib 9.1.0, for Linux (aarch64)
--
-- Host: localhost    Database: my-database
-- ------------------------------------------------------
-- Server version   9.1.0
(snip)
```

# 方法3: 環境変数にパスワードを入れる

```sh
$ MYSQL_PWD=password mysql -u user
```

環境変数 `MYSQL_PWD` にパスワードを入れると `mysql` コマンド上ではパスワードを入力せずに済みますが、この方法は [2019年の時点ですでに非推奨かつ廃止予定](https://qiita.com/miyamadoKL/items/849e8185b1a95f2e34e1#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0-mysql_pwd) とされています。

:::note warn
公式ドキュメントにおいても [**<i>extremely insecure</i>**](https://dev.mysql.com/doc/refman/9.1/en/environment-variables.html) と記述されています。
**使うべきではありません。**
:::

# 方法4: オプションファイルを使う

```ini:~/.my.cnf
[client]
user=user
password=password
```

```sh
$ touch ~/.my.cnf
$ chmod 600 ~/.my.cnf

# 上記の内容を書き込む
$ nano ~/.my.cnf

$ mysql
(snip)

$ rm ~/.my.cnf
```

MySQLのオプションファイルを記述すると、コマンドラインで指定できる内容をファイルとして記述できます。どのファイルを読むかについては [公式ドキュメントに記載](https://dev.mysql.com/doc/refman/9.1/en/option-files.html) があります。

当然ながら、オプションファイルにはパスワードをそのまま書くのでファイル権限は限定しなければなりません。必要なくなったらすぐに削除すべきです。

ところで、パスワードに特殊な文字が含まれているとオプションファイルとして正しく認識されないことがあります。その場合は [引用符で囲みましょう](https://qiita.com/s-katsumata/items/a678640bd21c3f6fc20d)。




# 方法5: オプションファイルを明示的に指定する

```sh
$ mysql --defaults-extra-file=./my.cnf
```

`--defaults-extra-file` を使うと、オプションファイルの場所を明示的に指定できます。
この方法の良いところは、例えば Docker Compose で MySQL コンテナを立ち上げた際、secretsとしてコンテナ外部のファイルをも参照できることです。

```yml:docker-compose.yml
services:
  mysql:
    image: mysql:9.1
    container_name: docker-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: my-database
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    secrets:
      - mysql_option_file

secrets:
  mysql_option_file:
    file: mysql_secret.txt
```

```ini:mysql_secret.txt
[client]
user=user
password=password
```

```sh
$ docker exec -it docker-mysql mysql --defaults-extra-file=/run/secrets/mysql_option_file
```

docker-compose.yml に `secrets` を指定すると、コンテナ側の `/run/secrets/` 以下にマウントされ、同期された状態になります。この方法の欠点として、`/run/secrets/` 以下にあるファイルの権限を変更できないことです。

```sh
$ docker exec docker-mysql chmod 600 /run/secrets/mysql_password
chmod: changing permissions of '/run/secrets/mysql_password': Read-only file system
```




# 方法6: 標準入力でオプションファイルを渡す

```sh
$ mysql --defaults-extra-file=/dev/stdin
```

オプションファイルを明示的に指定できるということは、デバイスファイルも指定できるということです。`/dev/stdin` で標準入力を使えるので、コマンドラインに入力を残さず、標準出力に `Enter password:` を出力させることもなく、一時的なオプションファイルを作る必要も、Docker であれば secrets の設定をする必要もありません。

この方法を使うと、スクリプトから `mysqldump` コマンドを呼び出す際に極めて便利です。例えば Python から呼び出そうとすると、

```python
import subprocess

command = [
    'docker exec -i docker-mysql '
    'mysqldump --defaults-extra-file=/dev/stdin my-database'
]
option_file = '[client]\nuser=user\npassword=password'

result = subprocess.run(command, input=option_file.encode(), shell=True,
                        capture_output=True)
print(result.stdout.decode())
```

とするだけで安全にコマンドを呼び出せます。

この方法の欠点は、シェルから直接実行するとオプションファイルとしてキーボード入力した内容がそのままエコーバックされ、標準出力にも出てきてしまうことです。

```sh
$ mysqldump --defaults-extra-file=/dev/stdin my-database | cat
# 以下の内容を手入力。^D は Ctrl+D
[client]⏎user=user⏎password=password⏎^D

# ここから下が標準出力で表示される内容
[client]
user=user
password=password
-- MySQL dump 10.13  Distrib 9.1.0, for Linux (aarch64)
--
-- Host: localhost    Database: my-database
-- ------------------------------------------------------
-- Server version   9.1.0
(snip)
```

# 参考文献

- MySQLの公式ドキュメントです。ドキュメントでは [mysql_config_editor ツール](https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0057) についても触れています。しかしこのツールの役割はあくまでも難読化なので、パスワードをどうやって安全に入力するかという話とは別問題です

https://dev.mysql.com/doc/refman/9.1/en/password-security-user.html

- オプションファイルを読み込ませる方法が解説されています

https://qiita.com/goto_yoshiki/items/4f75568799c044df811d

- 標準入力を使うという発想はこちらから着想を得ました。ただしこちらは `echo` を使ってパイプを使って渡しているのでコマンドライン上には入力する形になっています

https://unix.stackexchange.com/a/597587