docker secretsの部分的導入
当サイトの記事内ではdockerの管理には現状docker-composeを利用しており、コンテナへの設定受け渡しには原則環境変数を使用しています。しかし環境変数によるパスワードなどの受け渡しは推奨されていないという記事を最近では結構見かけます。代替するものとして、今回はdocker secretsを使用する方法を少し書いておきます(と言ってもdocker-composeのfake secrets)。
背景
設定を環境変数に格納することについて
環境変数を使用しようという風潮はTwelve-Factor App のIII. 設定から提唱されたと思います。引用すると
(前略)… Twelve-Factorは 設定をコードから厳密に分離すること を要求する …(中略)…
https://12factor.net/ja/config
アプリケーションがすべての設定をコードの外部に正しく分離できているかどうかの簡単なテストは、認証情報を漏洩させることなく、コードベースを今すぐにでもオープンソースにすることができるかどうかである。 …(中略)…
この“設定”の定義には、アプリケーション内部の設定は 含まない …(中略)…
Twelve-Factor Appは設定を 環境変数 に格納する …(後略)
となっています。素直に読むと認証情報を環境変数に格納するという意味に読めます。
Twelve-Factor Appへの反論
ネット上の記事を読むと、それらの反論もいくつか見つかり、
個人的に気になった点を抜き出すと
- 環境変数は実行時の情報をログする際などに全量簡単に漏洩しうる
- 環境変数は親プロセスから引き継ぐ前提なので、別のシェルからリスタートできない
- 環境変数で渡すと受け渡し後も残す場合が多く、その生存期間が問題
などのご意見のようです。
パスワード漏洩サンプル
個人的には権限に依らず攻撃者がアクセスしやすく、一覧可能かつ奪われたら最後な平文パスワードが入ったりするのでセキュリティが心配です。
例えば、WordPressを以下の ./docker-compose.yml
を作成し
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- ./wordpress:/var/www/html
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- ./db:/var/lib/mysql
以下で実行して(実行を終了するときは docker-compose down
)
docker-compose up -d
ブラウザから http://localhost:8080/ を開いてwordpressをインストール(初期設定)した後、 ./wordpress/phpinfo.php
を以下の内容で作成し
<?php phpinfo();
以下のコマンドでファイルの所有者を他と合わせてから
sudo chown 33:33 wordpress/phpinfo.php
ブラウザから http://localhost:8080/phpinfo.php を開くと、 Environment
に WORDPRESS_DB_PASSWORD
が examplepass
であることがしっかり表示されてしまっています。
これは「攻撃者は何とかして phpinfo()
を実行させるだけ、或いはphp動作確認用に用意した phpinfo()
をうっかり消し忘れるだけで、WordPressの記事データを含むデータベースへのパスワードを取得することができる」ということです。もちろんデータベースへの直アクセス手段がない限り取得したパスワードを使うことはできません。上記 docker-compose.yml
ファイルでは、コンテナ間はブリッジネットワークで繋がっているものの、ホストとの間はNATなので、通常のアクセス手段は8080だけで外からデータベースに直接アクセスすることはできません。
環境変数で渡さない方法
環境変数で渡した場合の問題点は制御を瞬間でも奪われたら、一切の追加情報や権限を必要とせずに誰でもその全量を取得できるという点だと思います。
しかしファイルならパスなどの追加情報がない限り、少なくとも全量を取得することが現実には不可能です。権限は設定可能ではあるものの、必ずしも制御を奪われた権限と異なる権限でアクセスできるアプリとは限りません。先のWordPressの例であればroot(0)だけに権限設定すればwww-data(33)からのアクセスはできなくできそうです。
環境変数で渡さず、ファイルで渡す方法はないのでしょうか?考えるまでもなく、bind mountすれば普通にファイルで渡せます。bind mountとはdockerのvolumeオプションで指定できるあれです。
bind mountでファイル渡し
先のWordPressの docker-compose.yml
を以下のように書き換えます(実行中の場合は先に停止してから編集してください)。
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
# 【変更】WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_PASSWORD_FILE: /wordpress_db_password.txt
WORDPRESS_DB_NAME: exampledb
volumes:
- ./wordpress:/var/www/html
# 【追加】
- ./wordpress_db_password.txt:/wordpress_db_password.txt
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- ./db:/var/lib/mysql
さらに ./wordpress_db_password.txt
を以下の内容で作成します。
examplepass
そして所有者とアクセス権の設定をしておきます。
sudo chown 0:0 ./wordpress_db_password.txt
sudo chmod 0600 ./wordpress_db_password.txt
この状態で docker-compose up -d
します。
起動したらブラウザから再度 http://localhost:8080/phpinfo.php を開いて見てみると、今度は Environment
に WORDPRESS_DB_PASSWORD
が出ておらず、代わりにWORDPRESS_DB_PASSWORD_FILE
が出ていて、値が /wordpress_db_password.txt
になっているのが分かります。とりあえず phpinfo()
だけでパスワード漏洩という事態は防げています。続けて、 ./wordpress/print_file.php
を以下の内容で作成し
<?php
$path = getenv('WORDPRESS_DB_PASSWORD_FILE');
echo "$path<br>";
echo file_get_contents($path);
以下のコマンドでファイルの所有者を他と合わせてから
sudo chown 33:33 wordpress/print_file.php
ブラウザから http://localhost:8080/print_file.php を開きます。
/wordpress_db_password.txt
だけでパスワードを含むファイル内容が表示されていないことが分かります。ただし、再び http://localhost:8080/ を開いてみると
Error establishing a database connection
となり、WordPress本体にもパスワードが渡っていないことが分かります。これは
$ docker-compose exec wordpress bash -c "grep WORDPRESS_DB_PASSWORD /var/www/html/wp-config.php"
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', 'example password') );
$
とすれば分かりますが、WordPress の official image がphp側でパスワードを取得しているからです。実際に getenv_docker()
の中身は
...
function getenv_docker($env, $default) {
if ($fileEnv = getenv($env . '_FILE')) {
return rtrim(file_get_contents($fileEnv), "\r\n");
}
else if (($val = getenv($env)) !== false) {
return $val;
}
else {
return $default;
}
}
...
であり、ここでファイルにアクセスしています(慣例として大抵の公式イメージは環境変数に_FILEを付けてファイルから取得できるようにしている)。なので、
sudo chown 33:33 ./wordpress_db_password.txt
とするだけで、再び http://localhost:8080/ を正常に見ることが出来るようになります。しかし今度は http://localhost:8080/print_file.php を見ると
/wordpress_db_password.txt
examplepass
のように表示されてしまいます(当然ですが)。
つまり、ファイルにすれば phpinfo()
だけではパスワードが漏れませんが、もう一手間でパスワードが漏洩する程度のセキュリティと考えられます。
docker secretsの部分的導入
この節ではさっきの節で説明したとおりのことを、docker-composeが用意しているdocker secrets用の仕組みを使用して行います。本来はdocker swarm modeで docker-compose を使用した場合の仕組みなので、docker swarm modeでない場合は暗号化されないfakeの実装になるのですが、それを使用するということです(その意味で『部分的』導入と呼びました)。docker secretsの詳細は以下を御覧ください。
https://docs.docker.com/compose/compose-file/compose-file-v3/#secrets
docker-compose.ymlだけ書き換えます。他は前節と一緒です。
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
# 【変更】WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/wordpress_db_password
WORDPRESS_DB_NAME: exampledb
volumes:
- ./wordpress:/var/www/html
# 【追加】
secrets:
- wordpress_db_password
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- ./db:/var/lib/mysql
secrets:
wordpress_db_password:
file: wordpress_db_password.txt
このファイルを使って起動しても、前節同様ブラウザから http://localhost:8080/ を開けばWordPress画面が表示されますし
http://localhost:8080/phpinfo.php を開けばパスワードが同様に表示されてしまっています。
まとめ
dockerコンテナで平文パスワードを環境変数渡しすると、phpinfo()などでダダ漏れになってしまいます。今回は僅かに攻撃者の手間を増やすために、ファイル渡しにする方法を紹介しましたが、セキュリティ的にはあまりよろしくありません。が、今回はここまでです。将来的にはkubernetesに変えてこの辺も暗号化したいと思っています。
ディスカッション
コメント一覧
まだ、コメントがありません