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

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

前回はプロセスに設定されているケーパビリティと、ファイルにあらかじめ設定しておくファイルケーパビリティについて説明しました。

今回はまず、execve(2) システムコールを使ってプログラムを実行する際にケーパビリティがどのように変化するのかを説明したあと、Ambientケーパビリティとケーパビリティバウンディングセットについて説明します。

プログラム実行時のケーパビリティ

Linux上で実行されるプログラムは、fork(2)clone(2)システムコールを使って親プロセスを複製して生成し、複製したあとにexecve(2)システムコールで目的のプログラムを実行します。

このexecve(2)でプログラムを実行する際に、カーネルは実行後のプロセスが持つケーパビリティを計算します。このときの計算は次のアルゴリズムが使われます。

P'(ambient)     = (file is privileged) ? 0 : P(ambient) ...(1)

P'(permitted)   = (P(inheritable) & F(inheritable)) |
                  (F(permitted) & P(bounding)) | P'(ambient) ...(2)

P'(effective)   = F(effective) ? P'(permitted) : P'(ambient) ...(3)

P'(inheritable) = P(inheritable)

P'(bounding)    = P(bounding)

ここで、

P(): execve(2) 前のスレッドのケーパビリティセット

P'(): execve(2) 後のスレッドのケーパビリティセット

F(): ファイルケーパビリティセット

を示します。

execve後のケーパビリティの計算とファイルケーパビリティ

ここで一番複雑に見えるのは式(2)のPermittedケーパビリティの計算です。

式(1)のAmbientケーパビリティについてはあとで詳しく説明しますので、まずは式(2)を見てみましょう。

式(2)の最初の部分"P(inheritable) & F(inheritable)"、ふたつめの部分"F(permitted) & P(bounding)"、最後の部分P'(ambient)はORですので、いずれかで許可されれば、execve(2)後のPermittedケーパビリティで許可されます。

AmbientP'(ambient)を除いたいずれも、ファイルケーパビリティがそれぞれ関係しています。この式(2)を見れば、ファイルケーパビリティの3つのケーパビリティの役割は明確です。

ファイルケーパビリティで設定できるそれぞれのケーパビリティセットを紹介しながら、この式(2)のAmbientを除いた部分について、合わせて説明しましょう。

Permitted
ここで許可したケーパビリティは、Inheritableケーパビリティでの許可の有無に関わずexecve(2)後のPermittedケーパビリティP'(permitted)で許可されます。ただし、バウンディングセットP(bounding)で許可されている場合のみです。あとでバウンディングセットの部分で詳しく説明します
Inheritable
ここで許可したケーパビリティは、プロセスのexecve(2)前のInheritableケーパビリティP(inheritable)で許可されていれば、execve(2)後のPermittedケーパビリティP'(permitted)で許可されます
Effective
ファイルケーパビリティのEffectiveケーパビリティは、他のふたつと違って0 or 1の単一の値です

式(3)のように、ファイルケーパビリティのEffectiveケーパビリティが、

設定されている場合
アルゴリズムで計算したexecve(2)後のPermittedケーパビリティP'(permitted)の値がexecve(2)後のEffectiveケーパビリティP'(effective)に設定されます
設定されていない場合
execve(2)後のAmbientケーパビリティの値P'(ambient)execve(2)後のEffectiveケーパビリティP'(effective)に設定されます

Ambientケーパビリティ

ここまで説明したファイルケーパビリティを使えば、一般ユーザに必要なケーパビリティを与えてプロセスを実行できます。先のpingコマンドのように、システム上のユーザ誰にでも必要なケーパビリティを与えたいという場合にはファイルケーパビリティが有効です。

ところがファイルケーパビリティはファイルに属性を持たせますので、誰が実行した場合でもその特権を与えた状態でプロセスが実行されます。

セキュリティ的に必要な特権を与える範囲を最小限に限定したいという場合、例えば一般ユーザ権限で必要な特権を持ったプログラムは実行したいけれども、誰でもそのプログラムを実行できては困るという場合には対応できません。

親プロセスが持っているケーパビリティの一部だけを継承し、一般ユーザ権限でプロセスを実行できれば、不要に広い範囲にケーパビリティを与えることにはなりません。

