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

第44回Linuxカーネルのケーパビリティ[3]

先日、CloudNative Days Kansai 2019というイベントに参加してきました。CloudNative方面に疎い私にとって勉強になり、かつ楽しいイベントでした。さらにアフターパーティーやその後の場で、今をトキメクCloudNative界の方々にこの連載をご覧いただいているという話を聞いたりして、書いてよかったと思える瞬間でもありました。実は参加するだけではなく、朝キーノートが始まるまでの受付もお手伝いしていましたので、このイベントに参加された方は私が受付を行ったかもしれません(^^)。

さて今年もAdvent Calendarの季節がやってきました。この記事は、Linux Advent Calendar 2019の10日目の記事です。

毎年この連載でLinux Advent Calendarに参加していますので、⁠連載でAdvent Calendarにエントリ」ということに驚く方も少なくなったかもしれませんね(^^)。

前回前々回でケーパビリティについて一通り説明しました。今回も引き続きケーパビリティのお話です。前々回に少し書いたように、今回ケーパビリティの記事として最初に記事として書こうとしたのは今回のお話で、その前提知識としてケーパビリティ全体の話もしないといけないな、と思って書いているうちに肥大化したのが前回、前々回の記事でした。

コンテナとファイルケーパビリティ

前回、前々回で紹介したように、ファイルケーパビリティを設定して、限られた特権を持った状態でコマンドを実行できます。

ここで問題になってくるのが、ユーザ名前空間を使った非特権コンテナ内でのファイルケーパビリティの扱いです。ユーザ名前空間は第16回で紹介したとおり、一般ユーザでコンテナを起動するために、名前空間内のrootと名前空間外の一般ユーザのUID/GIDをマッピングする機能です。

実は、少し前まではユーザ名前空間内ではファイルケーパビリティは機能しませんでした。

これは、ユーザ名前空間内ではroot権限で実行しているように見えでも、実際はユーザ名前空間の外では一般ユーザ権限でプログラムが実行されるためです。

ユーザ名前空間は一般ユーザで作成できます。ユーザ名前空間内のrootがファイルケーパビリティを設定できるとすると、一般ユーザが名前空間を作成し、自身のUIDを名前空間内のrootにマッピングして、プログラムファイルにファイルケーパビリティを設定し、ホスト上で権限を昇格できてしまいます。セキュリティの観点からできなかったことは納得できます。

まずは古いカーネルではユーザ名前空間内でファイルケーパビリティが設定できないことを確認したあとに、どのようにユーザ名前空間内でファイルケーパビリティを設定できるようにしているのかを見ていきましょう。

古いカーネルのユーザ名前空間内のファイルケーパビリティ(4.13以前)

Plamo 7.1上で4.12.7カーネルをインストールした環境です。libcapのバージョンは2.27です。

$ uname -r
4.12.7-plamo64

LXCを使って一般ユーザ権限でコンテナを作り、起動します。

$ id -u
1000 (一般ユーザ)
$ lxc-start c1 (一般ユーザ権限でコンテナを起動)
$ lxc-info -p c1
PID:            8246
(コンテナのPIDを確認)
$ cat /proc/8246/{u,g}id_map
         0     200000      65536
         0     200000      65536
(ユーザ名前空間内のrootはUID:200000のユーザにマッピングされている)
$ ps aux | grep 8246
200000    8246  0.0  0.0   2472   788 pts/1    Ss+  02:10   0:00 init [3]
(UID:200000でコンテナが起動している)

このコンテナ上でファイルケーパビリティを設定してみます。

