PodmanルートレスコンテナとCopy Failエクスプロイト
(garrido.io)- CVE-2026-31431 Copy Failは、ローカルの非特権ユーザーが
rootシェルを取得できるようにし、Podmanのルートレスコンテナ内でもコンテナ内部のroot権限昇格が可能になる - Podmanのルートレスコンテナは、ユーザーネームスペース、UID分離、Linux capabilitiesを組み合わせて、コンテナ内部の
rootをホストの非特権ユーザーにマッピングし、ホスト権限を制限する - テストでは、rootless non-rootコンテナの
fooユーザーはCopy Failの実行後にコンテナ内部でrootになれたが、権限はホストの非特権ユーザーbarが可能な範囲に制限され、ホスト上のroot所有ファイルは読み取れなかった --security-opt=no-new-privilegesや--cap-drop=allを適用すると、Copy Fail実行後もシェルはfooかつcapabilitiesnoneの状態に維持され、即座のrootシェル取得とcapability昇格を防げる- Copy Failの影響はコンテナのライフサイクルを超えて残る可能性があるため、カーネルのパッチ適用と再起動が必要であり、読み取り専用ルートファイルシステム、cgroupsのリソース制限、薄いランタイムイメージ、ファイアウォールといった多層防御を併用すべきである
Copy FailとPodmanルートレスコンテナの露出範囲
- CVE-2026-31431は4月29日にcopy.failで公開され、公開済みのPythonスクリプトを実行すると、ローカルの非特権ユーザーが
rootシェルを取得できる - Copy FailはLinuxコンテナ内でも悪用可能で、Podmanルートレスコンテナでもコンテナ内部の
rootシェル取得が可能である - テストでは、コンテナ
rootはホスト側ではコンテナを実行した非特権ユーザーbarの権限範囲に制限された - Podmanのルートレス実装は、ユーザーネームスペース、UID分離、Linux capabilitiesを組み合わせて、コンテナプロセスのホスト権限を制限する
- Copy Failは、ルートレスコンテナもこの脆弱性に対して無縁ではない一方、Podmanの設定によって侵害後の攻撃範囲を縮小できることを示している
ルートレスコンテナの仕組み
-
基本例: 非特権ユーザー
barがHTTPサーバーを実行- 例の環境は、UID
1001の非特権ユーザーbarがPodmanでubuntu:latestベースのイメージをビルドし、python3 -m http.serverを実行する構成である - ホストで
psを見ると、python3プロセスはユーザーbar所有で実行されている - Podmanはfork/execモデルを使用するため、コンテナプロセスは
podman runプロセスの子孫となり、一般的なUID分離によってコンテナプロセスをホストrootや他ユーザーから分離できる - 一般的なDocker構成では、非特権ユーザーが
docker runを実行しても、Dockerクライアントはroot権限のデーモンと通信し、デーモンが最終的にコンテナプロセスを生成するため、ホスト上ではコンテナプロセスがrootとして見える場合がある
- 例の環境は、UID
-
ルートレス rootful
- コンテナイメージは、明示的な
USERディレクティブや--userフラグがない場合、通常はコンテナコマンドを内部rootとして実行する podman topの出力では、HTTPサーバープロセスはホストユーザー1001にマッピングされるが、コンテナ内部ユーザーとしてはrootで実行される- この構成は、ホストでは非特権ユーザーとして実行される一方、コンテナ内部では
rootであるrootless rootful状態である
- コンテナイメージは、明示的な
-
ユーザーネームスペース
- Podmanルートレスコンテナはユーザーネームスペースを使って、コンテナ内外のUID/GIDを異なる形でマッピングする
- 例では、コンテナ内部UID
0のrootは、ホストのUID1001のbarにマッピングされる /etc/subuidのbar:165536:65536設定は、barのネームスペースプロセスに割り当て可能なUID範囲を定める- 例では、
barのUID1001に加えて、165536から231072までのUIDがbarプロセスに割り当て可能である - コンテナ内部ユーザー
www-dataでsleepを実行すると、内部ではwww-dataだが、ホストでは165568と表示される podman unshareでユーザーネームスペースに入ると、ホストでbar:bar所有のホームディレクトリが、ネームスペース内部ではroot:rootとして見える- Dockerもユーザーネームスペースをサポートするが別途設定が必要で、かつ1つのユーザーネームスペースしか許可されない一方、Podmanは各UNIXユーザーのルートレスコンテナをそのユーザーネームスペース内で実行する
-
権限操作とLinux capabilities
- PodmanはLinux capabilitiesを使って、コンテナプロセスに細分化されたroot権限を付与する
- イメージビルド中の
apt installのような処理は、chown、dac_override、fowner、setgid、setuid、net_bind_service、sys_chrootといったcapabilityの組み合わせで可能になる podman build --cap-drop=allで全capabilityを削除すると、aptがsetgroups、setegid、seteuid、chownなどに失敗し、イメージビルドが失敗する- 必要なcapabilityだけを追加する方式も可能で、例では
CAP_SETUID,CAP_SETGID,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FOWNERを追加してパッケージインストールを行っている - デフォルト実行状態のHTTPサーバーはコンテナ内部
rootとして動作し、CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOTのような多くのeffective capabilitiesを持つ - HTTPサーバーにはこのような権限は不要なので、
podman run --cap-drop=allで全capabilityを削除でき、このときpodman topではeffective capabilitiesがnoneと表示される
-
ルートレス non-root
- コンテナ内部でも非特権ユーザーとしてHTTPサーバーを実行するには、既存の
/etc/passwdのユーザー、たとえばwww-dataを使うか、イメージビルド中に専用ユーザーを作成できる - 例では、UID
1002のfooユーザーとグループを作成し、/var/www/htmlに読み取り権限を付与したうえでUSER foo:fooを設定する - このイメージを
--cap-drop=allで実行すると、プロセスはコンテナ内部でfoo、ホストUID166537、effective capabilitiesnoneの状態になる - コンテナプロセスは必要最小限の権限で実行すべきであり、たとえば
fooが特権ポート80にバインドする必要があるなら--cap-add=CAP_NET_BIND_SERVICEを追加すべきである - コンテナの実行形態は4つに分けられる
rootホストユーザー + コンテナroot: root rootfulrootホストユーザー + コンテナ非特権ユーザー: root non-root- 非特権ホストユーザー + コンテナ
root: rootless rootful - 非特権ホストユーザー + コンテナ非特権ユーザー: rootless non-root
- Podmanはrootless rootfulコンテナの実行を容易にし、コンテナプロセスを非特権ユーザーとして実行できるのであれば、rootless non-root構成も比較的容易に構築できる
- コンテナ内部でも非特権ユーザーとしてHTTPサーバーを実行するには、既存の
バインドマウントとUID分離
- ホストディレクトリをコンテナにマウントすると、ホスト
root、ホストbar、名前空間fooが所有するファイルにアクセスできるかどうかは、UIDマッピングによって異なる - 例では、
/var/lib/bar/testディレクトリにホストroot所有のroot.txt、ホストbar所有のbar.txtを作成し、コンテナから/testとして読み書き可能でマウントする - コンテナを
fooとして実行すると、ホストbar所有のファイルはコンテナ内部ではroot:rootに見え、ホストroot所有のファイルは名前空間にマッピングされないためnobody:nogroupに見える - コンテナ内部の
fooはbar.txtとroot.txtを読めず、rootless non-root は rootless rootful より追加の分離を提供する fooがマウントディレクトリに作成したfoo.txtは、ホストでは UID166537所有と表示され、ホストユーザーbarはそのファイル内容を読めない- コンテナを内部
rootとして実行すると、名前空間rootはホストbar所有ファイルとfoo所有ファイルを読めるが、ホストroot所有ファイルは読めない - 内部
rootとして実行しつつ--cap-drop=allを適用すると、fooのファイルも読めなくなり、ホストbar所有ファイルだけを読める
Copy Failテスト
-
テスト条件
- Copy Failテストでは、もともと公開されていたコミット
8e918b5のエクスプロイト版が使用される - 例示用コンテナイメージは、既存のHTTPサーバーイメージに
curlを追加し、コンテナ内でエクスプロイトスクリプトをダウンロードできるようにしている - イメージ名は
copyfailとしてビルドされる - テストカーネルは Debian の
6.12.74+deb13+1-amd64であり、Debian基準では最近のバージョンのうち6.12.85未満であれば、まだパッチ未適用のカーネルとして使用可能とみなせる - 一般に、非特権ユーザー
fooがsuを呼び出すと、rootパスワードが要求される - 各テストでは、コンテナユーザーが
/tmpに Copy Failスクリプトをダウンロードして実行し、rootシェルを得たらsleepを呼び出す - Copy Failはコンテナのライフサイクルを超えて持続するため、各テストの前にVMを再起動する
- Copy Failテストでは、もともと公開されていたコミット
-
rootless rootfulでの結果
--user=rootでコンテナを実行すると、コンテナ内部プロセスはすでにrootである- この状態でCopy Failスクリプトを実行して
suを呼び出すとuid=0(root)シェルを得るが、rootユーザーはもともとパスワードなしでsuにより別のrootシェルを開けるため、Copy Failが実質的に追加するものはない podman topでは/bin/bash、python3 copy_fail_exp.py、su、sleepはすべてコンテナ内部ではroot、ホストではユーザー1001と表示される- 同一のcapabilityセットが維持され、
CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,NET_BIND_SERVICE,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOTが見える - 内部
rootはマウントされた/testでbar.txtとfoo.txtは読めるが、ホストroot所有のroot.txtは読めない
-
rootless non-rootでの結果
- コンテナを
fooとして実行した後にCopy Failスクリプトを実行してsuを呼び出すと、コンテナ内部rootへ権限昇格する - 結果のシェルの
idはuid=0(root) gid=1002(foo) groups=1002(foo)と表示される podman topでは、初期の/bin/bash、エクスプロイト実行プロセス、su呼び出しは、ホストUID166537、コンテナユーザーfoo、capabilitiesnoneの状態で見える- 権限昇格後の
[sh]とsleepは、ホストではユーザー1001、コンテナではユーザーrootと表示され、rootless rootful と同じcapabilityセットを得る - 権限昇格したコンテナ
rootでも、ホストroot所有のroot.txtは読めない - この状態ではコンテナは侵害されているが、攻撃範囲はコンテナと、ホストの非特権ユーザー
barが可能な範囲に制限される
- コンテナを
-
no-new-privileges適用時の結果- Podmanでは
--security-opt=no-new-privilegesによって、コンテナプロセスが起動時点より多くの権限を得られないようにできる - rootless non-rootコンテナにこのオプションを適用してCopy Failを実行すると、シェルは開くが依然として
uid=1002(foo)のままである podman topでも、すべてのプロセスはホストUID166537、コンテナユーザーfoo、capabilitiesnoneのまま維持される- マウントされた
/testでも、fooは自分のファイルだけ読め、bar.txtとroot.txtは読めない - コンテナは侵害されているが、内部の非特権ユーザー
fooと capabilityなしの状態に制限される
- Podmanでは
-
--cap-drop=all適用時の結果- rootless non-rootコンテナを
--cap-drop=allで実行しても、fooはもともとcapabilityを持たない - この状態でCopy Failを実行して
suを呼び出すと、開いたシェルはuid=1002(foo)のまま維持される podman topでも/bin/bash、エクスプロイト実行、su、シェル、sleepはすべてfooかつ capabilitiesnoneの状態である- エクスプロイトは
rootシェルの取得に失敗し、fooは/testで自分のファイルだけ読める - この結果は
no-new-privilegesテストと似ており、両方の対策を併用することで capability 露出を効果的に減らせる
- rootless non-rootコンテナを
-
エクスプロイトの持続性
- 即時の
rootシェルと capability取得はno-new-privilegesや--cap-drop=allで防げたが、エクスプロイト自体の効果は残る - その後、capability制限なしで新しいコンテナを実行すると、非特権コンテナユーザー
fooがsuを呼び出すだけでコンテナrootになれてしまう - したがって、カーネルのパッチ適用と再起動は依然として必要である
- 即時の
多層防御戦略
-
読み取り専用イメージ
podman runに--read-onlyを追加すると、コンテナのルートファイルシステムが読み取り専用でマウントされる- Podman はデフォルトで
/tmp、/run、/var/tmpのような一部ディレクトリを書き込み可能でマウントするため、完全に読み取り専用にするには--read-only-tmpfs=falseも追加する必要がある - 読み取り専用コンテナが侵害された場合、システムへの書き込みが許可されないため、エクスプロイト後の一部攻撃を制限できる
- ただし
curlの出力をpython3にパイプできるため、読み取り専用設定だけではエクスプロイトの実行自体は防げない - 例の
python3HTTP サーバーはファイルシステムへの書き込みを必要としないため、このオプションを安全に使える - 多くの事前ビルド済みイメージは特定ディレクトリへの書き込みアクセスを前提としているため、読み取り専用ルートファイルシステムでは正常に動作しない可能性がある
- 読み取り専用ルートファイルシステムは、コンテナに接続された書き込み可能ボリュームとは独立しており、侵害時にもそのマウントディレクトリには引き続き書き込める
-
リソース制限
- Docker と Podman は cgroups を使って、コンテナに提供されるリソースを制限できる
- コンテナに無制限のメモリ、CPU、PID は必要ない
podman statsでコンテナのリソース使用量を確認し、それに合わせて制限を適用できる
-
利用可能なバイナリの制限
- 例では簡略化のために
ubuntuイメージを使っているが、ubuntuイメージには侵害時に攻撃者が利用できる多くのバイナリが含まれている - HTTP サーバーの実行には、そのようなバイナリの大半は不要である
- ランタイムイメージは可能な限り薄く構成するのが望ましい
- マルチステージビルドを使えば、ビルド時環境とランタイム環境を分離できる
- python3 のような用途別イメージ、Debian の
-slimバリアント、alpineのようなより小さなディストリビューションをベースにできる - コンテナプロセスと互換性があるなら、distroless images や
scratchを使って、シェル、パッケージマネージャー、システムユーティリティのないランタイムを作れる
- 例では簡略化のために
-
ファイアウォール
iptablesやnftablesを使って、コンテナプロセスをファイアウォールで制限 できる- コンテナプロセスに本当に必要な受信・送信接続だけを許可すべきである
- HTTP サーバーの例では、DNS やローカル・リモートサーバーへの接続は不要なため、確立済みの受信接続から来る
tcpパケットだけを許可するように制限できる
運用上の意味
- 標準的な Podman ルートレスコンテナは、標準的な Docker コンテナ構成よりも優れた隔離手段をデフォルトで提供する
- Docker でも rootless 実行 と 非特権ユーザー名前空間の利用 は可能だが、Podman より多くの設定作業が必要であり、アーキテクチャの違いも影響する
- Docker は依然として広く使われており、Dokku、Kamal、Coolify、Dokploy のようなセルフホスティングツールもデフォルトで Docker を使っている
- Docker Hub のイメージを十分に精査せずに実行したり、ロックダウン措置を適用しなかったりすると、必要以上に広い攻撃面を持つ状態でサービスが稼働することになる
- コンテナイメージの実装詳細を理解する必要がある
- どのユーザー、または複数ユーザーのうち誰がコンテナプロセスを実行するのか把握する必要がある
- コンテナプロセスがルートファイルシステム上のどのディレクトリに依存しているか把握する必要がある
- 必要な Linux capabilities と不要な capabilities を区別する必要がある
- Podman とコンテナが提供する複数のメカニズムを組み合わせることで、コンテナを強化し、侵害時の影響範囲を縮小できる
- ワークロードによっては、コンテナを唯一のセキュリティ境界として頼るべきではない
- コンテナと別個の物理・仮想マシンを併用すれば、効果的に分離できる
- Podman は、同一ホスト内でも各ワークロードを別々の非特権ユーザーと独自のユーザー名前空間で実行して隔離する方法を提供する
1件のコメント
Lobste.rs の意見
公開されたエクスプロイトよりも、脆弱性を成立させている根本的な挙動に注目すべき
この脆弱性は読み取り専用かどうかに関係なくページキャッシュへ書き込めるため、悪意あるコンテナが overlayfs のベースイメージファイルに属するページを改変でき、コンテナのデプロイ方法によってはその影響が別のコンテナにも及びうる
ここでの rootless 構成であれば、ホストシステム上で同じユーザーとして実行されている別のコンテナが対象になる
別のエクスプロイト手法としては、すでに使われていることが分かっているベースイメージベースのコンテナを起動または発見し、そのコンテナ内のページキャッシュを改変したうえで、同じランタイムと overlayfs データを共有する別のコンテナにそのコードを実行させることができる
rootless とユーザーネームスペースは重要だが、この件ではあまり役に立たず、copy.fail サイトが述べているようにコンテナでは seccomp で
socket(AF_ALG, ...)システムコールをブロックすることを検討すべき「コンテナのデプロイ方法によっては」が具体的に何を意味するのか、もう少し説明してもらえるとうれしい
rootless Podman の利点は、ワークロード次第ではホスト上で同じユーザーとしてコンテナを動かす必要がない点にある
メインのワークステーションユーザーとして複数の rootless コンテナを動かすケースを指しているなら同意するが、サーバーではそれぞれを別ユーザーに分離でき、同じコンテナイメージでも異なる非特権ユーザーで実行できる
ほとんどを
rootで動かす Docker のデフォルトとはかなり異なるが、これが究極のセキュリティ境界ではないことも記事の末尾に書いたし、複数の非特権ユーザーに rootless コンテナを分散して使う方式が適切かどうかは用途による特定のワークロードは VM に分離して使っている
rootless とユーザーネームスペースがここで役に立たないというのは、エクスプロイト防止の話を指しているのか気になる
seccomp については、まだコンテナに明示的なポリシーを書いたことがないので扱わなかったが、さらに調べる良いきっかけになった
Podman と rootless コンテナは好きだが、CopyFail を見て兄弟コメントと同じ結論に至った
podman+rootlessの追加的なアクセス制御の利点があっても、結局のところコンテナはセキュリティ境界ではないという古典的な助言を再確認することになり、カーネルエクスプロイトが 1 つあればすべて破られうる趣味でシステム管理をしている程度だが、この分野の新しい流れとして libkrun backend for crun with podman を見た
ほとんどのコンテナ化されたワークロードをそのまま扱いつつ、内部的には別個のゲストカーネルを持つMicroVMで実行されるという触れ込みだが、成熟度・実運用での検証・セキュリティ監査の水準はよく分からず、一部はかなり最先端に見える
MicroVM は LLM コーディングツールで積極的に採用されているので、そうした状態が長く続くかもしれない
podman machineも有望に見えたが、残念ながら開発者ワークステーション用途しか想定しておらず、ホストシステムごとにコンテナ実行 VM を 1 つだけ置くモデルだったそれでも「コンテナはセキュリティ境界ではない」という言い方は単純化しすぎだと思う。コンテナは確かにセキュリティ境界だが、私たちが信じたいほど強固ではないだけだ
ローカルデプロイではこの線引きはやや曖昧になる
ハードウェアの観点では、VM がプロセスより本質的に安全というわけではないが、3 つの理由で境界としてより防御しやすい
VM エスケープはシステムコールよりも一般的ではないため、性能低下なしにより多くのサイドチャネル緩和策を適用する余地がある
VM のホストインターフェースははるかに単純だ。ブロックデバイスにはブロック単位の読み書きインターフェースがあり、ネットワークデバイスはフレームを送受信する
Linux や *BSD がソケットに提供する
setsockopt呼び出しは、大半のエミュレーション・準仮想化ドライバよりはるかに大きな攻撃面であり、それですらカーネル全体の攻撃面のごく一部にすぎないVM インターフェースは状態もずっと少ない。リクエスト・レスポンス型のリングに進行中トランザクションがあるくらいで、そのほかはほとんどない
認証情報、UID、GID、ファイルディスクリプタテーブルのようなものはカーネルに状態ベースの複雑さを加え、バグがあればプロセスがそれを悪用できる
ワークステーション向けバリエーションの難しさは、こうした複雑さを再び持ち込んでしまう点にある
たとえばコンテナのベースレイヤーは不変ファイルシステムを格納したブロックデバイスとして公開できるが、ボリュームや共有フォルダはおそらく 9pfs や VirtIO-FS、つまり VirtIO 上の 9p または FUSE としてマウントされるだろう
すると攻撃面は再び広がる
運が良ければエクスプロイトチェーンが必要になる
FreeBSD 側のほうに詳しいのだが、通常は準仮想化・エミュレーションデバイスを提供するコンポーネントを Capsicum でサンドボックス化するので、まずホストプロセスを乗っ取り、その後で VM がアクセス権を持たなかったものにアクセスするにはカーネルまで突破しなければならない
しかしこの追加のサンドボックス化をしなければ、コンテナエスケープはユーザーができることをすべて行える世界に戻ってしまい、デスクトップで root が破られたのと大差なくなる
個人的には gVisor のほうが好みだ。VMM ランタイムではないが何年も存在しており、Tencent のような企業でも使われていて、すべてのコンテナをすでに Proxmox VM の中で動かしている自分の環境にはよく合う
もう 1 つ試しているのが syd-oci で、MicroVM や gVisor という定番の推奨先に比べるとやや注目度が低いように思う
libkrun の参考資料もありがとう。有望な可能性に見える
セキュリティ監査につながる可能性も高そうだ