このような場合に使うのがAmbientケーパビリティです。このAmbientケーパビリティは比較的新しい機能で、Linux 4.3で追加されました。

この機能が追加されるまで、先に紹介したアルゴリズムは次のようにAmbientがないアルゴリズムでした。

P'(permitted) = (P(inheritable) & F(inheritable)) |
                (F(permitted) & cap_bset)

P'(effective) = F(effective) ? P'(permitted) : 0

P'(inheritable) = P(inheritable)

(cap_bsetはバウンディングセット)

このアルゴリズムでもInheritableケーパビリティがありますので、必要最小限の任意のケーパビリティを持った子プロセスを生成できそうな気がします。しかし、実はいくら特権を持っていたとしても、このように誰にでもケーパビリティを与えたくないという要件は満たせません。

なぜなら、先に説明したようにexecve(2)で生成したプロセスにケーパビリティを与えるには、ファイルケーパビリティを設定しないとP'(permitted)で目的のケーパビリティセットを有効にできません。その結果、生成するプロセスでのケーパビリティセットP'(effective)でもケーパビリティを有効にできません。

ファイルケーパビリティを設定してしまえば、先に述べたような、与える特権を必要最小限にしたいという要求を満たせません。

そこで登場したのがAmbientケーパビリティです。Ambientケーパビリティは特権を持たないプロセスexecve(2)の前後で継承されるケーパビリティです。

Ambientケーパビリティは、設定する時点でPermittedケーパビリティとInheritableケーパビリティの両方で目的のケーパビリティが有効にされていなければ設定できません。

また、PermittedケーパビリティとInheritableケーパビリティから目的のケーパビリティが削除されると、Ambientケーパビリティからもそのケーパビリティは削除されます。

そして、アルゴリズムの(1)のように、setuidsetgidファイルケーパビリティといった、ファイル自体に特権を与えるような設定がされていない場合にのみAmbientケーパビリティが子プロセスに継承されます。

そして(2⁠⁠、⁠3)の式のようにファイルケーパビリティに関わらず、Ambientケーパビリティは継承されます。

先に書いたような目的の場合に、誰もが期待するように動作するすっきりとした機能です。

Ambientケーパビリティは説明だけでも比較的わかりやすい機能かもしれませんが、一応実行例を示しておきましょう。

前回の実行例のように、ファイルケーパビリティもsetuidも設定されていないpingコマンドをAmbientケーパビリティを使って実行してみましょう。

UbuntuやCentOS 8でインストールされるlibcap 2.25にはAmbientケーパビリティを操作する機能がありませんので、次の例はPlamo Linux 7.1上でlibcap 2.27を使っています。

$ cp /bin/ping . (コピーしたのでファイルケーパビリティは外れる)
$ /sbin/getcap ./ping (ファイルケーパビリティは設定されていない)
$ sudo capsh --caps="cap_setpcap,cap_setuid,cap_setgid+ep cap_net_raw+ip" \ (1)
> --keep=1 \ (2)
> --uid=1000 \ (3)
> --addamb="cap_net_raw" \ (4)
> --print \ (5)
> -- -c "./ping -c 1 127.0.0.1"
Current: = cap_net_raw+ip cap_setgid,cap_setuid,cap_setpcap+p (1で指定したケーパビリティが設定されている)
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Ambient set =cap_net_raw (Ambientケーパビリティが設定されている)
Securebits: 020/0x10/5'b10000
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: yes (unlocked) (--keepオプションを指定したのでyesになっている)
 secure-no-ambient-raise: no (unlocked)
uid=1000(karma)
gid=0(root)
groups=0(root),1(bin),2(daemon),3(sys),4(adm),5(tty),6(disk),10(wheel),11(floppy)
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.023 ms

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

引数も実行結果も長いのでかえってわかりづらいかもしれませんね(^^)。何をやっているのかを簡単に説明しましょう。capshコマンドは指定した順でオプションが処理されますので、指定する順番が変わるとエラーになる可能性があります。

