LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第47回 非特権コンテナの可能性を広げるseccomp notify機能

この記事を読むのに必要な時間:およそ 6.5 分

前回の連載が掲載されたあと,久々にコンテナの勉強会をオンラインで開催しました。2回に渡って,cgroupをテーマにカーネルの実装に踏み込んだ内容のお話が聞けました。私もcgroup v1の内部構造についてお話しました。動画は公開されていますのでぜひご覧ください。

さて,今年も気がつけばもう12月で,Advent Calendarの季節になりました。今年はいろいろなことがありましたが,今振り返るとあっという間だった気がします。今年もこの連載で毎年参加しているLinux Advent Calendarに参加します。この記事はLinux Advent Calendar 2020の15日目の記事となります。

この連載は,名前に「LXCで学ぶ」と付いているわりには,最近まったくLXCが出てきませんでしたが(^_^;),今回は久々にLXCコンテナを使って機能の説明をしたいと思います。とは言ってもLXCを直接使うわけではなく,LXDを通して操作してみたいと思います。LXDに関してはUbuntu Weekly Recipeで結構取り上げられていますので,LXDについて詳しく知りたい場合はぜひご覧ください。

非特権コンテナが行う操作

この連載の第16回で紹介したように,Linuxカーネルにはユーザ名前空間(User Namespace)という機能があります。

この機能を使って,コンテナ内ではroot権限を持っているにも関わらずホスト上では一般ユーザ権限しか持たない非特権コンテナが実行でき,セキュアなコンテナ実行環境が実現できるようになっています。

このユーザ名前空間内のrootユーザは,コンテナ内では必要な特権(ケーパビリティ)を持っているように見えるにも関わらず,実際に特権が必要な操作を行うとエラーになることがあります。

これはもちろん,ユーザ名前空間内で特権を保持しているとはいえ,ホスト上のrootと同じ権限を持つと,ホストや他のコンテナを危険にさらす操作ができる可能性があるため,カーネル内部で危険な可能性がある操作に関してはチェックが行われているためです。これはセキュリティを考慮すると必要なチェックです。

このように非特権コンテナ(ユーザ名前空間)内では行えない操作の例として,/dev以下のデバイスファイルを作成したり,ファイルシステムをマウントする操作が挙げられます。このような操作はカーネルの機能を使うため,システムコールを使って行います。

デバイスファイルには,非特権コンテナ内で作成すると危険なデバイスファイルがある一方で,非特権コンテナ内で作成しても安全なデバイスファイルも存在します。例えば/dev/null/dev/zeroなどはコンテナ内で作成しても安全でしょう。

デバイスファイルに関しては,コンテナを起動するために,これまでもコンテナマネージャやランタイムがバインドマウントを用いて必要なデバイスファイルをコンテナ内に出現させていますので,問題になることは少なかったかもしれません。

それでも,非特権コンテナ内のプログラムがデバイスファイルを作成する処理を行うような場合は,事前にコンテナに対して定義を行い,必要なデバイスファイルをバインドマウントで出現させるという方法では対処できません。

ファイルシステムのマウントに関しては,これまでは非特権コンテナ内からは一部の疑似ファイルシステムなどをのぞいてはファイルシステムをマウントできませんでした。このため,事前にホスト上でマウントしてコンテナで利用できるようにするなど,別の方法を採る必要がありました。

事前に必要なマウントがわかっている場合は,このようにコンテナマネージャ上で定義することで解決できます。しかし非特権コンテナ内のプロセスがマウントを行うような場合,コンテナマネージャはプロセスがいつマウント操作を行うのかを知ることはできませんので,事前にコンテナマネージャで必要な操作を行うことはできませんでした。このような非特権コンテナ内からファイルシステムをマウントすることに関しては,これまで長い時間をかけて議論されてきたテーマでした。

