Java開発者向けDocker:セキュリティで失敗しないために知っておくべき5つのこと
2020年11月20日
0 分で読めますDockerは、アプリケーションをコンテナ化するために最も広く使われている方法です。Docker Hubを使えば、作成済みのイメージを簡単に作成・取得できます。Docker Hubのイメージを使えば、Javaアプリケーションのイメージをすばやくビルドできるため、とても便利です。しかし、Javaアプリケーション用のカスタムDockerイメージを安易に作成すると、多くのセキュリティ上の懸念が生じます。では、Java開発者がDockerを使う際、セキュリティを欠かせない要素にするにはどうすればよいでしょうか?
Javaアプリケーションに最適なDockerイメージを作成する方法を詳しく見ていく前に、このテーマについてよく寄せられる質問を確認しましょう。
JavaアプリケーションをDocker化するにはどうすればよいですか?
JavaアプリケーションをDockerコンテナで実行するには、.jarまたは.warファイルをJREのベースイメージにコピーするだけでも可能ですが、その際にはいくつか注意すべき点があります。適切なJVM引数を選び、コンテナのランタイム設定を合わせることは、対策の半分にすぎません。どのベースイメージを使うかは、セキュリティの観点から非常に重要です。選択を誤ると、脆弱性を持ち込む可能性があるためです。
この記事では、ベースイメージの選択が及ぼす影響をより深く理解し、アプリケーションに利用できる最も安全なイメージを見つけるための情報をご紹介します。
DockerはJava開発者にどのようなメリットをもたらしますか?
Javaアプリケーションをコンテナにパッケージ化すると、JRE、設定、OSレベルの依存関係、ビルド成果物などを含むアプリケーション全体を、コンテナイメージと呼ばれる自己完結型のデプロイ可能な成果物として定義できます。これらのイメージはソフトウェアで定義されるため、作成を完全に再現でき、すべての環境で同じプラットフォームを実行する手段を開発者に提供します。また、コンテナを使えば、特別な権限を必要とせず、デスクトップ上で新しいプラットフォームのリリースやその他の変更を簡単に試せます。
Javaアプリケーションに適したDockerベースイメージを選ぶ
Dockerイメージを作成する際は、Docker Hubから取得したイメージを基にします。これをベースイメージと呼びます。ベースイメージは、Javaアプリケーション用にこれからビルドする新しいイメージの基盤です。ベースイメージを選ぶことは重要です。そのイメージに含まれるすべてのものを利用できるからです。ただし、それには代償も伴います。ベースイメージに脆弱性があると、新しく作成するイメージにも引き継がれます。
ベースイメージの脆弱性の多くは、そのイメージが使用するオペレーティングシステム(OS)レイヤーに存在します。2019年に実施した調査Shifting Docker security leftでは、選択するディストリビューションによって、OSレイヤーに起因する脆弱性の数が大きく異なることをすでに示しました。

2019年レポート - Shifting Docker security left
Adoptopenjdkの人気のあるJava向けDockerベースイメージ、openjdk11を見てみましょう。デフォルトのタグを使うと、このイメージはUbuntuディストリビューションをベースにビルドされます。一方、Debian、Centos、Alpineをベースにした特定バージョンのタグも選べます(Alpineはglibcベースではないため、ネイティブのJNI呼び出しを行うアプリケーションと互換性がない場合があります)。

セキュリティの観点から、適切なベースイメージの選択が極めて重要だとわかります。フル機能のオペレーティングシステムに含まれるバイナリがすべて必要とは限りません。Javaアプリケーション用の新しいDockerイメージをビルドする際は、最小限のベースイメージを使うのがおすすめです。含まれていないバイナリが害を及ぼすことはありません。
セキュリティ面に加え、最小限のベースイメージを使うと、新しく作成するイメージのサイズを小さくできます。Dockerイメージが小さければ、フットプリントも小さくなり、起動も速くなる可能性が高まります。また、jibを使ってビルドする方法もあります。Dockerfileを必要とせず、最小限のJavaイメージを作成できます。
JDKではなくJREを使う
Dockerイメージを作成する際は、正しく機能するために必要なリソースだけを割り当てましょう。本番用イメージには、完全なJava Development Kit(JDK)ではなく、適切なJava Runtime Environment(JRE)を使うことから始めます。また、本番用イメージにMavenやGradleなどのビルドシステムを含める必要はありません。たとえば、ビルドで生成されたjarファイルだけで十分です。
Dockerコンテナ内でアプリケーションをビルドしたい場合も、マルチステージビルドを使えば、ビルド用イメージと本番用イメージを簡単に分けられます。
例:mavenでビルドし、Java 8が必要なSpring Bootベースのjava-code-workshopアプリケーション用に、Java向けDockerイメージを作成するとします。
このJava向けDockerイメージを安易に作成するなら、次のようになります。