オプションを与えた順に処理されますので、この例のように実行すると、

  1. Ambientケーパビリティを設定するために
    • cap_setpcapを親プロセスcapshコマンド)に設定(このケーパビリティがないとIhneritableにケーパビリティを設定できません)
    • uid=1000でコマンドを実行するためにcap_setuid,cap_setgidを親プロセスに設定
    • Permitted、InheritableケーパビリティがないとAmbientに設定できないのでcap_net_rawを親プロセスに設定
  2. --keep=1はあとで説明するsecurebitsフラグを設定(このフラグを設定する際にもcap_setpcapが必要)
  3. 一般ユーザ権限で実行するために--uid=1000を指定
  4. --addamb=cap_net_rawpingコマンドの実行に必要なcap_net_rawをAmbientケーパビリティに設定
  5. capsh実行時の状態を確認するために--printオプションを指定

Current行でオプションで設定したケーパビリティが設定されていること、Ambient set行でcap_net_rawが設定されていることが確認できます。

Ambientケーパビリティを設定したので、pingコマンドが実行できています。

securebitsフラグ

通常は、特権を持ったプロセス(スレッド)のUIDが、0から特権を持たない0以外に変化する際、そのプロセスはケーパビリティを失います。特権を持たないプロセスになるわけですから、ケーパビリティを失うのはセキュリティの観点から言っても納得できる動きです。

先の実行例ではroot権限で実行するcapshを--uidオプションを使って0から1000に変更しようとしています。何もしなければ、せっかく--capsオプションで与えたケーパビリティが失われてしまいます。

この実行例のように、UIDを変更する際でもケーパビリティを保持し続けたままにしたいケースがあります。その他にもroot権限で実行されているプロセスのケーパビリティに関する扱いを変えたい場合があります。

そのときのための機能として、カーネルではsecurebitsフラグが実装されています。securebitsフラグはpcrtl(2)システムコールを使って指定し、ケーパビリティと同様にスレッドごとに設定されます。このフラグを設定するにはCAP_SETPCAPケーパビリティが必要です。

先の例で--keep=1というオプションを指定したのが、UIDが0から1000に変わる際にもケーパビリティを維持する指定です。コマンド実行の結果に"secure-keep-caps: yes (unlocked)"という行があるのがsecurebitsにフラグが設定されていることを示しています。

securebitsフラグに指定できるフラグは現時点では4つほどあります。先の実行例でSecurebitsという行があり、そのあとに4つある項目がsecurebitsに設定できるフラグです。それぞれの機能について詳しくはcapabilities(7)をご覧ください。

バウンディングセット

最後にバウンディングセットについて少し詳しく説明しておきましょう。

