コンテナを root として実行しているか、それとも一般ユーザーとして実行しているか。これは一見、極めて単純な質問です。すぐに答えたくなるかもしれませんが、ちょっと待ってください。あなたは脅威モデルについて明確に理解できているでしょうか。実はそうでもないのでは?このブログは、明確な理解を助けることを目的としています。

上記の質問に答える前に、この質問の対象についてクリアにする必要があります。つまり、コンテナエンジン (Podman、Docker、CRI-O、containerd など) なのか、コンテナ内のプロセス (apache、postgresql、mysql など) なのか、またはコンテナがマッピングされているプロセス ID を指しているのかを判断する必要があります (これら 3 つのどれかによって異なる場合があるためです)。一見すると、これは自明ではないかもしれません。コンテナエンジンやコンテナ内のサブプロセスについては、どちらも実質的に任意のユーザーとして実行できます。

Containers illustration Red Hat の Universal Base Image (UBI) をさらに活用する

root について理解する

Podman の登場により、ルートレスコンテナが現実のものとなりました。Podman はコンテナを自らの直接のサブプロセスとして作成するので、検討すべきオプションは 4 つあることになります。しかし、ルートレスとは何でしょうか。ルートレスを理解するには、コンテナ内の root を理解する必要があります。コンテナ内の root を理解するには、コンテナ外の root を理解する必要があります。(ここまでは良いでしょうか。)

次の表は、コンテナの内部と外部の root を示しています (DevConf.us 2019 でこれらの概念を明確に示してくれた Vincent Batts 氏に感謝します)。このフレームワークを使って、ルートレスとは何かを見ていきましょう。

 

Table 1: Root inside and outside the container showing how users appear to the system

上記の例について興味深い点をいくつかに着目してください。まず、コマンドラインオプションは -i (interactive) -t (terminal) と -u (user) を組み合わせたものです。これらのオプションは、コンテナ内で対話型のターミナルが使えるようにし、コンテナ化されたプロセスをユーザー sync として実行するように指定します。man podman-run コマンドを入力すると、これらをさらに詳しく調べることができます。 

次に、$ に注目してください。これはシェルが一般ユーザーとして実行されていることを意味し、# はシェルが root として実行されていることを意味します。これらは UNIX の慣例であり、コンテナの内外の root を説明するのに役立ちます。 

第 3 に、上記の例では、Podman は定義上、コンテナの外にあり、root または一般ユーザー (fatherlinux) として実行されます。一方、コンテナ内では bash が root または一般ユーザー (sync) として実行されます。コンテナホストの /etc/passwd ファイルのユーザーは Podman に使用され、コンテナイメージの /etc/passwd ファイルのユーザーは実行中のコンテナで使用されます。別の言い方をすれば、これら 2 つの passwd ファイルは、2 組のユーザー (1 組はコンテナの外部、もう 1 組はコンテナの内部) を使用できることを示しています。ユーザー名前空間があるのはまさにそのためです。

ユーザー名前空間

カーネルがコンテナ内に実行中のプロセスを作成すると、ユーザー名前空間はコンテナ化されたプロセスのユーザー ID をコンテナ外の別のユーザー ID にマッピングします。具体的に見てみましょう。

 

Table 2: Showing user IDs and username inside and outside container

コマンドラインオプションは -i (interactive) -d (detach) と -u (user) の組み合わせです。これらのオプションはコンテナをバックグラウンドで実行し、コンテナ化されたプロセスをユーザー sync として実行するよう指定します。先程と同様、man podman-run コマンドを入力すると、これらをさらに詳しく調べることができます。 

Podman には top という便利なサブコマンドがあり、コンテナホストのユーザーを実行中のコンテナのユーザーにマッピングできます。上記の例は、コンテナを root として実行すると、コンテナのユーザー sync (uid 5) が、基盤となるコンテナホストのユーザー sync (uid 5) にマッピングされることを示しています。つまり、プロセスがこのコンテナから出た場合、実際のユーザー sync の権限で実行される可能性があります。 

一方、まったく同じコンテナを一般ユーザー (fatherlinux) として実行すると、実行中のコンテナのユーザー sync (uid 5) は、基盤となるコンテナホストの uid 100004 にマッピングされます。ここで、ユーザー sync (uid 5) はなぜ fatherlinux (uid 1000) にマッピングされなかったのかと疑問に思われるかもしれません。

シンプルに答えるとこうなります。新しいカーネルと新しい shadow-utils パッケージ (useraddpasswd など) では、新しいユーザーには自由に使えるユーザー ID の範囲が与えられます。従来の UNIX システムでは、各ユーザーは 1 つの ID しかありませんでしたが、現在ではコンテナ内で使用できるように、各ユーザーが何千もの UID を自由に持てるようになっています。 

これは、コンテナが複数のユーザーを使用する場合に役立ちます。たとえば、単一のコンテナまたは Pod で Apache と MySQL を一緒に実行する場合や、別のユーザーとして実行されるエージェントを使用してサイドカーコンテナを実行する場合などです。では、このマッピングのベースとなるものは何かについてですが、/etc/subuid/etc/subgid の 2 つのファイルがマッピングに使用されます。usermod コマンドを使用するか、システム管理者の手動操作でユーザーが追加されると、これらのファイルにエントリーが作成されます。

