前回の連載が掲載されたあと、久々にコンテナの勉強会をオンラインで開催しました。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] 実際はユーザー空間のプログラムとカーネルの間にglibcなどのライブラリが存在するので、プログラムから使うのはライブラリのインターフェースですが、ここでは説明を簡単にするために「ユーザー空間」「 カーネル空間」のみを取り上げています。
seccomp
さて、「 システムコールが発行されたのか知ることができません」 、「 必要な検査を行う方法がありません」と書きましたが、実はこのようなシステムコールの発行を検知したり、検査を行う機能はすでにLinuxカーネルには実装されています。
これはseccomp と呼ばれ、タスクが発行するシステムコールをフィルタリングする機能です。
seccompは2005年、2.6.12カーネルではじめて導入された機能です。この時点でのseccompは、決められたごく一部のシステムコールのみの実行を許可するだけの機能でした。
その後、3.5カーネルで柔軟な設定ができるseccomp mode 2が導入され、システムコールごとに制限ができるようになりました。また、呼ばれたシステムコールの種類をチェックするだけでなく、システムコールに与えられた引数も検査した上でシステムコールを実行するかどうか決定できます。
フィルタリングの指定は、ホワイトリスト的な指定、ブラックリスト的な指定のどちらでも指定できます。つまり、すべてのシステムコールの実行を制限した上で許可したいシステムコールを指定すできますし、逆にすべてのシステムコールの実行を許可した上で制限したいシステムコールを指定することもできます。
図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を受け渡し
ファイルディスクリプタを渡されたコンテナマネージャは、このファイルディスクリプタに対してioctl
を呼び出し、必要なデータが格納されるのを待ちます。
そして、フィルタに設定したシステムコールが実行されると、カーネルは図4 の②のようにこのファイルディスクリプタへ通知を送ります。そしてシステムコールの実行はブロックされます。
コンテナマネージャは②で送られた通知を読み取ります。この通知からは、呼び出されたシステムコール、システムコールを呼び出したプロセスのPID、システムコールが実行されたアーキテクチャ、システムコールの引数などがわかります。
図4 seccomp notify fd経由のシステムコールの実行
ここでコンテナマネージャ内で必要な検査を行い、同じファイルディスクリプタへ検査結果を返します(図4 の③) 。
もしコンテナマネージャが呼び出されたシステムコールによる操作が許可できる操作であると判断した場合は、カーネルはそのままシステムコールを実行し、結果をシステムコールを呼び出したプログラムへ返します(図4 の④)( ※3 ) 。
もしコンテナマネージャが呼び出されたシステムコールによる操作を許可しない場合は、エラーを返すと通常のseccompで行うフィルタリングのようにシステムコールを実行せずに呼び出したプログラムへエラーを返します(図4 の④') 。
これがseccomp notify機能の概要です。
LXDでのseccomp notify機能の実装
それではseccomp notify機能の動きを実際に見ていきましょう。先に書いた通り、今回はLXDを使ってコンテナを作成し、seccomp notify機能を設定して機能を試していきます。LXDでは、特に指定しなければ一般ユーザー権限でコンテナが起動します。
LXCプロジェクトの開発者はカーネルにも積極的にコンテナ関連機能の実装を進めており、seccomp notify機能の実装も、LXCプロジェクトの開発者を中心に開発されています。
このため、カーネル側での実装とともにLXDでもseccomp notify機能の実装が進めることができ、カーネルで実装されてからかなり短い期間でLXDにもこの機能が実装されています。
デバイスファイルの作成
最初にseccomp notify機能がサポートされたのは、LXDでは5.0カーネルリリースから2ヶ月後のLXD 3.13 です。LXD 3.13では、まずはmknod
とmknodat
システムコール が使えるようになり、デバイスファイルが作成できるようになりました。
LXDが依存しているLXCでは3.2.1 から、libseccompは2.5.0からseccomp notify機能が使えます。LXDはLXC、libseccompを必要としていますので、これらの新しいライブラリが必要です。
LXDでmknod
、mknodat
が使えるようになったと言っても任意のデバイスファイルが作成できるわけではありません。許可されているデバイスは先に紹介した/dev/null
や/dev/zero
などの一部のデバイスだけです。詳細は公式ドキュメント (日本語訳 )に記載があります。
それでは、デバイスファイルを作成してseccomp notify機能を試してみましょう。今回の実行例はUbuntu 20.04.1にsnapでインストールしたLXD 4.8という環境で実行しています。snapであれば前述のようなライブラリの依存関係を考えることなく、seccomp notify機能が使える形で作成されていますので安心です。なお、LXDに関してはstableリリースの4.0シリーズでもseccomp notify機能が使えます。
$ snap list lxd
Name Version Rev Tracking Publisher Notes
lxd 4.8 18520 latest/stable canonical✓ -
$ lxc version
Client version: 4.8
Server version: 4.8
まずはコンテナを作成します。そしてこのコンテナが一般ユーザで起動していることを確認します。
$ lxc launch ubuntu:20.04 c1
Creating c1
Starting c1
$ lxc info c1 | grep Pid
Pid: 41463
$ ps aux | grep 41463
1000000 41463 0.0 0.4 104104 8032 ? Ss 15:05 0:00 /sbin/init
それではコンテナ内に入ってデバイスファイルが作成できるか試してみましょう。
$ lxc shell c1
root@c1:~# mknod my-dev c 1 5
mknod: my-dev: Operation not permitted
失敗しました。デフォルトではデバイスファイルの作成は許可されていませんのでこれは当然の動作です。
それでは、LXDで設定してデバイスファイルの作成を許可して試してみましょう。許可するには、LXDの設定でコンテナに対してsecurity.syscalls.intercept.mknod
をtrue
に設定します。
$ lxc config set c1 security.syscalls.intercept.mknod=true
$ lxc config show c1 | grep intercept
security.syscalls.intercept.mknod: "true"
設定されました。設定を反映させるためにコンテナを再起動し、再度コンテナ内でシェルを実行します。
$ lxc restart c1
$ lxc shell c1
root@c1:~# mknod my-dev c 1 5
root@c1:~# ls -l my-dev
total 1
crw-r--r-- 1 root root 1, 5 Dec 7 06:05 my-dev
無事、メジャー番号1、マイナー番号5のデバイスファイル(/dev/zero
)が作成されています。さらにもうひとつテストで作ってみましょう(/dev/random
) 。
root@c1:~# mknod my-dev2 c 1 8
root@c1:~# ls -l my-dev2
crw-r--r-- 1 root root 1, 8 Dec 7 06:08 my-dev2
問題なく作成できました。
shiftfs
次はファイルシステムのマウントを試してみましょう。その前に、この後のマウント操作で使用する機能であるshiftfsについて少しだけ紹介しておきます。
この機能は正式にカーネルにはマージされておらず、おそらくカーネルにマージされる際は別の名前で、今回紹介するshiftfsとは異なる実装となっているかもしれません[4] 。Ubuntu 20.04のカーネルではshiftfsのパッチがマージされていて使えるようになっています。
LXDに限らず、ネットワーク越しに取得するコンテナイメージや、自分で作成したコンテナイメージ内のファイルの所有権は、イメージ作成時に設定された所有権が設定されていることが普通でしょう。通常はほとんどのファイルがroot:root
となっているのではないでしょうか。
しかしユーザ名前空間を使った非特権コンテナの場合、ホストから見たコンテナイメージ内の所有権は、非特権コンテナを起動するユーザ・グループの所有になっていないと、コンテナ内ではnobody:nogroup
となってしまうなど、本来期待する所有権とは異なる設定がされていて使えない状態になります。
そこで、非特権コンテナの場合、コンテナ起動時に再帰的にchown
して所有権を期待する設定にする必要があります。
ここで、overlayfsのように重ね合わせのファイルシステムとして、オリジナルのファイルシステムを下層として、コンテナ用のファイルシステムを上層として重ね合わせた上で所有権を調整して、コンテナに提供できるようにしているのがshiftfsです。Ubuntuでは19.04でこのパッチを適用したカーネルが提供されました。
非特権コンテナからファイルシステムをマウントする場合は、この機能を使って所有権を調整する必要があるので、今回LXDでseccomp notifyを使う場合も、snapパッケージで動作するLXDがこの機能を使えるようにする必要があります。デフォルトでは次のように有効化されていません。
$ lxc info | grep shiftfs
shiftfs: "false"
そこで、まずはLXDでshiftfsが有効になるように設定しましょう。snapパッケージでインストールしたLXDでshiftfsが使えるようにするには次のようにします[5] 。
$ sudo snap set lxd shiftfs.enable=true
$ sudo systemctl reload snap.lxd.daemon
$ lxc info | grep shiftfs
shiftfs: "true"
これで準備OKです。shiftfsを有効にする前にコンテナを作成していた場合は、ここで一度コンテナを再作成します[6] 。
$ lxc delete --force c1
$ lxc launch ubuntu:20.04 c1
Creating c1
Starting c1
ファイルシステムのマウント
準備ができましたので、seccomp notify機能を使ってファイルシステムをマウントしてみましょう。LXDで非特権コンテナ内でmount
システムコール を実行し、マウントができるようになったのは3.19 からです。
ここの例では、ホストシステム上にマウントされていないパーティション/dev/sdb1
が存在しています。このパーティションはext4でmkfsしています。
$ sudo fdisk -l /dev/sdb | grep sdb1
/dev/sdb1 2048 10485759 10483712 5G 83 Linux
このsdb1
はコンテナ内ではデバイスファイルが存在しないので、デフォルトのままではマウントできません。また/dev/sdb1
はコンテナ内でデバイスファイルが作成できませんので、LXDで設定してホスト側のデバイスファイルをバインドマウントしておきましょう。
$ lxc config device add c1 sdb1 unix-block path=/dev/sdb1
Device sdb1 added to c1
$ lxc restart c1
コンテナ内に/dev/sdb1
が出現しており、マウントする準備が済みました。
root@c1:~# ls -l /dev/sdb1
brw-rw---- 1 root root 8, 17 Dec 7 14:54 /dev/sdb1
seccomp notify機能を使ってマウントする前に、コンテナ内でマウント操作ができないことを確認しておきます。
$ lxc shell c1
root@c1:~# mount /dev/sdb1 /mnt
mount: /mnt: permission denied.
マウント操作は失敗します。
ここで、コンテナに対して次の設定を行い、設定を反映させるためにコンテナを再起動します。
マウントができる設定(security.syscalls.intercept.mount
)
マウントしたファイルシステムに対してshiftfsを有効にする設定(security.syscalls.intercept.mount.shift
)
マウントできるファイルシステムとしてext4を許可する設定(security.syscalls.intercept.mount.allowed
)
$ lxc config set c1 security.syscalls.intercept.mount true
$ lxc config set c1 security.syscalls.intercept.mount.shift true
$ lxc config set c1 security.syscalls.intercept.mount.allowed ext4
$ lxc restart c1
コンテナ内のシェルからマウントしてみましょう。
$ lxc shell c1
root@c1:~# mount /dev/sdb1 /mnt
root@c1:~# df -h | grep /mnt
/mnt 4.9G 20M 4.6G 1% /mnt
マウント操作が成功し、ファイルシステムがマウントできています。
ここでは直接ext4をマウントしていますが、安全のためにコンテナ内にfuse2fsパッケージをインストールし、FUSE(fuse2fs)を使ってマウントすることもできます(security.syscalls.intercept.mount.fuse
) 。
まとめ
今回は、非特権コンテナ内でのデバイスファイルの作成とマウント操作を通してseccomp notify機能について紹介しました。
LXDでは、今回紹介したmknod
、mknodat
、mount
システムコール以外にもいくつかのシステムコールを許可する設定が追加されています。
seccomp notify機能を使うことで、これまで非特権コンテナ内で実行できなかった操作ができるようになり、非特権コンテナ活用の幅が広がりました。
今回の記事を書くに当たって、udzuraさん にレビューをしていただき、特に実装に近い部分について色々と教えていただきました。ありがとうございました。
参考文献