Mavenとopenjdk8を含むベースイメージを選び、ソースコードをイメージにコピーして、Mavenでアプリケーションをビルド・実行します。この例は問題なく動作し、アプリケーションもスムーズに起動します。しかし、作成したDockerイメージのサイズは631 MBです。
このDockerfileを変更し、マルチステージビルドを使ってみましょう。

ここでは引き続きmaven-openjdk8イメージを使ってプロジェクトをビルドします。ただし、これを最終成果物にはしません。大幅に小さいJava 8 JREイメージをベースに新しいイメージを作成し、実行可能なSpring Boot jarだけをコピーします。あとはjar-fileを実行すれば完了です。こうして作成したDockerイメージにはJDKやMavenは含まれず、JREだけが入ります。イメージのサイズは大幅に小さくなり、132 MBになります。
イメージが小さいとアップロードしやすく、起動時間も短縮できますが、安全性も高まります。JDK、ソースコード、ビルドツールがそろった実行中のコンテナに、何らかの理由で攻撃者がアクセスできた場合、どうなるか想像してみてください。
プライベートリポジトリへのアクセスに必要なシークレットを含める場合にも、この方法を使えます。本番用イメージのキャッシュに、こうしたシークレットを残したくはありません。ビルド用イメージは本番環境で使わないため、そこではシークレットを使っても問題ありません。この手法を使えば、他のイメージから必要なものだけを選び、本当に必要なリソースだけを含む本番用Dockerイメージを作成できます。
Dockerコンテナをrootとして実行しない
Dockerコンテナは、デフォルトではrootとして実行されます。開発では便利ですが、本番用イメージでこれを使うべきではありません。何らかの理由で攻撃者が端末にアクセスしたり、コードを実行できたりした場合、実行中のコンテナに対して大きな権限を持つことになります。また、不適切に高いアクセス権を設定したファイルシステムのバインドマウントを通じて、ホストのファイルシステムにアクセスされる可能性もあります。
これを防ぐ最も簡単な方法は、次の例のように専用ユーザーを作成することです。

3行目では、新しいグループを作成してユーザーを追加しています。このユーザーは、パスワードとホームディレクトリを持たないシステムユーザー(-r)です。また、新しく作成したグループにも追加しています。
次に、6行目でアプリケーションフォルダーへのアクセス権をユーザーに付与します。7行目も忘れないでください。ここでは使用するユーザーを指定しています。これにより、最後のコマンドは、新しく作成した権限を制限されたユーザーで実行されます。
開発中にDockerイメージとJavaアプリケーションをスキャンする
DockerfileからDockerイメージを作成するときも、イメージを再ビルドするときも、システムに新たな脆弱性が持ち込まれる可能性があります。脆弱性をできるだけ早く検出できるよう、開発中にDockerイメージをスキャンすることをワークフローに組み込みましょう。
Snyk CLIを使えば、Dockerイメージを簡単にスキャンできます。ローカルマシンで使うことも、パイプラインの一部として使うことも、その両方も可能です。Snyk CLIをインストールして認証した後、イメージをスキャンするには次のコマンドを実行します。
最初のセクションで触れたadoptopenjdkイメージをスキャンする場合、コマンドは次のようになります。
出力:

