Docker の基本的な使い方はわかったけど、その上で何を気を付ければ良いの?という点を調べたのでその時のメモです。

この記事では以下の説明をしています。

コンテナイメージのセキュリティスキャン等、コンテナセキュリティに関するトピックは他にもありますが、この記事では扱いません。

環境

この記事に含まれる例は以下の環境でテストしています。

マルチステージビルド

Docker イメージをビルドする時、ソースコードのビルドもコンテナ内で行いたい時がよくあります。

例えば Jenkins 等の CI パイプラインでコンテナをビルドする時、最新のソースコードを Git から持ってきて、必要なパッケージをダウンロードして、(コンパイルが必要な言語なら) ソースコードをコンパイルして、ビルド結果をコンテナイメージに含める、ということを継続的に行うと思います。

この一連の操作を Jenkins サーバー上で行うと、ホストOS上でビルド環境を管理しなくていけないので (特にランタイムのバージョンを変更する時など) 大変です。ビルド自体もコンテナで行えると管理が楽になります。

しかし、パッケージマネージャーやコンパイラ等は、アプリケーションの実行時には必要になりません。そのため、

  1. まずビルド用のコンテナでアプリケーションをビルドする
  2. ビルドした結果のファイルを、アプリケーション実行用のコンテナに配置する

という方法でビルドすることで、アプリ用コンテナにはアプリの実行に必要なファイルしか含まれなくなります。

このように、複数のコンテナを使ってコンテナイメージをビルドすることをマルチステージビルドと言います。

不必要なソフトウェアがコンテナに含まれるということは、それだけコンテナに含まれる (可能性のある) 脆弱性が増えることになるので、セキュリティ的にもマルチステージビルドを行う方が望ましいとされています。

マルチステージビルドの例

ここでは例として 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 を使って依存パッケージをダウンロードする必要があります。

そこで、

  1. まず Composer がインストールされたビルド用コンテナで依存パッケージをダウンロード
  2. その後、依存パッケージとソースコードをまとめてアプリ用コンテナにコピー

という手順で 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 以外で実行することが推奨されます。コンテナ内のプロセスを指定するには、 DockerfileUSER を使用します。

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つに、(イメージビルド時ではなく) コンテナ実行時に aptyum を実行する、というものがあります。ただこれはインストールされるパッケージを事前にスキャンできない等セキュリティリスクがありますので、推奨されません。

必要なパッケージはイメージビルド時にインストールし、イメージに対して継続的にセキュリティスキャンを実行するのが推奨です。

秘密情報の取り扱いについて

ユーザーのパスワードや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 は使うには、

  1. Dockerfile では RUN --mount=type=secret として secret をマウントして使用する
  2. 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 のセキュリティ機構、といったトピックがあります。本番向けにコンテナを運用する場合はこれらのトピックも確認するのをお勧めします。