$ lxc-attach c1 (コンテナ内に入る)
root@c1:~# id -u
0 (rootユーザ)
root@c1:~# cp /bin/ping .
root@c1:~# /sbin/getcap ./ping
(ファイルケーパビリティは設定されていない)
root@c1:~# /sbin/setcap cap_net_raw+p ./ping
Failed to set capabilities on file `./ping' (Operation not permitted)
(rootなのにファイルケーパビリティが設定できない)
root@c1:~# grep Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
(プロセスでケーパビリティが削られているわけではない)

rootで実行しているにも関わらずファイルケーパビリティが設定できません。ファイルケーパビリティを設定するためのケーパビリティが削られているわけでもありません。

以上のように、4.13カーネル以前では非特権コンテナ内でファイルケーパビリティは設定できません。4.13カーネルまではOS起動後に作られる初期の名前空間でしかファイルケーパビリティを設定できませんでした。

ユーザ名前空間内のファイルケーパビリティ(4.14以降)

これを解決する機能がカーネルにマージされたのは4.14カーネルです(※1)

ユーザ名前空間内でファイルケーパビリティが設定できることを、新しいカーネルを使って確認してみましょう。

Plamo 7.1上の5.3カーネルで確認しています。libcapは先の実行例と同じ2.27です。

$ uname -r
5.3.11-plamo64 (5.3.11カーネルで起動している)
$ id -u
1000
$ lxc-start c1
$ lxc-info -p c1
PID:            8668
$ cat /proc/8668/{u,g}id_map
         0     200000      65536
         0     200000      65536
$ ps aux | grep 8668
200000    8668  0.0  0.0   2468  1740 pts/1    Ss+  17:55   0:00 init [3]
(UID:200000でコンテナが起動している)

先の実行例と同じように一般ユーザ権限でコンテナを起動しました。確かにUID:200000でコンテナが起動しており、コンテナ内のrootはUID:200000にマッピングされています。

このコンテナ内でファイルケーパビリティを設定してみましょう。

$ lxc-attach c1
root@c1:~# id -u
0
root@c1:~# cp /bin/ping .
root@c1:~# /sbin/getcap ./ping
(ファイルケーパビリティは設定されていない)
root@c1:~# /sbin/setcap cap_net_raw+p ./ping
(ファイルケーパビリティを設定してもエラーは発生しない)
root@c1:~# /sbin/getcap ./ping 
./ping = cap_net_raw+p
(ファイルケーパビリティが設定されている)

このように非特権コンテナ内でファイルケーパビリティが設定できました。

このpingコマンドが実行できるかも確認しておきましょう。

root@c1:~# su - gihyo
$ id -u
1000 (一般ユーザ)
$ ./ping -c1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.029 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms
(pingが実行できた)

このように、非特権コンテナ内でファイルケーパビリティが設定できて機能しています。ただ、コンテナの外でこのファイルケーパビリティが機能するとセキュリティ上の問題となりますので、コンテナ外では機能しないことも確認しておきましょう。

まずはコンテナの外でファイルケーパビリティを確認してみます。ホスト環境上で上の例で使ったpingコマンドを確認します。

$ cd ~/.local/share/lxc/c1/rootfs/home/gihyo/
(ホスト環境上でコンテナのファイルシステムがある場所に移動)
$ ls -l ./ping
-rwxr-xr-x 1 200000 200000 183,872 12月  1日  18:03 ./ping*
(ユーザ名前空間内のrootにマッピングされていたUID所有になっている)
$ /sbin/getcap ./ping
./ping = cap_net_raw+p
(ファイルケーパビリティが設定されている)

このように、先にコンテナ内で設定したファイルケーパビリティが設定されていることが確認できます。このpingコマンドを実行してみましょう。

$ id -u
1000 (一般ユーザ)
$ ./ping -c1 127.0.0.1
./ping: socket: Operation not permitted
(名前空間の外で実行したため実行できない)

ファイルケーパビリティが設定されているにも関わらずエラーになっており、セキュリティ上の問題が起こらないようになっています。

カーネルデータ構造の変更

上の例では、ファイルケーパビリティが非特権コンテナ内でのみ機能し、コンテナの外でファイルを実行しようとした場合はエラーになりました。これがどのように実現されているかをもう少し深く追ってみましょう。

この機能がカーネルにマージされたのは"Introduce v3 namespaced file capabilities"というパッチです。

4.13カーネルまでは、ファイルケーパビリティ用に次のような構造体のみが定義されていましたinclude/uapi/linux/capability.h:66行目付近⁠。

#define VFS_CAP_REVISION_2    0x02000000
  : (略)
struct vfs_cap_data {
        __le32 magic_etc;            /* Little endian */
        struct {
                __le32 permitted;    /* Little endian */
                __le32 inheritable;  /* Little endian */
        } data[VFS_CAP_U32];
};

上記のvfs_cap_data構造体に加えて4.14カーネルで、前述のパッチにより次のような構造体が新たに定義されましたinclude/uapi/linux/capability.h:82行目付近⁠。

#define VFS_CAP_REVISION_3      0x03000000
  : (略)
struct vfs_ns_cap_data {
        __le32 magic_etc;
        struct {
                __le32 permitted;    /* Little endian */
                __le32 inheritable;  /* Little endian */
        } data[VFS_CAP_U32];
        __le32 rootid;
};

4.13カーネル以前からあるvfs_cap_data構造体と、4.14カーネルで追加されたvfs_ns_cap_data構造体は、vfs_ns_cap_data構造体の最後にrootidという変数が追加されている以外は同じです。

このvfs_ns_cap_data構造体は、ファイルケーパビリティで設定できる次の情報が設定されます。

  • permitted変数: Pemittedケーパビリティセット
  • inheritable変数: Inheritableケーパビリティセット
  • magic_etc変数: Effectiveケーパビリティや、ファイルケーパビリティのバージョン
  • rootid変数: ユーザ名前空間内のrootユーザにマッピングされている名前空間外のUID

magic_etc変数には、ファイルケーパビリティでは0 or 1の情報であったEffectiveケーパビリティや、ファイルケーパビリティのバージョンを設定します。

ファイルケーパビリティのバージョンは、rootidを含む情報であれば、4.14カーネルで追加された定義であるVFS_CAP_REVISION_3(バージョン3ということですね)を設定します。rootidが設定されていないファイルケーパビリティであれば、magic_etcにはVFS_CAP_REVISION_2が設定されています。

このファイルケーパビリティのバージョンで、カーネルはファイルケーパビリティに名前空間の情報が含まれているかどうかを判断します。

rootid変数がユーザ名前空間用の変数で、ユーザ名前空間内のrootとマッピングされているUIDが代入されます。カーネルはこのrootidの値と名前空間にマッピングされているUIDの一致を確認して、ファイルケーパビリティを使うかどうか判断します。

libcapは2.26でこの機能を扱えるようになり、同時にsetcapgetcapコマンドにも-nオプションが追加されています。

先の例で、ファイルケーパビリティを設定したコンテナ内でgetcap -nを実行してみましょう。

$ lxc-attach c1 (コンテナ内に入る)
root@c1:~# /sbin/getcap -n ./ping
./ping = cap_net_raw+p [rootid=200000]

このコンテナは、コンテナ内のrootがコンテナ外のUID:200000とマッピングされていましたので、このように[rootid=200000]という表示が追加されています。

このようにgetcapコマンドで-nを使うと、rootidに設定されているUIDを表示できます。

名前空間外から名前空間内で使うファイルケーパビリティを設定する

ここまでは、ユーザ名前空間内でファイルケーパビリティを設定していました。ユーザ名前空間の外から名前空間内のファイルケーパビリティを操作したり確認したりもできます。

まずはgetcapです。getcap-nオプションはコンテナ外からも同じように使えます。

$ /sbin/getcap -n ~/.local/share/lxc/c1/rootfs/home/gihyo/ping
(コンテナ外でgetcapコマンドを実行)
/home/gihyo/.local/share/lxc/c1/rootfs/home/gihyo/ping = cap_net_raw+p [rootid=200000]

setcap-nオプションで、コンテナ外からあらかじめコンテナ内のファイルにファイルケーパビリティを設定できます。コンテナ内でこのオプションを指定するとエラーになります。setcapコマンドで-nオプションを使って、rootidにUIDを設定できます。

ここまでの例と同様にUID:200000とマッピングされるコンテナ向けにpingコマンドをコピーし、コンテナ内のrootとマッピングされるUID:200000を指定してsetcapコマンドを実行します。

# cd /home/karma/.local/share/lxc/c1/rootfs/home/karma/
(コンテナ外でコンテナの/に移動)
# cp ../../bin/ping .
# /sbin/getcap -n ./ping
(ケーパビリティは設定されていない)
# /sbin/setcap -n 200000 cap_net_raw+p ./ping
(UID:200000を指定してファイルケーパビリティを設定)
# /sbin/getcap -n ./ping
./ping = cap_net_raw+p [rootid=200000]
(設定された)

このようにコンテナ外から準備をした状態でコンテナを起動し、コンテナ内でファイルケーパビリティを設定したpingコマンドを実行してみましょう。

$ lxc-start c1
$ lxc-attach c1
root@c1:~# /sbin/getcap -n ./ping
./ping = cap_net_raw+p [rootid=200000]
(コンテナ外で設定した通りにコンテナ内でも見える)
root@c1:~# su - gihyo
gihyo@c1:~$ id -u
1000 (一般ユーザ)
gihyo@c1:~$ ./ping -c1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.029 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms
(pingが実行できた)

コンテナ外で設定したrootid=200000がコンテナ内でも設定できていることがgetcapコマンドで確認できました。そして一般ユーザ権限でpingコマンドが実行できており、コンテナ外で設定したファイルケーパビリティが機能していることがわかります。

コンテナイメージを作成する際はコンテナ起動前のホストOS上で実行しますので、このようにコンテナ外からコンテナ内で有効なファイルケーパビリティが設定できることは重要です。

それでは、このUID:200000のユーザ権限で、コンテナ外でファイルケーパビリティが設定されたpingコマンドが実行できるでしょうか? 試してみましょう。

$ id
uid=200000(gihyo) gid=200000(gihyo) groups=200000(gihyo)
(ホスト上でUID:200000のgihyoユーザになっている)
$ ls -l ./ping
-rwxr-xr-x 1 gihyo gihyo 183,872 12月  1日  22:49 ./ping
$ /sbin/getcap -n ./ping
./ping = cap_net_raw+p [rootid=200000]
(rootid=200000でファイルケーパビリティが設定されている)
$ ./ping -c1 127.0.0.1
./ping: socket: Operation not permitted
(実行できない)

ファイルケーパビリティは設定されているものの、ユーザ名前空間内にいないのでコマンドの実行はエラーになっています。

もうひとつ、rootidにコンテナのrootにマッピングされていないUIDを設定して、コンテナ内でファイルケーパビリティが機能していないことを確認しておきましょう。皆さん、結果は想像できますね。

$ sudo setcap -n 100000 cap_net_raw+p .local/share/lxc/c1/rootfs/home/gihyo/ping
(コンテナ外からrootid=100000でファイルケーパビリティを設定)
$ /sbin/getcap -n .local/share/lxc/c1/rootfs/home/gihyo/ping
.local/share/lxc/c1/rootfs/home/gihyo/ping = cap_net_raw+p [rootid=100000]
(設定されている)

このようにrootidを100000に設定してみました。

$ id -u
1000 (一般ユーザ)
$ /sbin/getcap -n ./ping
./ping = cap_net_raw+p [rootid=100000]
(rootid=100000でファイルシステムケーパビリティが設定されている)
$ ./ping 127.0.0.1
./ping: socket: Operation not permitted
(rootidが名前空間にマッピングされているUIDと異なるので実行できない)

以上のように、必要なチェックを行った上でファイルケーパビリティが機能するようになっています。このときの条件は次のような条件です。

  • ユーザ名前空間内で実行されている
  • ユーザ名前空間内のrootとマッピングされている名前空間外のUIDが、ファイルケーパビリティに設定されているrootidと一致している

まとめ

今回は、4.14カーネルから非特権コンテナ内でファイルケーパビリティが設定できるようになったことを紹介しました。

このような名前空間内で機能するファイルケーパビリティは、名前空間内でのみ機能するようになっています。

そして、ファイルケーパビリティには名前空間内のrootとマッピングされているUIDの情報が記録されており、実行時には名前空間のrootにマッピングされているUIDとファイルケーパビリティに記録されているUIDが照合されます。

このように、非特権コンテナ内でも、ファイルケーパビリティを使って限定的に特権を与えてファイルが実行できるようになりました。

参考文献

おすすめ記事

記事・ニュース一覧