とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど
読者登録と各種SNSのフォローもよろしくお願いいたします。

Trivyを使ってみた PART2 (Dockerfile/Containerfileのスキャン)

Trivyというと、コンテナイメージのスキャン用のツールとして有名だと思います。 例えばDocker Desktopなどの拡張機能を使って利用したり、コマンドラインで使ったり、CI/CDの中でスキャンするなどで利用していると思います。このブログでもtrivyについて、以前取り上げました。

今回はDockerfile/Containerfileのスキャンについて取り上げます。

Dockerfile/Containerfileという名前について

コンテナでアプリケーションを実行する場合に必要なのが、自分たちのアプリケーションに合わせてベースイメージをカスタマイズすることだと思います。

コンテナイメージのレジストリーには多種多彩のコンテナイメージが用意されています。例えばPHPベースのアプリケーションを実行したいならPHPのイメージを使えば良いですし、その他、Java、Python、Ruby、Golangなど、様々な言語別のイメージが用意されていますし、MySQL、PostgreSQLなどのデータベース用のコンテナイージも用意されています。

これらを使えば、必要な開発環境、必要なサーバーをかんたんに手元で実行できますし、イメージを組み合わせて利用することで、自分たちが動かしたいアプリケーションを簡単に動かすことができるのです。

ただ、ベースイメージは基本的にはそのまま使わずに、何らかのカスタマイズをした上で使うと思います。例えばWebサーバーに何らかのモジュールを追加したり、予めWebコンテンツをイメージに組み込んだりなどです(他にも色々行うと思います)。

そのような場合に使うのがDockerfileです。Dockerfileという名前は、元々Dockerでアプリケーションを動かすために使うイメージを作成するための設定一式を書いたファイルであり、イメージ作成するツールとしてはDockerがスタンダートなツールだったので、そのような名前のファイルを作っていたと思います。

現在はDocker以外のコンテナイメージを作成するツールも色々あるので、Dockerでは引き続きDockerfileという名前がデフォルトで利用されますが、PodmanbuildahではContainerfileという名前のファイルが例としてあげられることがあります。ただこれらのツールでは互換性を考慮して、Dockerfileという名前のファイルもデフォルトで読み込むようです。

どのコンテナイメージ作成ツールを使う場合も、オプションでファイル名を指定すれば別の名前のファイルを使ってコンテナイメージを作成できますが、パット見すぐファイルが何をするためのものか分かるようにするため、結局はDockerfileかContainerfileの名前のいずれかを含めておくほうが良いと思います。

Dockerfile/Containerfileのスキャン

こんなディレクトリー構成でDockerfileなどがあるとします(Containerfileという名前の場合も同様です)。

% tree
.
├── Dockerfile
└── index.php

0 directories, 2 files

ファイルのスキャンをするには、つぎのように実行します。

% trivy config .

一つ上のディレクトリーからDockerfileやコンテンツファイル一式が入ったディレクトリーを指定する場合はつぎのような感じでしょうか?

% trivy config ../trivy-test 

実行すると、つぎのような出力結果が表示されます。

% trivy config ../trivy-test 
2022-09-26T13:45:33.788+0900    INFO    Misconfiguration scanning is enabled
2022-09-26T13:45:33.907+0900    INFO    Detected config files: 1

Dockerfile (dockerfile)

Tests: 22 (SUCCESSES: 18, FAILURES: 4, EXCEPTIONS: 0)
Failures: 4 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 2, CRITICAL: 0)

MEDIUM: Specify a tag in the 'FROM' statement for image 'docker.io/php'
═════════════════════════════════════════════════════════════════════════════════════════════════════
When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.

