先日、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
$ cat /proc/8246/{u,g}id_map
0 200000 65536
0 200000 65536
$ ps aux | grep 8246
200000 8246 0.0 0.0 2472 788 pts/1 Ss+ 02:10 0:00 init [3]
このコンテナ上でファイルケーパビリティを設定してみます。
$ 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
Failed to set capabilities on file `./ping' (Operation not permitted)
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
$ 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でコンテナが起動しており、コンテナ内の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
コマンドを確認します。
$ cd ~/.local/share/lxc/c1/rootfs/home/gihyo/
$ ls -l ./ping
-rwxr-xr-x 1 200000 200000 183,872 12月 1日 18:03 ./ping*
$ /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行目付近 ) 。
: (略)
struct vfs_cap_data {
__le32 magic_etc ;
struct {
__le32 permitted ;
__le32 inheritable ;
} data [ VFS_CAP_U32 ];
};
上記のvfs_cap_data
構造体に加えて4.14カーネルで、前述のパッチにより次のような構造体が新たに定義されました(include/uapi/linux/capability.h:82行目付近 ) 。
: (略)
struct vfs_ns_cap_data {
__le32 magic_etc ;
struct {
__le32 permitted ;
__le32 inheritable ;
} 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でこの機能を扱えるようになり、同時にsetcap
、getcap
コマンドにも-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
/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
# /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
コンテナ外で設定したrootid=200000
がコンテナ内でも設定できていることがgetcap
コマンドで確認できました。そして一般ユーザ権限でping
コマンドが実行できており、コンテナ外で設定したファイルケーパビリティが機能していることがわかります。
コンテナイメージを作成する際はコンテナ起動前のホストOS上で実行しますので、このようにコンテナ外からコンテナ内で有効なファイルケーパビリティが設定できることは重要です。
それでは、このUID:200000のユーザ権限で、コンテナ外でファイルケーパビリティが設定されたping
コマンドが実行できるでしょうか? 試してみましょう。
$ id
uid=200000(gihyo) gid=200000(gihyo) groups=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]
$ ./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
$ /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]
$ ./ping 127.0.0.1
./ping: socket: Operation not permitted
以上のように、必要なチェックを行った上でファイルケーパビリティが機能するようになっています。このときの条件は次のような条件です。
ユーザ名前空間内で実行されている
ユーザ名前空間内のrootとマッピングされている名前空間外のUIDが、ファイルケーパビリティに設定されているrootid
と一致している
まとめ
今回は、4.14カーネルから非特権コンテナ内でファイルケーパビリティが設定できるようになったことを紹介しました。
このような名前空間内で機能するファイルケーパビリティは、名前空間内でのみ機能するようになっています。
そして、ファイルケーパビリティには名前空間内のrootとマッピングされているUIDの情報が記録されており、実行時には名前空間のrootにマッピングされているUIDとファイルケーパビリティに記録されているUIDが照合されます。
このように、非特権コンテナ内でも、ファイルケーパビリティを使って限定的に特権を与えてファイルが実行できるようになりました。
参考文献