アプリケーション開発者がやっておくとよいコンテナセキュリティ
この記事は、Finatextグループ Advent Calendar 2024の14日目の記事です。
本記事のスタンス
アプリケーションの実装についてはやろうとしていることに対して、ある程度仕組みの部分から理解しているのが望ましいと思いますが、ことセキュリティについては一旦仕組みよりも対策を優先した方が良いと言うのが筆者の考えです。(そもそもセキュリティの勉強って後回しにされがち...。)なので、理屈はある程度置いておいて、じゃあどんなことをすればコンテナをもっとセキュアにできるのか?について勉強した結果を独断と偏見に基づいてこの記事では紹介していきます。
最低限抑えておきたいコンテナセキュリティ
最小限の依存関係
イメージに含まれる依存関係は少なければ少ないほど攻撃の機会が減ります。
マルチステージビルドを使ったり、ベースイメージにDistrolessを使うことで、イメージに含まれる依存関係を減らすことができます。
FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN go build -o main
FROM gcr.io/distroless/static
COPY /app/main /main
CMD ["/main"]
依存関係の少ないベースイメージを使うことで、仮にビルドに使ったイメージやその途中でインストールされるパッケージに脆弱性があったとしてもそれが本番環境まで波及することはなくなります。
追加のメリットとしてDockerfileのビルドではRUNなどの一部コマンドで新しくレイヤーを作ります。ベースイメージにレイヤーが重なって最終的なイメージになりますが、それらのレイヤーに含まれるパッケージなどはイメージ自体に残り続けます。コンテナイメージの増大にも繋がるのでマルチステージビルドはしておくと良いでしょう。
distrolessは確かに軽量でセキュアですが、逆に言えば何も入っていない(シェルすら入っていない)ため、デバッグには少々苦労するのが欠点です。
非rootユーザによる実行
DockerfileではUSER命令によってUID=0(root)以外のユーザでコンテナを実行することが可能です。
ただし注意点としてはroot権限が必要な操作が実行できないこと(ファイルへの書き込み権限がなかったり、ウェルノウンポート、システムポートが開けられないなど)とホストからマウントしたディレクトリへのコンテナ内からの書き込み権限がなくなることが挙げられます。
前者については以下のようにDockerfile内で権限を付与したり、他のポートを使うように設定を変更する必要が出てくる可能性があります。
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
COPY html /usr/share/nginx/html
EXPOSE 8080
RUN chown -R 1000:1000 /var/cache/nginx /var/run /var/log/nginx
USER 1000
CMD ["nginx", "-g", "daemon off;"]
後者については少々厄介です。書き込み権限がコンテナ内からだけで良いのであれば、マウントされるディレクトリに対してコンテナ内で使うUIDからの書き込み権限を付与すれば良いですが、ホストからも同時に書き込みを行いたい場合は問題になります。docker起動時にARGとしてホストのUIDをセットするといった方法が採れそうです。
FROM ubuntu:22.04
ARG HOST_UID=1000
USER $HOST_UID
WORKDIR /app
CMD ["bash"]
services:
app:
build:
context: .
args:
HOST_UID: ${UID}
volumes:
- ./host-dir:/app
tty: true
UID=$(id -u) docker compose up
後述するRootlessモードが使えれば開発環境においてはUIDのセットは必要ないかもしれません。(そもそもrootで動かなくなるので)ただし、不特定多数が開発に参加する状況では基本的にセットした方が良さそうです。
脆弱性スキャン
Trivyなどのツールを使うことでイメージに脆弱性があるかどうかを調べることができます。
試しにUbuntuのイメージをスキャンしてみます。
trivy image ubuntu:22.04
以下のように脆弱性の一覧が出てきました。
CI/CDでこのスキャンを行うことで意図せず脆弱性のあるバージョンがデプロイされてしまうと言った状況が防げるでしょう。
コマンドライン引数で特定のSeverityで終了ステータス1を返すことができます。
trivy image --exit-code 1 --severity HIGH, CRITICAL ubuntu:22.04
ただし、古いイメージを比較的長期間使い続ける場合はCI/CDでのチェックでは見逃されてしまうため、別途コンテナレジストリでのスキャン結果も確認するとなお安心できそうです。
また、スキャンはあくまで生成されたコンテナイメージに対するものです。つまり、コンテナ起動時に何かファイルを追加するような操作があった場合それらのファイルに対してのスキャンは不可能です。特にそのようなことをする理由がない限りコンテナイメージはイミュータブルにしておくべきです。
ポート公開の制限
Dockerを使った開発時、ローカルとコンテナのポートバインドを行うことがよくあると思います。
そのとき127.0.0.1:8080:8080
のようにすることで、不用意にLAN内の公開されるのを防ぐことができます。
docker run -p 127.0.0.1:8080:80 my-image
services:
web:
image: my-image
ports:
- "127.0.0.1:8080:80"
...
基本的に他のマシンからアクセスできて嬉しい状況はあまりないはずなので、手癖として127.0.0.1まで含めてポートバインドするようにしておくと良いかもしれません。
Rootlessの利用
若干発展的な内容になりますが、そもそもコンテナランタイムをroot権限で実行しないことでセキュアにする方法が採用できます。
例えばDockerはrootlessで動作させることができます。参照
他にもDockerの代わりにPodmanを使うことでデフォルトでrootlessによるコンテナビルドやコンテナ実行が可能になります。筆者はここしばらくPodmanを使っていますが、互換性が結構高いのかほぼほぼDockerと同じような感覚で使えています。(極稀にビルドに失敗します)
まとめ
セキュリティの文脈では何かをやれば絶対大丈夫ということはありません。多層防御の考えの下、取り入れられそうなことはどんどん取り入れて行って欲しいなと思います。
この記事がコンテナのセキュリティについて考えるきっかけになってもらえれば幸いです。
Discussion