バウンディングセットにはふたつの役割があり、スレッドごとに設定されます(2.6.24 以前はシステム全体で共通の値でした⁠⁠。

  • execve(2)実行時に取得できるケーパビリティを制限する役割
  • capset(2)システムコールでスレッドのケーパビリティを設定する際に制限をかける役割

まずはひとつめの役割を説明しましょう。

上にあげたプログラム実行時のアルゴリズムのP'(permitted)の式(2)で"F(permitted) & P(bounding)"とあるように、ファイルケーパビリティでPermittedケーパビリティが指定されていても、バウンディングセットで許可されていないケーパビリティは許可されません。

$ cp /bin/ping .
$ sudo setcap "cap_net_raw+p" ./ping (permittedにcap_net_rawを設定)
$ id -u
1000
$ ./ping -c 1 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.007 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.007/0.007/0.007/0.000 ms

上の例はコピーしたpingに設定するファイルケーパビリティで、Permittedケーパビリティにcap_net_rawを設定して実行している例です。Permittedケーパビリティが有効になっているので一般ユーザーでも実行できていますが、次のようにバウンディングセットでcap_net_rawを落としたシェルから実行すると実行できません。

$ sudo capsh --drop="cap_net_raw" --uid=1000 --
$ grep Cap /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffdfff
CapAmb: 0000000000000000
(バウンディングセットからcap_net_rawは落とされている)
$ id -u
1000
$ getcap ./ping (ファイルケーパビリティでcap_net_rawは設定されている)
./ping = cap_net_raw+p
$ ./ping 127.0.0.1
ping: socket: Operation not permitted

このようにexecve(2)でプログラムを実行する際に取得できるケーパビリティを制限できます。

しかし先に説明した通り、同じ式(2)にある"P(inheritable) & F(inheritable)"とのORですので、この式で許可されていれば、バウンディングセットで許可されていなくてもケーパビリティを獲得できてしまいます。

また同様にAmbientP'(Ambient)ケーパビリティセットともORですので、Ambientケーパビリティで許可されているケーパビリティも獲得できます。

pingに対してファイルケーパビリティでInheritableケーパビリティを設定して、プロセスのInheritableケーパビリティを設定して試してみましょう。

$ sudo setcap "cap_net_raw+pi" ./ping (permittedとinheritableを設定)
$ sudo capsh --caps="cap_setpcap,cap_setuid+eip" --inh="cap_net_raw" --drop="cap_net_raw" --uid=1000 --
(inheritableにcap_net_rawを設定しつつ、バウンディングセットからはcap_net_rawを落としてシェルを実行)
$ grep Cap /proc/self/status
CapInh: 0000000000002000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffdfff
CapAmb: 0000000000000000
(cap_net_rawはバウンディングセットで設定されていないがinheritableでは設定されている)
$ getpcaps $$ (getpcapsコマンドでもinheritableが設定されていることを確認)
Capabilities for `8346': = cap_net_raw+i
$ ./ping -c 1 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.008 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.008/0.008/0.008/0.000 ms

以上のようにバウンディングセットでcap_net_rawを無効にしたにも関わらず、ファイルとプロセスでInheritableケーパビリティで有効になっているためにpingコマンドは実行できました。

これではプロセスが取得できるケーパビリティを制限できないではないか、と思われるかもしれません。しかし、バウンディングセットにはもうひとつ役割があります。

もうひとつの役割は、capset(2)システムコールでスレッドが自身でケーパビリティを設定する際に制限をかける役割です。capset(2)でInheritableケーパビリティを追加する場合、バウンディングセットで設定されているケーパビリティのみInheritableケーパビリティに追加できます。

先の実行例のcapshに指定するオプションの順を少し変えて、--inhでInheritableケーパビリティを設定する前に、--dropでバウンディングセットからcap_net_rawを削除してみましょう。

$ sudo capsh --caps="cap_setpcap,cap_setuid+eip" --drop="cap_net_raw" --inh="cap_net_raw" 
Unable to set inheritable capabilities: Operation not permitted

このようにバウンディングセットで許可していないと、Inheritableケーパビリティに追加しようとするとエラーになります。

つまり、一度バウンディングセットからケーパビリティが削除されると、それ以降はプロセスのInheritableケーパビリティP(inheritable)にそのケーパビリティを追加できません。ファイルケーパビリティのInheritableケーパビリティF(inheritable)で許可をしたとしても、"P(inheritable) & F(inheritable)"の計算でケーパビリティは許可されないことになりますので、結局execve(2)実行後はそのケーパビリティはPermittedケーパビリティP'(permitted)には持てないことになります。

以上のようにバウンディングセットはexecve(2)の前後や、その子孫で取得できるケーパビリティを制限する役割を持っています。

まとめ

ここまでで、Linuxカーネルが持つケーパビリティの機能について一通り説明しました。

ケーパビリティは複雑な機能です。マニュアルをすみずみまで行ったり来たりしながら読まないとなかなか理解できないと思います。実は筆者は理解しようとして何度も挫折しており、今回の記事を書くために色々調べたり試したりしてようやく納得できた気がしています。

今回のケーパビリティの記事を書くに当たっては、筆者は理解に自信がなかったため、udzuraさんと、コンテナに関するすばらしい記事をお書きの@hayajoさんにレビューをしていただき、役に立つフィードバックをいただきました。ありがとうございました。

また、日本語でケーパビリティについて解説している色々な記事を参考にしましたので、最後に参考文献として挙げておきます。今回の記事でわかりづらい部分が合った場合に参照すると理解が進むかもしれません。

次回は、今回のケーパビリティの記事を書くきっかけとなった、ファイルケーパビリティのコンテナ関連の機能ついて紹介する予定です。

参考文献

ケーパビリティで権限を少しだけ与える (いますぐ実践! Linux システム管理)
実行例が豊富です
コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう (エンジニアHub)
今回レビューいただいた@hayajoさんの記事
明日使えない Linux の capabilities の話 (@nojima's blog)
Ambientケーパビリティについてわかりやすく書かれています
Linux Capability - ケーパビリティについての整理 (ローファイ日記)
udzura さんの記事

おすすめ記事

記事・ニュース一覧