ユーザー ID についてさらに詳しく

以下に示したのは、私のシステムのエントリーの例です。以下のエントリーでは、ユーザー Fatherlinux は、コンテナ内の最大 65,535 のユーザー ID を、システム上の 100,000 から始まる実際のユーザー ID にマッピングできます。デフォルトでは、shadow-utils (useradd、passwd など) により、この範囲のユーザー ID は 1 人のユーザー専用に予約されています。useradd コマンドは、次のユーザーのために次の範囲を予約します。この例ではユーザー fred で、ユーザー ID は 165536 から始まります。

cat /etc/subuid
fatherlinux:100000:65536
fred:165536:65536

cat /etc/subgid
fatherlinux:100000:65536
fred:165536:65536

このマッピングは、コンテナの内側からも表示できます。

 

Table 3: More on users inside and outside the container

Podman が root として実行されている場合、コンテナでユーザー ID の全範囲 (4294967295 == 32 ビット) を使用できることに注目してください。しかし、Podman が Fatherlinux として実行されている場合は、コンテナ内の root はユーザー fatherlinux (1000) にマッピングされ、ユーザー sync (uid 5) は 100,000 から 165,535 の範囲の UID にマッピングされます。 

コンテナエンジンと実行中のコンテナ内のコンテナ化されたプロセスの両方が、特権のない別のユーザーとして実行されるため、これは優れたセキュリティ機能です。100,000 から 165,535 までのユーザー ID のセットには、システムに対する特別な特権はなく、ユーザー Fatherlinux (1000) にもありません。つまり、コンテナ内のプロセスが抜け出したとしても、コンテナホスト上でのそのプロセスの権限は大幅に制限されます。 

ここで、「多数のユーザーを追加してシステムの UID が不足することはあるのか?」という質問が生じます。端的に言えば、答えは「はい」です。しかし、UID は 32 ビットの数字で表現され、40 億もあることを考えると、そう簡単に不足する状態が発生するものではありません。計算すると、1 つのシステムには最大 65,535 人のユーザーを追加できることがわかります (4,294,967,295 ÷ 65535)。ほとんどのユースケースではこれで十分なはずです。

ルートレスコンテナについてもう少しだけ詳しく見てみましょう。/etc/subuid ファイルは、コンテナ内のユーザーをコンテナ外のユーザーにマッピングするために使用されますが、ユーザー (以下の例では fatherlinux) はコンテナイメージで定義されている必要があります。そうでないと、Podman はコンテナを起動できません。

podman run --user Fatherlinux -it ubi8 bash

出力:

unable to find user fatherlinux: no matching entries in passwd file

コンテナでは、コンテナイメージ内の /etc/passwd ファイルに存在するユーザー ID を指定する必要があります。これは、コンテナが本質的にコンテナ内のオペレーティングシステムにリンクされており、コンテナホストのオペレーティングシステムからは分離されていることを示す例の 1 つです。コンテナ自体が Linux なのです。

コンテナの多層防御

docker デーモンを使用している場合、クライアントサーバーモデルであるため、この概念を理解するのは容易ではありません。docker クライアントサーバーモデルを使用すると、一般ユーザーとしてコマンドを実行する場合でも、コンテナは root として実行できます。なぜならば、docker デーモンが root として実行され、root のすべての権限を持つためです。これは、ここまでの説明で明確になっているはずです。例として、次のコマンドを実行してみます。

 

Table 4: Commands to check user IDs

コンテナを実行しているユーザーがホストへの root アクセス権を取得しないようにするには、コンテナエンジンとコンテナ化されたプロセスを非 root ユーザーとして実行する必要があります 。これにより、サービス (httpd、MySQL など) とオペレーティングシステムの特権付きのリソースとの間に複数のセキュリティレイヤーが提供されます。コンテナエンジンを非 root ユーザーとして実行することは 1 つの防御層となり、コンテナ内で別の非 root ユーザーとしてプロセスを実行することはさらに別の防御層となります。 

Dan Walsh が、「非 root ユーザーとしてルートレス Podman を実行する」という記事でこれについてさらに詳しく解説してくれています。高レベルの視点で見ると、Podman のようなルートレスコンテナエンジンはユーザーアカウントとして実行できます。そしてコンテナ内では、コンテナ化されたプロセスのアカウントによってのみ制御される一連のユーザー ID にマッピングされた仮想的なユーザーセットを使用できます。 

このブログが、コンテナの内部と外部での root の機能についてよりよく理解するのに役立つことを願います。


執筆者紹介

At Red Hat, Scott McCarty is Senior Principal Product Manager for RHEL Server, arguably the largest open source software business in the world. Focus areas include cloud, containers, workload expansion, and automation. Working closely with customers, partners, engineering teams, sales, marketing, other product teams, and even in the community, he combines personal experience with customer and partner feedback to enhance and tailor strategic capabilities in Red Hat Enterprise Linux.

McCarty is a social media start-up veteran, an e-commerce old timer, and a weathered government research technologist, with experience across a variety of companies and organizations, from seven person startups to 20,000 employee technology companies. This has culminated in a unique perspective on open source software development, delivery, and maintenance.

Read full bio