特権付きDockerコンテナ—本当に必要ですか?
2020年11月5日
0 分で読めます今週、Podmanでテストをしているうちに、あるコンテナをrootless構成で実行するのに--privilegedフラグが必要な理由が気になり、調べ始めたら深みにはまってしまいました。同僚のEric Smallingが、そのフラグが必要な理由を尋ねたのも、もっともなことです。
結局のところ、--privilegedは「あらゆるもの」を許可するための簡略表記です。rootlessで実行するならそれほど問題ないと思うかもしれませんが、最小権限とゼロトラストの原則をある程度損ないます。rootlessで実行する場合でも、コンテナが実際に必要とするものを正確に把握し、必要最低限の権限だけを付与することが大切です。
rootlessで実行するプロセスにこのフラグを設定しても、ユーザーが持つ以上の権限がプロセスに与えられるわけではありません。それでも、この設定が推奨される理由となったプロセスの必要なケイパビリティが何なのか、興味をそそられました。探究心に駆られ、探検の装備を身にまとって暗闇の奥へと足を踏み入れました。
詳しく見ていきましょう
まず、コンテナをrootlessで実行するとはどういうことかを見てみましょう。これは、Linuxカーネルのユーザー名前空間の機能によるものです。非特権ユーザーでも新しいユーザー名前空間を作成できます。ユーザーが新しいユーザー名前空間を作成して入ると、その名前空間の中ではrootになり、コンテナを正常に起動するのに必要な権限の大部分を得ます。ユーザー名前空間内のrootは、システム全体のrootと同じ権限を持つわけではありません。しかし、これにより昇格した権限を必要とせずにコンテナを実行できるため、脆弱性が悪用される可能性のある攻撃対象領域を大幅に縮小できます。
今回使うシステムは、最小構成のCentOS 8をインストールした仮想マシンで、MacBook上のVirtualBoxで動作しています。最初にdnfを使ってpodmanをインストールしました。
次に、ユーザーのホームディレクトリにディレクトリを作成し、そこで自己署名証明書を生成しました。
次に、この証明書を使ってテスト用のローカルDockerレジストリを動かします。ここからが本題です。この方法について調べたドキュメントは、どれも--privilegedフラグの使用を勧めていました。
上のコマンドを見ると、タグ2のレジストリイメージを実行し、現在のディレクトリにあるcertsディレクトリをコンテナ内の/certsにバインドマウントするボリュームを作成し、レジストリを設定する環境変数を渡しています。そして、podmanにこのコンテナを特権モードで実行するよう指示する--privilegedフラグを、ためらいなく追加しています。
podmanでレジストリを使うには、/etc/containers/registries.confにエントリを追加する必要があります。
特権モードでは問題なく動作します。そこでまず、非特権モードで実行するとどうなるかを確認しました。調べるのは簡単でした。コンテナは起動した後にクラッシュして再起動を繰り返し、ログには証明書にアクセスできないと表示されます。
この時点では、原因はLinuxのケイパビリティに関係していると思いました。--privilegedフラグが行う主なことの一つは、コンテナがカーネルの提供するすべてのケイパビリティにアクセスできるようにすることだからです。このコンテナを特権モードで実行したときの状態は、podmanで確認できます。
次に、Linuxのmanページで各ケイパビリティがプロセスに許可する操作を詳しく調べました。ここでもpodmanを使って、実行中の非特権コンテナが持つケイパビリティを確認できます。
この一覧をLinuxのmanページと照らし合わせると、非特権モードで有効なケイパビリティがあれば、コンテナはファイルを読み取れるはずです。実際、この一覧にはこのコンテナが必要とする以上のケイパビリティが含まれているようです。この点には後ほど戻ります。
次に確認するのはホストのaudit.logです。予想どおり、次の記録がありました。
この記録から、SELinuxがdomain.crtファイルへの読み取り呼び出しをブロックしたことが分かります。CentOSおよびRHELシステムでは、SELinuxはデフォルトでEnforcingモードに設定されています。sestatusツールで確認できます。
SELinuxはEnforcingモードでファイルやディレクトリにラベルを付け、アクセスを管理します。このラベル付けは、コンテナエンジンの動作にも反映されます。コンテナエンジンはcontainer_tラベルでプロセスを起動し、コンテナ自体にはcontainer_file_tラベルを付けます。SELinuxは、プロセスがこのラベルの付いたファイルとのみやり取りできるようにし、コンテナ外のファイルへのアクセスをデフォルトで拒否します。--privilegedフラグを付けて実行すると、ラベルが無効になり、コンテナはコンテナエンジンの起動時に使われたラベルで実行されます。podmanを使ってコンテナを確認すると、この違いが分かります。こちらは特権コンテナです。
こちらは非特権で実行しているコンテナです。
ホストのファイルシステムにあるcertsディレクトリを見ると、user_home_tラベルが付いていることが分かります。
つまり、非特権モードでは、ファイルをイメージにバインドマウントしていてもコンテナからアクセスできません。
そこで、コンテナを--privilegedで実行する代わりに、いくつかの方法で解決できます。まず、podmanコマンドラインで--security-opts label=disableを指定すれば、ラベルを完全に無効化できます。セキュリティの観点からは明らかに望ましくありません。そこでpodmanとDockerには、マウントのラベルを付け直す仕組みがあります。マウントを個別に使う場合はZスイッチを、共有する場合はzスイッチを使います。
コンテナを修正するには、次のように実行するだけです。
最後にケイパビリティについてですが、コンテナを有効なケイパビリティの絶対的な最小限で実行するのは、常に良い考えです。今回のレジストリコンテナは、私のテスト環境では、すべてのケイパビリティを削除して実行できます。
コンテナに必要なケイパビリティを調べる方法はいくつかあります。たとえば、動作中にSELinuxがブロックするケイパビリティを監査ログで確認し、必要なものを--cap-add引数で追加できます。最小権限の原則に従うのが、常に最善の選択です。この話の教訓は、必要なものまで一緒に捨ててはいけない、ということです。
まとめ
ユーザー名前空間内であっても、コンテナに--privilegedを指定するのは望ましい方法ではなく、最小権限とゼロトラストの原則に反します。SELinuxなどのツールの出力を活用して、コンテナイメージが求めているケイパビリティや権限を監査し、実行前に実際に必要なものを確認しましょう。そして、本番環境でコンテナを実行する際は、それらだけを設定してください。必要な権限は、実際にはごくわずかであることがよくあります。
開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。


