eBPFのRootlessコンテナでの応用の可能性について調べてみた
先日参加した第57回 情報科学若手の会の懇親会の席でRootlessコンテナにおけるeBPFの活用の可能性に関して話題が上がりました. そのときにはあまりいい答えが出せず, そこで話題は終わってしまったのですが, 会が終わった後も個人的に気になるところがあったので調べて情報を整理しました.
結論
いきなり結論を言うとRootlessコンテナの定義を「Runtimeも含め, 作成に一切の特権を必要としないコンテナ」だとするとコンテナ内でeBPFを使うことはまずできません. eBPFのProgramなりMapなりを作るのに特権が必要だからです. ただ, それだけでは面白くないので, このポストではRuntimeは特権を持つのを許容した上で, 特権を持たないコンテナにeBPFを使わせるというシナリオを想定してどんな方法があるかを議論します.
eBPFとCapabilityの関係
先ほど述べたとおり, 大前提としてeBPFは特権ユーザにしか使うことができません (一部例外はありましたが脆弱性の温床となったために現状主要ディストリビューションではデフォルトオフです). eBPFが出た当初はどんなeBPFの Programをロードするにも CAP_SYS_ADMIN
が必要でした.
それがv5.8以降 CAP_BPF が登場し, 権限が細分化されました. 現状Mapの作成等のBPFシステムコールの機能には CAP_BPF
が, ネットワーク系のProgramのロードには CAP_BPF + CAP_NET_ADMIN
, トレーシング系の Programのロードには CAP_BPF + CAP_PERFMON
が必要です.
User Namespaceの中でeBPFは使えるのか?
Rootlessコンテナなどの実装に使われるUser Namespace の中ではユーザはNamespaceによって分離されたCapabilityを持つことができます. では, 例えばUser Namespaceの中で CAP_BPF + CAP_PERFMON
を持つことができたとして, 非特権ユーザがトレーシングのeBPF Programをロードすることはできるのでしょうか?
結論から言えばそれはできませんし, するべきでもありません. 現状BPFシステムコールの権限チェックではInit Namespace (ホストのNamespace) の中でCapabilityを持っているかがチェックされているので, User Namespaceの中でCapabilityを持っていても ProgramをロードしたりMapを作成したりすることはできません.
トレーシング等のeBPF Programはカーネルのあらゆる動きを見ることができるので, Namespaceの影響を受けません. ですので, eBPF ProgramをロードするプロセスがUser Namespaceで分離されていようと安全ではないので, この制限は妥当だと思います.
File Descriptor Passingによる権限移譲
User NamespaceにいるプロセスはeBPFを使うことはできません. ですが, Init Namespaceにいる特権プロセスから権限移譲を受けることによってeBPFを使うことはできます. 例えば, 権限のあるコンテナランタイムなどが用意した「無害」なeBPF Programを使わせるだけなら許容できる, あるいはなんらかの統計情報を収集するトレーシングのProgramを特権プロセス側でロードしておいて, 非特権プロセスはそれをただ読み出してPrometheusメトリクスとしてExportするだけといった場合にはこのようなアプローチが有効だと思われます.
権限移譲をする方法の一つとして, Unix Domain SocketによるFile Descriptor Passingが使えます. ロードしたeBPF Programや作成済みのeBPF Mapはユーザ空間ではFile Descriptorを通してアクセスするようになっているので, 特権を持ったプロセスでProgramのロードやMapの作成等を済ませてからそのFile DescriptorをUnix Domain Socketを使ってUser Namespace内に送り込めばUser Namespace内でもProgramをAttachしたりMapを読み書きしたりすることができます.
先ほど, User Namespaceの中にいるプロセスはProgramのロードやMapの作成はできないと述べましたが, ProgramのAttachやMapの読み書きはProgramやMapのFile Descriptorを持ってさえいればできるので, このようなことができるようになっています.
BPF Tokenによる権限移譲
BPF Tokenはv6.9から入った比較的新しめの機能で, 特権プロセスがBPF Tokenというオブジェクトを通して権限移譲をすることによって, User Namespaceで分離されたプロセスにProgramをロードさせたりMapを作成させたりすることができる機能です. TokenはUser Namespaceと紐づいており, そのUser NamespaceにいるプロセスがどのType (TC, XDP, Tracing, etc...) のProgramをロードできるか, どのType (Hash, Array, etc...) のMapを作成できるかといった CAP_BPF
だけではできなかた細かいレベルのポリシが設定できるようになっています. 移譲を受けたプロセスはBPFシステムコールを呼ぶ際にこのToken (ユーザスペースからはFile Descriptorとして扱えます) を渡すことによってカーネルに対して移譲された権限を提示することができます. ちなみにBPF TokenはFile Descriptorとして扱えますが, 作成されたUser Namespaceに紐づくので, 他のUser Namespaceに持っていっても使うことはできません.
File Descriptor Passingによる権限移譲との最大の違いはUser Namespaceの中にいるプロセスが自らProgramやMapを作成することができることです. 従来はこのような操作はInit Namespaceにいるプロセスにしかできませんでしたが, BPF Tokenの登場によってTokenを持っていればという条件付きでUser Namespaceの中からでも許可されるようになりました. 正直個人的にはTypeを絞ったとしてもそのTypeの任意のProgramをLoadするのを許容できる状況というのはいまいち想像できませんが, 必要最低限の権限しか与えないというのはセキュリティ上は意味のあることなのかなとは思います (呼べるHelperを絞るとかもできるようになればもう少し意味がありそうな気もする).
まとめ
User Namespaceの切られた非特権プロセス, あるいはコンテナにeBPFを使わせる方法を考察しました. 現状eBPFを扱うプロセスは CAP_SYS_ADMIN
など必要以上に強い権限で動いていることが多いので, 権限を落としつつもeBPFを使えるというのはセキュリティ上意義があると思います. 一方で, Rootlessコンテナにおけるネットワークのパフォーマンス問題をeBPFで解決するといったような興味深い応用は現状だと難しそうだと思いました (そもそもRootlessでできない). BPF Tokenによって粒度の細かい権限移譲のための土台ができたというのは興味深く, 今後の発展を期待したいところです.
Discussion