コンテナでプログラムをrootとして実行することがなぜ問題なのか KubernetsのCVE-2019-11245を例に考える

By

Category: Unit 42

Tags: , , ,

This post is also available in: English (英語)

概要

5月31日、Kubernetes Product Security Committeeは、Kubernetesのセキュティ リグレッション(CVE-2019-11245を割り当て済み)を公開しました。この問題は「非rootユーザーとしての動作が想定されているイメージを使用しているコンテナが、当該イメージの2度目の使用ないしコンテナの再起動によりrootとして動作してしまう」というものです。

この特定のセキュリティ問題について詳しく説明する前に、まずは「コンテナでプログラムをrootとして実行することがそもそもなぜ問題なのか」を明確にしましょう。

非rootコンテナ

さて、コンテナ化されていないLinux環境(ホスト マシン上など)でアプリケーションを実行する場合に、rootユーザーと非特権ユーザーの分離が望ましい理由については一般的に理解されているものと思います。それは、侵害されたアプリケーションや不正な挙動を行うアプリケーションがrootとして動作していれば、システム ファイルを変更したり、特権プロセスを停止・起動したりすることで、システムに簡単に大きな損害を与えてしまえるからです。一般的なLinuxデーモンの多くは、初期のセットアップ段階でさまざまな権限を取り除きます。たとえばUbuntuのnginxデーモンであれば、プロセスをフォークさせ、それをwww-dataという非特権ユーザーとして実行します。

コンテナを使用すれば特権の分離はもっと簡単です。各コンテナはLinux側の名前空間から分離され、ケーパビリティを落とした上でそれぞれに独立したランタイムで動作しますし、各アプリケーションは独自コンテナ内で動作しますので、そのコンテナ内でrootを使用することは安全と見なされます。

このため、コンテナ内のアプリケーションがroot権限で動作するのはよくあることで、ほとんどのイメージは非特権ユーザーに変更されていません。残念ながら、それによりイメージの重要なセキュリティ レイヤーが失われかねないことはあまり理解されていません。そうしたイメージはその大半が本来rootユーザーとして動作する必要がないものなので余計に残念な状況です。

ただし、一部のコンテナ プラットフォームについては、すべてのコンテナをデフォルトで非rootユーザーとして実行していて、これは称賛に値します。たとえばOpenShiftは、ランダムな非rootユーザーとして動作することをサポートするイメージを使用するようにユーザーに要求します。そして、各コンテナを任意の非rootユーザーとして実行します。1

また、一部のコンテナ化されたアプリケーションは、セットアップ後に非rootユーザーに変更することでroot権限を落とし、ユーザーが設定したファイル権限に基づいてコンテナ内の機密ファイル(構成など)やプロセスへのアクセスを防止できるようにしています。こうすることで、攻撃者が侵害したコンテナに与えることができるダメージを制限します。Jenkinsは、そのように設計されたよい例です。

しかし、コンテナ内で非rootユーザーを使用することが有効なセキュリティ上の予防策になる主な理由は、コンテナ ブレイクアウトの防止です。

ほとんどのコンテナ ランタイムは、ホストとコンテナが同一rootユーザーを共有します。これはそれ自体では問題になりません。なぜなら、コンテナ プロセスはLinuxのケーパビリティとLinux名前空間(PID、net、mount、IPCなど)を使用してサンドボックス化されているからです。しかし、カーネルがホストとコンテナのユーザーIDまたはグループIDを区別しない場合、ホストからコンテナ プロセスに任意の情報を公開する脆弱性がコンテナ ランタイムに存在すると、重大なトラブルを招きます。簡単に言うと、プロセスがrootとして動作している場合、コンテナ ブレイクアウトに成功する可能性が非常に高くなります。

そのような状況では、悪意のあるプロセスは、権限をrootユーザーに昇格させなくても、システムの任意のファイルを変更できます。また、一部のコンテナ エスケープの脆弱性は、root権限さえあれば利用可能です。

ここ数年で、KubernetesとDockerを含むコンテナ エンジンで、複数のブレイクアウト脆弱性が明らかになっています。CVE-2016-9962はその1つの例です。これはrunc 2の脆弱性であり、コンテナ プロセスのケーパビリティが削除される前にそれらのファイル記述子をホストからグラブすることによって、コンテナ プロセスがホストをエスケープできるようになります。この欠陥を利用するために必要なのは、プロセスをrootとして実行することでした。今年前半に見つかった同様のrunc脆弱性であるCVE-2019-5736も、悪意のあるコンテナ プロセスがrootユーザーとして動作する必要がありました。詳細は、Yuval Avrahamiの利用方法に関する記事を参照してください。

ユーザー名前空間とrootlessコンテナ

ユーザー名前空間は、コンテナ内のrootの問題を解決するために設計された最新のLinuxカーネル機能です。ユーザー名前空間を使用すると、コンテナのユーザーIDとグループIDを分離できます。このカーネル機能は、UIDとGIDのマッピングを使用して、ホストのユーザーとグループをコンテナ内のユーザーとグループと区別します。これにより、プロセスが、コンテナ内ではUID 0 (root)として動作すると同時に、ホスト上ではUID 100000 (または他の任意のUID)として動作することが可能になります。その場合、コンテナのプロセスがブレイクアウトしても、カーネルはその有効ユーザーをマッピングされている非rootユーザーIDとして扱うので、そのプロセスがホスト上でroot権限を持つことはありません。これは優れた方法です。