しかし,ホストの管理者自身がホスト上で実行するコンテナを管理する場合であったり,ホストの管理者がコンテナの管理者を信頼できる場合は,カーネルで禁止されている操作であっても許可できる場合はあるでしょう。また,同じ操作であっても,コンテナによって許可したり許可しなかったりしたい場合もあるかもしれません。そのような操作をコンテナマネージャが制御できると便利です。カーネルで制御しようとすると,ある操作は一律禁止したり,許可したりという決まった動作でしか制御できません。

現在のようにコンテナ上でアプリケーションを実行することが一般的になってきている状況では,どのような操作が危険で,どのような操作が危険ではないか?というのはコンテナマネージャやコンテナマネージャを使う管理者の方がよく知っているでしょう。コンテナマネージャが許可できる操作のうち,管理者が安全と判断した操作はコンテナマネージャに許可する設定ができるほうが,セキュアで柔軟なアプリケーション実行環境ができるでしょう。

つまり

  • コンテナ内のタスクは非特権コンテナから実行できない操作を行う,つまりシステムコールを発行する可能性があります

このような場合,

  • コンテナマネージャはそのシステムコールを非特権コンテナ内で実行しても安全であることを知っている可能性があります

しかし,

  • コンテナマネージャはいつそのシステムコールが発行されるのか,発行されたのかを知ることができません
  • もし,いつ発行されるのか,発行されたのかを知っていたとしても,必要な検査を行う方法がありません

というような問題がありました。

システムコール

ここまでなんの前提もなく「システムコール」という言葉を使ってきました。今回紹介する機能を説明する前に,簡単にシステムコールについて説明しておきましょう。

通常,ユーザー空間のプログラムは直接OSのリソースを利用するような操作,つまりカーネル空間の操作はできません。カーネルに必要な操作を行う依頼を行うインターフェースとしてシステムコールが準備されています。

これによりカーネルに対する操作の共通的なインターフェースが提供でき,安全にOSリソースを利用できます。もちろんシステムコールを実行する際には,呼び出し側が実行に必要な権限を持っているかどうかのチェックが行われます。

先に書いたデバイスファイルの作成やマウントなども,もちろんシステムコールを呼びます。このようにユーザー空間のプログラムがOSに関わる操作を行う場合,図1のようにシステムコールを使い,結果を受け取ります※1⁠。

図1 システムコールの呼び出しと実行

図1 システムコールの実行

※1)
実際はユーザー空間のプログラムとカーネルの間にglibcなどのライブラリが存在するので,プログラムから使うのはライブラリのインターフェースですが,ここでは説明を簡単にするために「ユーザー空間」⁠カーネル空間」のみを取り上げています。

seccomp

さて,⁠システムコールが発行されたのか知ることができません⁠⁠,⁠必要な検査を行う方法がありません」と書きましたが,実はこのようなシステムコールの発行を検知したり,検査を行う機能はすでにLinuxカーネルには実装されています。

これはseccompと呼ばれ,タスクが発行するシステムコールをフィルタリングする機能です。

seccompは2005年,2.6.12カーネルではじめて導入された機能です。この時点でのseccompは,決められたごく一部のシステムコールのみの実行を許可するだけの機能でした。

その後,3.5カーネルで柔軟な設定ができるseccomp mode 2が導入され,システムコールごとに制限ができるようになりました。また,呼ばれたシステムコールの種類をチェックするだけでなく,システムコールに与えられた引数も検査した上でシステムコールを実行するかどうか決定できます。

フィルタリングの指定は,ホワイトリスト的な指定,ブラックリスト的な指定のどちらでも指定できます。つまり,すべてのシステムコールの実行を制限した上で許可したいシステムコールを指定すできますし,逆にすべてのシステムコールの実行を許可した上で制限したいシステムコールを指定することもできます。

図2 seccompによるシステムコールのフィルタリング

図2 seccomp

図2のように,プロセスを実行する際に,あらかじめシステムコールの実行に関するポリシーを定義しておきます。そして,実際にシステムコールが発行されると,ポリシーに従ってシステムコールを実行するかどうかが判断されます。指定したポリシーは子プロセスにも引き継がれます。