See https://avd.aquasec.com/misconfig/ds001
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:1
─────────────────────────────────────────────────────────────────────────────────────────────────────
   1 [ FROM docker.io/php
─────────────────────────────────────────────────────────────────────────────────────────────────────


HIGH: Specify at least 1 USER command in Dockerfile with non-root user as argument
═════════════════════════════════════════════════════════════════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002
─────────────────────────────────────────────────────────────────────────────────────────────────────


LOW: Consider using 'COPY index.php /var/www/html/' command instead of 'ADD index.php /var/www/html/'
═════════════════════════════════════════════════════════════════════════════════════════════════════
You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.

See https://avd.aquasec.com/misconfig/ds005
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:4
─────────────────────────────────────────────────────────────────────────────────────────────────────
   4 [ ADD index.php /var/www/html/
─────────────────────────────────────────────────────────────────────────────────────────────────────


HIGH: MAINTAINER should not be used: 'MAINTAINER Developer'
═════════════════════════════════════════════════════════════════════════════════════════════════════
MAINTAINER has been deprecated since Docker 1.13.0.

See https://avd.aquasec.com/misconfig/ds022
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:2
─────────────────────────────────────────────────────────────────────────────────────────────────────
   2 [ MAINTAINER Developer
─────────────────────────────────────────────────────────────────────────────────────────────────────

出力された内容を一つ一つ確認してみましょう。

FROM行に指定するイメージには適切なタグを使用しましょう

一行目のメッセージは表題の通り、イメージにタグを設定しましょうという警告です。コンテナエンジンはイメージのタグを指定しないと暗黙の了解で、latestというタグを利用して処理を行います。latestはそのタグが示すように最新版という意味のものです。コンテナイメージは何らかのベースOSの上にアプリケーションライブラリーが載った形でイメージが構成されています。例えばそのOSが1.0から2.0に更新されたとき、latestタグのイメージは2.0に更新されます。イメージの中身が大幅に更新されてしまうと、アプリケーションが正常に動かなくなる可能性があります。そのため、きちんとイメージタグは指定しましょうというメッセージが出ています。

コンテナイメージの多くはライブラリーのバージョンだけでなく、ベースイメージのバージョンも含めてリリースされているものが多いです。例えば今回の例であげたPHPのイメージであれば、Debian busterベースの「8.0.23-apache-buster」、Debian bullseyeベースの「8.0.23-apache-bullseye」、Alpine Linuxベースの「8.0.23-alpine」といったようにイメージタグが用意されています。その他にもコンフィグレーションが異なるもの、以前のバージョンなどのイメージがいくつか公開されています。

MEDIUM: Specify a tag in the 'FROM' statement for image 'docker.io/php'
═════════════════════════════════════════════════════════════════════════════════════════════════════
When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.

See https://avd.aquasec.com/misconfig/ds001
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:1
─────────────────────────────────────────────────────────────────────────────────────────────────────
   1 [ FROM docker.io/php
─────────────────────────────────────────────────────────────────────────────────────────────────────

ちなみに、latestを指定した場合も同じようなアラートが表示されます。イメージによってはlatestタグをあえて用意しないイメージもありますので、latestを安易に使うのはやめるべきでしょう。

MEDIUM: Specify a tag in the 'FROM' statement for image 'docker.io/php'
═════════════════════════════════════════════════════════════════════════════════════════════════════
When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.

See https://avd.aquasec.com/misconfig/ds001
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:1
─────────────────────────────────────────────────────────────────────────────────────────────────────
   1 [ FROM docker.io/php:latest
─────────────────────────────────────────────────────────────────────────────────────────────────────

アプリケーションはユーザー権限で動かしましょう

多くの場合、アプリケーションの実行にroot権限は必要ない場合が多いです。しかし、コンテナは何も指定しないとroot権限でアプリケーションを実行します。コンテナをroot権限で実行すると、コンテナエスケープの事象が発生する可能性があるため、ユーザー権限でアプリケーションを実行するほうが適切です。

コンテナのセキュリティが騒がれるようになって、公式のコンテナイメージも一部はユーザー権限でアプリケーションを実行するようになっているものもあります。

自分でビルドするイメージの場合は、Userを利用して、ユーザー権限でアプリケーションを実行しましょう。当然ながら指定するユーザーが作成済みである必要があるので、User指定の前にRUN行でユーザーの追加を実行しておく必要があります。ただ闇雲にUserを使えば良いわけではなく、イメージ側の調整が必要な場合もある(サーバーを動かすユーザーとかディレクトリパーミッション、設定ファイルの変更など)ため、これを実行するには注意が必要です。

HIGH: Specify at least 1 USER command in Dockerfile with non-root user as argument
═════════════════════════════════════════════════════════════════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002
─────────────────────────────────────────────────────────────────────────────────────────────────────

ADDではなくCOPYを使いましょう

ADD と COPY はどちらも、ディレクトリとファイルを Docker イメージに追加するように設計されています。

ADDは、ファイルをダウンロードして抽出し、ホスト イメージにコピーできる古い命令です。 COPYは、ファイルとディレクトリのみをコピーできます。COPYのほうがシンプルな機能を実装しているため、 COPYを使うことを推奨されるようです。

LOW: Consider using 'COPY index.php /var/www/html/' command instead of 'ADD index.php /var/www/html/'
═════════════════════════════════════════════════════════════════════════════════════════════════════
You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.

See https://avd.aquasec.com/misconfig/ds005
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:4
─────────────────────────────────────────────────────────────────────────────────────────────────────
   4 [ ADD index.php /var/www/html/
─────────────────────────────────────────────────────────────────────────────────────────────────────

MAINTAINER命令は使うべきではありません

以前は誰が作者か明記するためのMAINTAINER命令をDockerfileに記述していましたが、現在のDockerではこの記述は非推奨になっています。Docker 1.13で非推奨になったので、随分前ですね。

現在はLABELを代わりに使って、イメージにメタデータとしてデータを追加しましょうというメッセージです。メタデータの追加が特に不要であれば、単にMAINTAINER行を削除するだけでも対応できます。

HIGH: MAINTAINER should not be used: 'MAINTAINER Developer'
═════════════════════════════════════════════════════════════════════════════════════════════════════
MAINTAINER has been deprecated since Docker 1.13.0.

See https://avd.aquasec.com/misconfig/ds022
─────────────────────────────────────────────────────────────────────────────────────────────────────
 Dockerfile:2
─────────────────────────────────────────────────────────────────────────────────────────────────────
   2 [ MAINTAINER Developer
─────────────────────────────────────────────────────────────────────────────────────────────────────

対処してみる

trivyの推奨に従って、Dockerfileを書き換えてみました。

FROM docker.io/php:8.1.10-apache-bullseye
LABEL maintainer="ytooyama"
LABEL description="Sample PHP Container Image."

//create appuser
RUN useradd --create-home appuser
WORKDIR /home/appuser
USER appuser

COPY index.php /var/www/html/
EXPOSE 80

ただ、ここで指定しているベースイメージはApache Web Serverが動いており、残念ながらユーザー権限でアプリケーションを実行するにはもうひと手間、ふた手間掛ける必要がありそうです。

警告は残ってしまいますが、最終的には次のようなDockerfileとしました。

FROM docker.io/php:8.1.10-apache-bullseye
LABEL maintainer="ytooyama"
LABEL description="Sample PHP Container Image."

COPY index.php /var/www/html/
EXPOSE 80

実際このイメージを作って動くか確認してみます(Podmanの場合)。

% podman image build --compress -t php-demo:v2 -f Dockerfile .
% podman container run -p 50080:80 --name some-serv -d localhost/php-demo:v2

% curl http://localhost:50080
<html>
  <body>
    <h1>index</h1>
    Hello World!   </body>
 </html>

問題なくコンテナアプリケーションにアクセスできました。

ユーザー権限で実行するコンテナイメージとして動作する例も最後に載せておきます。アプリケーションがサーバー型ではなくてリクエストすると応答を返すだけのようなシンプルなアプリケーションの場合、ユーザー権限で実行しても問題なく動作します。そのようなアプリケーションの場合はUSERを使うべきでしょう。

FROM docker.io/debian:bullseye-slim
# Runs as root:
RUN apt-get update && apt-get -y upgrade && apt-get install -y python3 && rm -rf /var/lib/apt/lists/*

# Switch to non-root user:
RUN useradd --create-home appuser
WORKDIR /home/appuser
USER appuser
COPY test.py /home/appuser/

# Runs as non-root user:
ENTRYPOINT ["python3", "test.py"]

ちなみに、trivyのDockerfileのチェックには「Aqua Vulnerability Databaseのルール」が使われます。Dockerfileを書く場合は基本的にはBest practices for writing Dockerfilesのルールを守るようにして記述していれば多くの場合は問題ありませんが、ベストプラクティスは常に変わり続けるものなので、定期的に内容を確認して頭の中の知識を更新する必要があります。

--