docker secretsの部分的導入

当サイトの記事内ではdockerの管理には現状docker-composeを利用しており、コンテナへの設定受け渡しには原則環境変数を使用しています。しかし環境変数によるパスワードなどの受け渡しは推奨されていないという記事を最近では結構見かけます。代替するものとして、今回はdocker secretsを使用する方法を少し書いておきます(と言ってもdocker-composeのfake secrets)。

背景

設定を環境変数に格納することについて

環境変数を使用しようという風潮はTwelve-Factor AppIII. 設定から提唱されたと思います。引用すると

(前略)… Twelve-Factorは 設定をコードから厳密に分離すること を要求する …(中略)…
アプリケーションがすべての設定をコードの外部に正しく分離できているかどうかの簡単なテストは、認証情報を漏洩させることなく、コードベースを今すぐにでもオープンソースにすることができるかどうかである。 …(中略)…
この“設定”の定義には、アプリケーション内部の設定は 含まない …(中略)…
Twelve-Factor Appは設定を 環境変数 に格納する …(後略)

https://12factor.net/ja/config

となっています。素直に読むと認証情報を環境変数に格納するという意味に読めます。

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 を開くと、 EnvironmentWORDPRESS_DB_PASSWORDexamplepass であることがしっかり表示されてしまっています。

これは「攻撃者は何とかして 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 を開いて見てみると、今度は EnvironmentWORDPRESS_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に変えてこの辺も暗号化したいと思っています。

未分類docker,linux

Posted by first_user