Docker の基本的な使い方はわかったけど、その上で何を気を付ければ良いの?という点を調べたのでその時のメモです。
この記事では以下の説明をしています。
- マルチステージビルド
- ユーザーの指定
- 認証情報の扱いについて
コンテナイメージのセキュリティスキャン等、コンテナセキュリティに関するトピックは他にもありますが、この記事では扱いません。
環境
この記事に含まれる例は以下の環境でテストしています。
- Ubuntu Desktop 22.04 LTS
- Docker 24.0.6
- PHP 8.1.2
- Composer 2.6.5
マルチステージビルド
Docker イメージをビルドする時、ソースコードのビルドもコンテナ内で行いたい時がよくあります。
例えば Jenkins 等の CI パイプラインでコンテナをビルドする時、最新のソースコードを Git から持ってきて、必要なパッケージをダウンロードして、(コンパイルが必要な言語なら) ソースコードをコンパイルして、ビルド結果をコンテナイメージに含める、ということを継続的に行うと思います。
この一連の操作を Jenkins サーバー上で行うと、ホストOS上でビルド環境を管理しなくていけないので (特にランタイムのバージョンを変更する時など) 大変です。ビルド自体もコンテナで行えると管理が楽になります。
しかし、パッケージマネージャーやコンパイラ等は、アプリケーションの実行時には必要になりません。そのため、
- まずビルド用のコンテナでアプリケーションをビルドする
- ビルドした結果のファイルを、アプリケーション実行用のコンテナに配置する
という方法でビルドすることで、アプリ用コンテナにはアプリの実行に必要なファイルしか含まれなくなります。
このように、複数のコンテナを使ってコンテナイメージをビルドすることをマルチステージビルドと言います。
不必要なソフトウェアがコンテナに含まれるということは、それだけコンテナに含まれる (可能性のある) 脆弱性が増えることになるので、セキュリティ的にもマルチステージビルドを行う方が望ましいとされています。
マルチステージビルドの例
ここでは例として Laravel アプリケーションを含む Docker イメージをビルドしてみたいと思います。
事前準備
まず開発環境 (のつもり) のディレクトリを作成して、Laravel プロジェクトを作成します。
shell
$ mkdir -p ~/docker-multi-stage-build/dev && cd $_
# PHP と Composer のインストール
$ sudo apt update
$ sudo apt install php php-cli php-common php-mysql php-zip php-gd php-mbstring php-curl php-xml php-bcmath
$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"
$ sudo mv composer.phar /usr/local/bin/composer
# Laravel プロジェクトの作成と動作確認
$ composer create-project laravel/laravel example-app
$ cd example-app
$ php artisan serve
# ブラウザで http://localhost:8000 にアクセスして Laravel のページが表示されるのを確認
そしてソースコードを Git にコミットします。通常の運用と同じように依存パッケージ (vendor
フォルダ) はソース管理に含めないようにします。
shell
$ echo "vendor/" > .gitignore
$ git init
$ git config user.email "test@example.com"
$ git config user.name "example"
$ git commit -m "initial commit"
Docker でのマルチステージビルド
次にビルド環境 (のつもり) のディレクトリを作成して、先ほど作成した Git レポジトリを clone します。
shell
$ mkdir -p ~/docker-multi-stage-build/build && cd $_
$ git clone ~/docker-multi-stage-build/dev/example-app/
$ ls
example-app
これで準備が整ったので、Docker でマルチステージビルドを試してみます。
今回、Composer はアプリケーションを実行するためには必要ありません。しかし Git レポジトリには依存パッケージが含まれていないので、最初に Composer を使って依存パッケージをダウンロードする必要があります。
そこで、
- まず Composer がインストールされたビルド用コンテナで依存パッケージをダウンロード
- その後、依存パッケージとソースコードをまとめてアプリ用コンテナにコピー
という手順で Docker イメージをビルドしたいと思います。
マルチステージビルドを行う場合、Dockerfile には FROM
が2回 (またはそれ以上) 書かれます。そして COPY
に --from
オプションを付けることで、前のコンテナで実行された結果をコピーすることができます。
今回の Dockerfile は以下のようになります。
~/docker-multi-stage-build/build/Dockerfile
FROM composer:2.6.5
WORKDIR /multi-stage-build/
COPY example-app/ ./example-app/
WORKDIR /multi-stage-build/example-app/
RUN composer install
FROM php:8.1.25RC1-apache-bullseye
COPY --from=0 --chown=www-data:www-data /multi-stage-build/example-app/ /var/www/html/
これで Docker イメージをビルドして、コンテナを実行してみます。
shell
$ cd ~/docker-multi-stage-build/build
$ ls
Dockerfile example-app
$ sudo docker build -t example-app .
$ sudo docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
example-app latest 50da05fdc63b About a minute ago 510MB
$ sudo docker run -d -p 8080:80 --name=laravel-test example-app
ブラウザで http://localhost:8080/public/
へアクセスすると、Laravel の初期ページが表示されます。
ユーザーの指定について
Docker コンテナでは、何も指定しないと root
ユーザーでコマンドが実行されます。root
ユーザーが使われていてもコンテナからホストOSへアクセスすることはできませんが、1) コンテナ内では何でもできてしまう、2) ボリュームがマウントされている場合はマウントされたホストOS側のファイルに自由にアクセスできる、3) 何か脆弱性があった場合に利用されやすい、などのセキュリティリスクは存在します。
そのためコンテナ内のプロセスも root
以外で実行することが推奨されます。コンテナ内のプロセスを指定するには、 Dockerfile
で USER
を使用します。
Dockerfile
FROM ubuntu:22.04
RUN groupadd --gid 2001 myuser
RUN useradd --create-home --uid 2001 --gid 2001 myuser
USER myuser:myuser
CMD echo "current user is: $(id)"
shell
$ ls
Dockerfile
# イメージをビルド
$ sudo docker build -t user-test .
# コンテナを実行。指定したユーザーでコンテナ内のプロセスが実行されているのがわかる
$ sudo docker run --name mytest1 user-test
current user is: uid=2001(myuser) gid=2001(myuser) groups=2001(myuser)
コンテナ内で root
ユーザーが使われる理由の1つに、(イメージビルド時ではなく) コンテナ実行時に apt
や yum
を実行する、というものがあります。ただこれはインストールされるパッケージを事前にスキャンできない等セキュリティリスクがありますので、推奨されません。
必要なパッケージはイメージビルド時にインストールし、イメージに対して継続的にセキュリティスキャンを実行するのが推奨です。
秘密情報の取り扱いについて
ユーザーのパスワードやDB接続情報など、認証などに使われる秘密情報は Docker イメージに含めるのは推奨されません。これはイメージに含まれるファイルは、イメージにアクセスできる人であれば誰でも読むことができるためです。
例えばアプリケーションでは構成ファイルにDB接続情報などを保存しておく場合がありますが、これも推奨されません。DB接続ユーザーやパスワード等は AWS KMS や Vault 等の外部サービスに保存し、アプリケーションはこれらの外部サービスから認証情報を取得して使うのが推奨されます。
ただ、Docker イメージのビルド時に秘密情報が必要になることもあります。例えば前のセクションで独自ユーザーを作ってからコンテナ内のプロセスを実行しました。このユーザーにパスワードを設定したいとした場合、どのような方法があるか考えてみます。
(非推奨) Dockerfile にパスワードを書く
まず、Dockerfile 内に直接パスワードを記述する方法が考えられますが、これは危険です。これはイメージにアクセスできる人であれば、docker hisotry <image>
コマンドを実行することでビルド時に実行された操作が全て見えてしまうためです。
以下の例で試してみましょう。
Dockerfile
FROM ubuntu:22.04
RUN groupadd --gid 2001 myuser
RUN useradd --create-home --uid 2001 --gid 2001 myuser
RUN echo "myuser:mypassword" | chpasswd
USER myuser:myuser
CMD echo "current user is: $(id)"
shell
# イメージをビルドし実行
$ sudo docker build -t secret-test1 .
$ sudo run --name mytest2 secret-test1
current user is: uid=2001(myuser) gid=2001(myuser) groups=2001(myuser)
# イメージビルド時の操作履歴を表示
$ sudo docker history --human --no-trunc secret-test1
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:aa... 2 minutes ago CMD ["/bin/sh" "-c" "echo \"current user is: $(id)\""] 0B buildkit.dockerfile.v0
<missing> 2 minutes ago USER myuser:myuser 0B buildkit.dockerfile.v0
<missing> 2 minutes ago RUN /bin/sh -c echo "myuser:mypassword" | chpasswd # buildkit 601B buildkit.dockerfile.v0
<missing> 43 minutes ago RUN /bin/sh -c useradd --create-home --uid 2001 --gid 2001 myuser # buildkit 657kB buildkit.dockerfile.v0
<missing> 43 minutes ago RUN /bin/sh -c groupadd --gid 2001 myuser # buildkit 1.67kB buildkit.dockerfile.v0
... (省略) ...
上記の通りイメージビルド時に実行されたコマンドは全てイメージ内に記録されていて、RUN /bin/sh -c echo "myuser:mypassword" | chpasswd
と表示されている通りパスワードもそのまま見れてしまいます。
これは一時ファイルなども同様で、最終的に削除されたとしてもイメージ内にはファイル自体が履歴として残ります。
(推奨) イメージビルド時に secret を使用する
それではイメージビルド時に必要な秘密情報をどう渡せば良いかというと、 docker build
時に --secret
を指定する方法が推奨されています。--secret
オプションを使用することで、秘密情報は外部ファイルに保存しておき、イメージビルド時に安全に秘密情報をイメージに組み込むことができます。
secret
は使うには、
Dockerfile
ではRUN --mount=type=secret
としてsecret
をマウントして使用するdocker build --secret
を使用して、Dockerfile でマウントされるsecret
を指定する
という手順を踏みます。
Dockerfile
FROM ubuntu:22.04
RUN groupadd --gid 2001 myuser
RUN useradd --create-home --uid 2001 --gid 2001 myuser
RUN --mount=type=secret,id=mysecret echo "myuser:$(cat /run/secrets/mysecret)" | chpasswd
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret > /my-secret-in-container
USER myuser:myuser
CMD echo "Secret content: $(cat /my-secret-in-container)"
shell
# secret としてマウントするファイルを作成
$ echo "my-password" > mysecret.txt
# イメージをビルド
$ sudo docker build -t secret-test2 --secret id=mysecret,src=mysecret.txt .
$ sudo docker run --name mytest3 secret-test2
Secret content: my-password
# イメージのビルド履歴を確認
$ sudo docker history --human --no-trunc secret-test2
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:e6f... About a minute ago CMD ["/bin/sh" "-c" "echo \"Secret content: $(cat /my-secret-in-container)\""] 0B buildkit.dockerfile.v0
<missing> About a minute ago USER myuser:myuser 0B buildkit.dockerfile.v0
<missing> About a minute ago RUN /bin/sh -c cat /run/secrets/mysecret > /my-secret-in-container # buildkit 12B buildkit.dockerfile.v0
<missing> About a minute ago RUN /bin/sh -c echo "myuser:$(cat /run/secrets/mysecret)" | chpasswd # buildkit 601B buildkit.dockerfile.v0
<missing> About an hour ago RUN /bin/sh -c useradd --create-home --uid 2001 --gid 2001 myuser # buildkit 657kB buildkit.dockerfile.v0
<missing> About an hour ago RUN /bin/sh -c groupadd --gid 2001 myuser # buildkit 1.67kB buildkit.dockerfile.v0
... (省略) ...
docker build --secret
で指定したファイルは、デフォルトで /run/secrets/
にマウントされます。Dockerfile
ではこのファイルを使ってパスワードを設定しています。
docker history
を確認しても、先ほどとは異なり secret の中身は記録されていません。そのため安全に秘密情報を Docker イメージのビルド時に使用することができます。
その他のセキュリティ考慮事項について
この記事では扱いませんでしたが、Docker のセキュリティには他に Rootless mode、イメージのセキュリティスキャン、seccomp や AppArmor / SELinux などの Linux のセキュリティ機構、といったトピックがあります。本番向けにコンテナを運用する場合はこれらのトピックも確認するのをお勧めします。