しかし、現在の主要なコンテナ ランタイムは、デフォルトでユーザー名前空間を使用することに慎重です。この機能がLinux 3.8でカーネルに追加されたのは2013年のことであり、Dockerが初めてユーザー名前空間を組み込のは2015年です。しかし、その後Dockerでこれがデフォルト機能として有効にされたことはありません。おそらく、この機能を使用することで制限が生じるためだと考えられます。たとえば、外部のボリュームまたはストレージのドライバは、ユーザー マッピングを認識しません。また、ホストからファイルをマウントする処理は、ファイルの権限のために複雑になる可能性があります。

その一方で、現在のrootlessコンテナの実装はすべて、その中核部分でユーザー名前空間に依存しています。rootlessコンテナを、この記事で非rootコンテナと呼んでいるコンテナと混同しないでください。rootlessコンテナは、ホスト上の非特権ユーザーが実行し、管理できるコンテナです。Dockerや他のランタイムではデーモンはrootとして動作する必要がありますが、rootlessコンテナは任意のユーザーがケーパビリティを追加しなくても実行できます。

rootlessコンテナは、従来のコンテナに比べて多少有利な点があります。たとえば、共有マシン上のユーザーは、管理者権限がなくてもコンテナを実行できます(大学の研究室など)。また、(非特権コンテナであっても)ネストされたコンテナを実行できます。しかし、実際にrootlessコンテナを設計する主な動機は、コンテナ ランタイムの前述の脆弱性のリスクを緩和することです。3

rootlessコンテナは、採用される傾向が高まっています。runcやDockerなどの主要なコンテナ ツールは、rootlessのサポートをリリースしています。また、ネイティブのrootlessコンテナを使用できる新しいコンテナ エンジンやイメージ ビルダがリリースされています。デーモンレス コンテナ エンジンのPodmanは、rootlessコンテナだけでDockerの代わになる十分なレベルに達していると思われます。

ユーザー名前空間はこの記事の論点ではありませんが、従来の非rootコンテナに比べてセキュリティ上の利点が得られるということは言及する価値があります。ユーザー名前空間を使用しない場合、コンテナ プロセスをrootなしで実行したとしても、コンテナに権限昇格の脆弱性があれば、ホストが侵害される可能性があります。たとえば、悪意のあるコンテナ プロセスは、脆弱なsetuidバイナリを利用してrootになることができます。これは、コンテナ内でrootユーザーがホスト上の非rootユーザーにマッピングされている場合は不可能です。

非rootコンテナのビルドと実行

Dockerでは、DockerbuildファイルでUSER命令を使用して、それ以降のすべてのコマンドで使用するUIDを指定した値に変更できます。最後に実行したUSER命令は、コンテナがデフォルトでどのUIDで動作するのかも指定します。多くのコンテナは、そのすべてのビルド ロジックをrootとして実行し、ビルド ファイルの終了前にこの命令を追加します。

Dockerでは、ユーザーが–user flagを使用して、コンテナ内で特定のUIDでイメージまたはコマンドを実行することもできます。

Kubernetesでは、セキュリテ コンテキストまたはポッド セキュリティ ポリシーのrunAsUserフィールドを通じて、特定のUIDでポッドまたはコンテナが動作するように制限できます。ポッド セキュリティ ポリシーを使用する場合、MustRunAsフィールドで選択した範囲内のUIDで動作するようにコンテナを制限することや、MustRunAsNonRootフィールドを使用してコンテナがrootとして動作できないようにすることもできます。

CVE-2019-11245のKubernetesの問題

Kubelet v1.13.6とv1.14.2のリリース以降、複数のKubernetesユーザーから、非rootコンテナがその2回目の実行からrootとして動作するという苦情がありました。4この問題を調査したところ、これはDockerコンテナを実行するKubernetesコンポーネントであるdockershimに固有の問題であることが判明しました。

USER命令を使用して非rootユーザーとして動作するようにビルドされたDockerコンテナが、その2回目の実行からKubernetesによってrootとして実行されていました。

もちろん、これはセキュリティ問題でした。コンテナでrootとして動作することについて前述した危険のほかに、ユーザーが、ユーザー構成に依存した設計を行っている可能性があります。たとえば、ユーザーは、root権限を持っていないことを知って、ボリュームをコンテナに公開している可能性があります。

この問題は、イメージUIDの検出を中断していたKubeletコードの特定コミット元に戻すことによってすぐに修正されました。中断されたロジックは、そのDockerイメージが以前ノードにプルされていた場合のみ実行されていました。これは、このバグが2回目以降の実行時のみ発生していた理由を説明しています。

この脆弱性の影響を受けるのは、このコミットを使用する2つのKubeletのリリース(v1.13.6とv1.14.2)のみでした。

注意点

コンテナは、通常は責任とケーパビリティが制限されているという性質から、最小権限の原則に従って使用されます。コンテナで厳密にはrootを必要としない場合はrootの使用を制限することによって、コンテナのセキュリティをさらに高め、コンテナ エンジンで見つかる可能性がある欠陥を攻撃者が利用できないようにすることができます。

そう遠くない将来にコンテナ ランタイムでデフォルトでユーザー名前空間のサポートが有効になり、rootlessコンテナが主流として採用されるようになると信じるだけの十分な理由があります。確かに、現在の実装にはわずかに制限がありますが、それらのプロジェクトの範囲が拡大するにつれて、すぐに開発者が制限を解消してくれるでしょう。

1https://docs.openshift.com/container-platform/3.3/creating_images/guidelines.html#use-uid
2現在Dockerで使用されている最も一般的なコンテナ ランタイム向けOCI実装については、GitHubを参照。
3Rootless Containers、DevConf.CZ (Jan 25, 2019)、Giuseppe ScrivanoおよびAkihiro Suda
4https://github.com/kubernetes/kubernetes/pull/78178https://github.com/kubernetes/kubernetes/issues/78175、およびhttps://github.com/kubernetes/kubernetes/issues/78308を参照。