Dockerイメージのテストとモニタリングの両方が可能です。モニタリングにはsnyk container monitor <image>を使います。モニタリングではスナップショットを作成し、時間の経過に伴ってイメージに新たな脆弱性が見つかったり、修正が利用可能になったりしたかを確認します。
イメージをスキャンする際にDockerfile(新しいJava向けDockerイメージを作成したもの)がある場合は、snyk container testまたはsnyk container monitorに--Dockerfile=<dockerfile>フラグを追加してください。より適切な修復アドバイスが得られます。たとえば、脆弱性の数を減らせるベースイメージがある場合、それを確認できます。
例:
Javaアプリケーションをスキャンする
ビルドするJava向けDockerイメージには、アプリケーションも含まれます。つまり、ここも攻撃の対象になり得ます。Javaアプリケーションにセキュリティ脆弱性がないことを確認し、Java開発者がDockerを使う判断を最初から安全なものにしましょう。たとえば、RESTエンドポイントの呼び出しによってリモートコード実行を許してしまうライブラリがアプリケーションに含まれているとします。イメージの他の部分に脆弱性がなくても、深刻な事態につながりかねません。
Dockerイメージに含まれるJavaバイナリの大半は、外部から取り込んだコードでしょう。アプリケーションが依存するライブラリやフレームワークを思い浮かべてください。Snyk CLIを使えば、依存関係を簡単に確認できます。先ほどイメージのスキャンに使ったものと同じCLIです。ルートフォルダーでsnyk testまたはsnyk monitorを実行すれば、ライブラリに潜むセキュリティ脆弱性をスキャンまたはモニタリングできます。
自分で書いたコードには、SonarLint、PMD、spotbugsなどのコード解析ツールやリンターを使うのが効果的です。これらはより良いコードを書くための汎用ツールですが、明らかなセキュリティ上のミスを防ぐのにも役立ちます。
再ビルドを前提にビルドする
Javaアプリケーションは、いつでも破棄して再ビルドできるようにDockerイメージ向けにビルドしましょう。実行中のコンテナに問題があると気づいたとき、コンテナを停止して新しいインスタンスを立ち上げるだけで済めば理想的です。そのためには、データをコンテナの外部に保存するステートレスなJavaアプリケーションを設計する必要があります。たとえば、次の点を検討しましょう。
コンテナ内でデータストアやデータベースを実行しない。
コンテナ内に(ログ)ファイルを保存しない。
該当する場合は、キャッシュが自動的に復旧することを確認する。
いつでもアプリケーションを破棄して新しいインスタンスを起動できるようにビルドすれば、Dockerイメージ全体を安全に再ビルドできます。脆弱性のあるDockerイメージの20%では、イメージを再ビルドするだけで1つ以上のセキュリティ問題を修正できることをご存じでしょうか?多くの場合、Dockerイメージはベースイメージの「latest」タグを基にしています。この「latest」は時間とともに更新され、より新しく改善されたバージョンに置き換わります。aptやyumなどのパッケージマネージャーでコンテナにインストールする主要なバイナリも同様です。最新バージョンを使えば、最新のセキュリティ修正を自動的に取り込めるため、セキュリティの観点では有効です。ただし、ベースイメージは時間とともに変わるため、特定時点の状態を再現しにくくなることも考慮する必要があります。
アプリケーションに変更がなくても、Dockerイメージを定期的に再ビルドし、必要に応じて新しいバージョンや最新のベースイメージタグを使いましょう。OSレイヤーなど基盤レイヤーの改善によって、イメージの品質が向上し、セキュリティ脆弱性を減らせる可能性があります。
https://www.youtube.com/watch?v=v2SkWn-ZRDg
まとめとして、Docker全般またはJavaアプリケーション向けに最適なイメージをビルドするためのセキュリティのベストプラクティスを確認したい方は、次の資料をご覧ください。
DockerでJavaコンテナを構築するための10のベストプラクティス - Javaアプリケーション向けに、安全で高性能なDockerイメージを構築する方法を、手順を追って詳しく解説します
Javaセキュリティのベストプラクティス10選 - どのような環境向けにJavaアプリケーションを構築する場合にも実践すべきセキュリティ対策です。
Capture the Flagを始めよう
オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。
