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

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

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

先日,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
(名前空間の外で実行したため実行できない)

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

※1)
Ubuntu 16.04のカーネルは4.4ですが,この機能がバックポートされているため,ファイルケーパビリティが設定できます。

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

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

この機能がカーネルにマージされたのは"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を表示できます。

著者プロフィール

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

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

Plamo Linuxメンテナ

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