実行が許可されていないシステムコールが実行された場合の動作として,すぐにプロセスを終了させる,設定したエラー(番号)を返すなど,いくつか選択できます。システムコールの実行が失敗しても,システムコールを呼び出したプロセスの実行を中断せずに処理を続けることはできますが,システムコールは実行されません。

seccomp notify

このように,seccompは特定のシステムコール呼び出しを検出し,インターセプトできますので,先に示した問題を解決するのに一番近いところにいる機能であることは間違いありません。

しかし,コンテナ内のプロセス内でシステムコールが呼ばれたことを別のプロセスであるコンテナマネージャが知ることはできません。

また,seccompを使ってシステムコールがあたかも成功したように見せかけることはできます。しかし,いずれにせよ実際にはシステムコールは実行されません。つまりシステムコールの実行を検出し,その実行を許可するか拒否するか以外には選択肢がありませんでした。

そこで,ここまで説明した問題を解決できるようにseccompの機能を拡張したのが,今回紹介する"Seccomp notify"機能です※2⁠。この機能は5.0カーネルで導入されました。

※2)
特に正式に決まった機能名があるわけではありません。"Seccomp notify","Seccomp trap to userspace","seccomp user notifications"などと紹介されていたりします。

この機能を使うには,プロセスに設定するseccompフィルタにSECCOMP_RET_USER_NOTIFというフラグを設定します図3の①⁠⁠。するとフィルタがロードされたあと,図3の②のようにカーネルが呼び出し元のタスクにファイルディスクリプタ(fd)を返します。

呼び出し元のタスク自体は,この返されたfdを使って何かをするわけではありません。このfdをコンテナマネージャなど,他のタスクに渡します図3の③⁠⁠。

図3 seccomp notify fdを受け渡し

図3 notify fdの受け渡し

ファイルディスクリプタを渡されたコンテナマネージャは,このファイルディスクリプタに対してioctlを呼び出し,必要なデータが格納されるのを待ちます。

そして,フィルタに設定したシステムコールが実行されると,カーネルは図4の②のようにこのファイルディスクリプタへ通知を送ります。そしてシステムコールの実行はブロックされます。

コンテナマネージャは②で送られた通知を読み取ります。この通知からは,呼び出されたシステムコール,システムコールを呼び出したプロセスのPID,システムコールが実行されたアーキテクチャ,システムコールの引数などがわかります。

図4 seccomp notify fd経由のシステムコールの実行

図4 notify fd経由のシステムコール実行

ここでコンテナマネージャ内で必要な検査を行い,同じファイルディスクリプタへ検査結果を返します図4の③⁠⁠。

もしコンテナマネージャが呼び出されたシステムコールによる操作が許可できる操作であると判断した場合は,カーネルはそのままシステムコールを実行し,結果をシステムコールを呼び出したプログラムへ返します図4の④)※3⁠。

もしコンテナマネージャが呼び出されたシステムコールによる操作を許可しない場合は,エラーを返すと通常のseccompで行うフィルタリングのようにシステムコールを実行せずに呼び出したプログラムへエラーを返します図4の④'⁠⁠。

これがseccomp notify機能の概要です。

※3)
seccomp notifyの結果,カーネルがシステムコールの実行を続けられるようになったのは5.5カーネルからです。

著者プロフィール

加藤泰文(かとうやすふみ)

2009年頃にLinuxカーネルのcgroup機能に興味を持って以来,Linuxのコンテナ関連の最新情報を追っかけたり,コンテナの勉強会を開いたりして勉強しています。英語力のない自分用にLXCのmanページを日本語訳していたところ,あっさり本家にマージされてしまい,それ以来日本語訳のパッチを送り続けています。

Plamo Linuxメンテナ

Twitter:@ten_forward
技術系のブログ:http://tenforward.hatenablog.com/