安全なコンテナイメージを構築するためのヒントとベストプラクティス
2021年7月6日
0 分で読めますコンテナイメージのスキャンを始めると、大量の脆弱性が見つかって戸惑うかもしれません。以下は、脆弱性のあるNodeイメージを使って先週実施したスキャン結果です。かなり極端な例ですが、このイメージは何も手を加えていない状態で、800件を超える脆弱性があることがわかります。

このような結果を前にすると、CVEの長いリストを突きつけられて、ヘッドライトに照らされた鹿のように立ちすくんでしまう人も多いでしょう。特に、システム管理ではなくアプリケーション開発に注力している場合はなおさらです。この情報をどうすればいいのか、何から始めればいいのか。Nodeアプリケーションを動かすイメージが欲しかっただけなのに、すでにセキュリティを確保するための膨大な作業に直面しています。
ここで最も重要なのは、コンテナ内の問題を修正する方法は、オペレーティングシステムの場合とは異なるということです。個々のパッケージのアップグレードや、システム全体の管理について取り上げるわけではありません。コンテナでは、Dockerのセキュリティに関する10のベストプラクティスのような修復戦略を検討する前に、脆弱性がどのようにイメージに入り込んだのかを理解する必要があります。
コンテナイメージの中身とは?
まず理解しておきたいのは、使用しているイメージがどのように構築されているかです。ゼロからイメージを構築していない限り、Dockerfileではベースイメージから始めているはずです。ベースイメージと呼んでいても、そのイメージ自体が親イメージをもとに構築され、ビルドプロセスでソフトウェアがインストールされている可能性があります。親イメージも何らかの方法で構築されており、さらに別の親イメージを使っている場合や、ルートファイルシステムを構築するツールを使っている場合もあります。スキャン対象のソフトウェアが、そもそもどのようにイメージに取り込まれたのかを理解することが、脆弱性を最小限に抑える戦略を決めるうえで重要です。
その例として、Docker Hubにある公式のNginxイメージを見てみましょう。このイメージのDockerfileを確認すると、Debian Buster slimイメージをベースにしており、Nginxイメージのビルド時にソフトウェアと設定が追加されることがわかります。
さらに、Debian Busterイメージは別のDockerfileから構築されています。このDockerfileでは、scratchイメージにtarballを追加しています。
このtarballの構築方法を調べると、Debianプロジェクトがrootfsを構築するために使う一連のスクリプト、debuerreotypeツールの出力であることがわかります。これはDebianの方法ですが、ベースイメージとして一般的に使われる他のすべてのオペレーティングシステムでは、異なる方法で構築されています。
つまり、ベースイメージを見るだけでも、ソフトウェアがイメージに取り込まれるまでには、長く複雑なプロセスを経ることがあります。さまざまな仕組みを理解していないと、その流れを追うのは困難です。
scratch
scratchを使い、空のファイルシステムから独自のイメージを構築すればよいという意見もあります。状況によっては有効な方法であり、依存関係のないGoやCなどのコンパイル済み言語のバイナリには適しているでしょう。しかし、それ以外の多くの場合、イメージに含まれるすべてのものを自分で保守することになり、継続的に大きな負担がかかります。多数のコンテナイメージを構築している場合、この負担はすぐに手に負えなくなります。攻撃対象領域を小さくできるメリットが、保守の負担によって失われる可能性もあります。
信頼するか、しないか
イメージの脆弱性管理を考える際、ベースイメージを信頼するかどうかは重要な検討事項の一つです。アップストリームのイメージを信頼するのか、それともイメージのビルドプロセス全体を自分たちで管理し、インストールされるすべてのソフトウェアに責任を持つのか、常にバランスを考える必要があります。
前述のとおり、利用するイメージに至るまでのビルドプロセス全体を信頼する必要もありますが、その流れを明確に追うのは困難かもしれません。公開レジストリにあるイメージには、適切に構築されていないものや、メンテナンスされていないものが多くあります。一般に、公開レジストリは掲載されているほとんどのイメージに品質保証を提供していません。もちろん、これは大半のオープンソースソフトウェアを利用する場合と変わりません。選定に影響する品質面の要素も、同じように当てはまります。ソフトウェアは定期的にメンテナンス、更新されているか。幅広いユーザーコミュニティがあるか。支援している企業はあるか。こうした情報はすべてオンラインで確認できます。時間をかけて、自分が実際に使っているものを調べましょう。調査にはSnyk Advisorが役立ちます。
アップストリームのベースイメージを信頼すると決めた場合、ベースイメージに起因する問題は、自分たちでパッケージをアップグレードしたフォーク版を保守するのではなく、アップストリームでの修正を確認しましょう。コンテナは本質的にイミュータブル(不変)となるよう設計されています。コンテナのビルドプロセス内でパッケージをアップグレードし始めると、ベースイメージを使うという考え方を損なうことになります。また、自分たちがイメージの事実上のメンテナーとなるため、この方法はすぐに管理しきれなくなるでしょう。
ただし、ベースイメージを選ぶのは簡単とは限りません。たとえば、Docker Hubの「公式」Pythonベースイメージには脆弱性が多く、サイズも非常に大きいです。公式のランタイムイメージは、あらゆる用途に対応できるよう汎用的に設計されているため、これはよくあることです。より小さく、脆弱性も少ないslim版を選ぶ方法もあります。あるいは別のイメージを探すこともできますが、リポジトリには非常に多くのタグがあります。どれを選べばよいのでしょうか。
まず、言語フレームワークのイメージに付いた汎用のlatestタグは、本番環境には適さないでしょう。どのバージョンのフレームワークが使われているのか判断しづらく、将来変更される可能性もあります。一方、slimが自動的に最善の選択肢になるわけでもありません。脆弱性は少なくても、ビルド依存関係を自分たちで管理する必要が出てくるかもしれません。
最適解は……マルチステージビルド
この場合のベストプラクティスは、マルチステージビルドを使うことです。大きく汎用的なイメージをソフトウェアのビルドに使用し、ビルド成果物をslim版にコピーして本番環境にデプロイします。これならビルド依存関係を管理せずに済み、slim版のサイズと脆弱性の少なさも活用できます。また、正確なランタイム環境を把握でき、予期せず変更されることがないよう、特定のランタイムバージョンを指定しましょう。
ベースイメージを選ぶためのベストプラクティス
ベースイメージを選ぶ際の一般的な推奨事項を紹介します。
面倒な作業や脆弱性の修正は、アップストリームプロバイダーに任せましょう。 大規模なチームが対応しているため、問題を迅速に修正できる可能性が高くなります。
アプリでは、バージョンが指定されたイメージを使いましょう。少なくともメジャーバージョン、できればマイナーバージョンまで固定します。 そうすれば、将来、環境が予期せず変わることを防げます。
マルチステージビルドを活用しましょう。 これにより、ビルド時には実績のある組み合わせを使いながら、デプロイ時にはslimイメージを使用できます。
こまめに再ビルドしましょう。 ビルドプロセスを通じて、セキュリティ修正を適用できることがよくあります。
ときどきアップグレードすることも検討しましょう。 新しいバージョンには、より多くのセキュリティ修正が含まれていることもあります。
イメージの脆弱性を必ずスキャンしましょう
SnykでイメージとDockerfileをスキャンすれば、脆弱性の総数を減らすために使える代替ベースイメージについての情報を得られます。また、ベースイメージを変更するDockerfile向けのPRを、Snykが自動作成することもできます。さらに、無料で利用できます。

このブログシリーズのパート2では、ベースイメージに自分たちで追加するソフトウェアと、そこで見つかった脆弱性の修正方法について解説します。近日中に公開予定です。公開時に通知を受け取りたい方は、Twitterで@snyksecをフォローしてください。
開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。



