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

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

前回からだいぶ間隔が空いてしまいました。前回の最後に案内したudzuraさんのCRIUに関する記事はもう少し時間がかかるようですので、もうしばらく私が担当したいと思います。

今回から数回は、Linuxカーネルに実装されているケーパビリティについて説明します。ケーパビリティは2.2カーネルのころから実装されてきているかなり古くからある機能で、コンテナ向けの機能ではなく一般的に使われている機能です。もちろん、コンテナの安全性を高めるための重要な機能でもあります。

setuid

一般的にはUNIX系のOSでは、プロセスはroot権限(実効ユーザIDが0)で実行される特権プロセスと、一般ユーザ権限で実行される(実効ユーザIDが0以外の)非特権プロセスに分けられます。

一般ユーザは、通常はそのユーザの権限でプログラムを実行すれば良いのですが、一般ユーザが実行するプログラムであっても処理の内容によっては特権が必要な場合があります。例えば、ネットワークの疎通確認に良く使うpingコマンドは特権が必要で、Linuxディストリビューションによっては次のようにsetuidされており、root権限で実行するようになっています(例はUbuntu 18.04⁠⁠。

$ ls -l $(which ping)
-rwsr-xr-x 1 root root 64424 Jun 28 20:05 /bin/ping
   ↑(このsがsetuidされている印)

ケーパビリティ

先に紹介したpingコマンドは、パケット送信にRawソケットを使います。このRawソケットを使う場合には特権が必要ですのでsetuidされており、root権限で実行されていたのでした。

逆に言うとRawソケットを使うのに必要な権限のみが必要なのに、広範囲に渡るrootが持つ強い権限すべてをpingコマンドに渡していることになります。これはセキュリティ上の観点から望ましいことではありません。

Linuxにはこのような場合に使える、rootが持っている絶対的な権限を細かく分け、必要な権限だけを与える仕組みが存在します。これがケーパビリティ(capability)です。ケーパビリティはスレッドごとに設定され、スレッドは自身が持つケーパビリティを変更できます。

例えば、CAP_NET_RAWというケーパビリティがあればRawソケットを使用できます。Linuxカーネルでは、このような細分化されたケーパビリティが多数定義されており、/usr/include/linux/capability.hで定義されています。

$ grep "#define CAP_" /usr/include/linux/capability.h 
#define CAP_CHOWN            0
#define CAP_DAC_OVERRIDE     1
#define CAP_DAC_READ_SEARCH  2
#define CAP_FOWNER           3
#define CAP_FSETID           4
#define CAP_KILL             5
    :(略)

ケーパビリティの実装は結構古くて、2.2カーネル以降で使えます。しかし、カーネルの進化とともに新たなケーパビリティが追加されているので、カーネルのバージョンによって指定できるケーパビリティが異なります。どのようなケーパビリティが定義されており、それぞれのケーパビリティがどのような権限に対応しているのか、いつのカーネルから使えるようになったかについては、マニュアルcapabilities(7)に載っています。

プロセスのケーパビリティ

プロセス(実際はスレッド)はそれぞれ4種類のケーパビリティセットというものを持っています。ケーパビリティセットは、内部的にはビット列で、ケーパビリティを持っていれば1がセットされます。

Permitted
EffectiveとInheritableで持つことを許されるケーパビリティセット
Inheritable
execve(2)した際に継承できるケーパビリティセット
Effective
実際にカーネルがスレッドの実行権限を判定するのに使うケーパビリティセット
Ambient
特権のない(setuid/setgidされていない)プログラムをexecve(2)した際に子プロセスに継承されるケーパビリティセット(Linux 4.3以降で使用可能)

さらに、プロセスごとに取得できるケーパビリティセットを制限するためのCapability bounding Set(以降、バウンディングセット)というものがあります。

実際にカーネルがプロセスが持つ権限をチェックする時は、Effectiveケーパビリティをチェックします。つまりEffectiveで許可されていない操作はできません。

そして、ケーパビリティが変化する機会は3種類あります。

  • capset(2):システムコールでケーパビリティを設定する
  • execve(2):システムコールの前後でケーパビリティが変化する
  • prctl(2):システムコールでAmbientケーパビリティやバウンディングセットを設定する

execve(2)システムコールはプログラムを実行するためのシステムコールです。このシステムコールとケーパビリティの関係については次回で説明します。

実行中のプロセスが持つケーパビリティを確認する

では実際にプロセスでケーパビリティがどのように設定されているかを見てみましょう。

プロセス(スレッド)が持つケーパビリティは/proc/<PID>/statusファイルで確認できます。例えばPIDが1であるinitのPermittedCapPrm⁠、EffectiveCapEff⁠、バウンディングセットCapBndは、次のようにすべて1が設定された状態になっています。

$ grep Cap /proc/1/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

特定のケーパビリティだけが設定されている、ホストの時刻合わせを行うためのデーモンであるntpdは次のようになっています。

$ grep Cap /proc/$(pgrep ntpd)/status
CapInh: 0000000000000000
CapPrm: 0000000002000400
CapEff: 0000000002000400
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

上記の例のように status ファイルでケーパビリティを確認できます。しかし、このファイルを見ただけでは何かのビットが立っていることはわかっても、それぞれのビットが意味するケーパビリティを覚えていないと、プロセスが何のケーパビリティを持っているかはわかりません。

もう少しわかりやすくプロセスのケーパビリティセットを確認したい場合はlibcapに含まれるgetpcapsコマンドが使えます(Ubuntuの場合はlibcap2-binパッケージに含まれています⁠⁠。

$ getpcaps $(pgrep ntpd)
Capabilities for `21935': = cap_net_bind_service,cap_sys_time+ep

ポート番号1024番未満を使うには特権が必要です。しかし、ntpdは一般ユーザであるntpユーザ権限で動作しており、本来であればntpdで使うポートである123番ポートは使えないはずです。しかし、特権ポートが使えるケーパビリティであるcap_net_bind_serviceを持っているので123番ポートを使えます。

また、システムクロックを設定できるケーパビリティであるcap_sys_timeを持っていますので、本来は一般ユーザではできないシステムクロックの設定ができるわけです。

つまりntpdは必要最低限のケーパビリティのみを持った状態で実行されているということです。不要な特権を持たずにデーモンが実行されていますので、より安全性が高まるというわけです。

プロセスのケーパビリティを操作する

実際にケーパビリティによって必要な特権のみを持って動く例を見ましたので、この後は実行中のプロセスが持っているケーパビリティを操作した場合の例を見てみましょう。

特権を持っていないプロセスがいきなり特権を取得できませんので、実行中のプロセスが自身のケーパビリティを操作する場合は、持っているケーパビリティを減らしていくことになります。

ケーパビリティを持っていなければ、たとえrootであってもコマンドは実行できません。試してみましょう。libcapに含まれるcapshコマンドで簡単に試せます。ここで指定するケーパビリティは、マニュアルやcapability.hで定義されているケーパビリティを指定します(小文字でも構いません⁠⁠。

# id
uid=0(root) gid=0(root) groups=0(root)
# capsh --drop="cap_net_raw" -- -c "ping -c 1 127.0.0.1"
ping: socket: Operation not permitted
(cap_net_rawを削除して実行したのでrootユーザなのにpingが実行できない)

cap_net_rawケーパビリティを削除したときにケーパビリティがどのようになっているのかを確認してみましょう。まずはrootで普通に実行しているbashが持つケーパビリティです。

# grep Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

サポートされているケーパビリティすべてが有効になっているのがわかります。ここでcap_net_rawを削除すると次のようになります。

# capsh --drop="cap_net_raw" --
(cap_net_rawを削除してbashを実行)
# grep Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000003fffffdfff
CapEff: 0000003fffffdfff
CapBnd: 0000003fffffdfff
CapAmb: 0000000000000000

/usr/include/linux/capability.hを見ると、CAP_NET_RAWは13ですので、13ビットシフトした下位から14ビット目が0になっているのがわかります。

ファイルケーパビリティ

プロセスは、先に述べたようにケーパビリティセットを持っています。実行中にケーパビリティを変更できますが、今持っていないケーパビリティをいきなりプロセス実行中に増やすことはできません。基本的にケーパビリティは実行中に操作すると減っていくだけです。

セキュリティ的には不要な特権が減っていくわけですから意味があるわけですが、最初から特定の特権を与えておきたいケースには対応できません。先のpingコマンドに設定されていたsetuidのようなケースです。

そこでsetuidのように、あらかじめファイルにケーパビリティを設定しておくことができます。これがファイルケーパビリティです。

ファイルケーパビリティも、プロセスのケーパビリティと同様にPermittedInheritableEffectiveという3つのケーパビリティセットがあります。ただし、プロセスのケーパビリティセットと違い、ファイルケーパビリティのEffectiveケーパビリティは0 or 1の単一の値です。

次の例は CentOS 8にインストールされているpingコマンドです。

$ ls -l /bin/ping
-rwxr-xr-x. 1 root root 69160 May 11 23:22 /bin/ping
(setuidされていない)
$ 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.021 ms

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

このようにsetuidされていませんがpingコマンドは実行できています。これはファイルケーパビリティが設定されているからです。

ファイルケーパビリティを確認するにはlibcapに含まれるgetcapコマンドを使います。

$ getcap /bin/ping
/bin/ping = cap_net_admin,cap_net_raw+p

これは、/bin/pingにはファイルケーパビリティのPermittedケーパビリティとして、cap_net_admincap_net_rawケーパビリティが設定されているという意味です(※1)

ファイルケーパビリティは安全のためにコピーすると設定が外れます(※2)

$ cp /bin/ping .
$ getcap ./ping
(ファイルケーパビリティが何も設定されていない)
$ ./ping 127.0.0.1
ping: socket: Operation not permitted (権限がないので実行できない)

このようにファイルケーパビリティが設定されていないので、コピーしたpingコマンドは一般ユーザーでは実行できません。

ここでコピーしたpingコマンドを一般ユーザ権限で実行できるようにファイルケーパビリティでcap_net_rawを付与してみましょう。同じくCentOS 8で実行しています。

$ sudo setcap cap_net_raw=p ./ping (permittedをオン)
$ getcap ./ping 
./ping = cap_net_raw+p (cap_net_rawが設定された)
$ ./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.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
(実行できた)

ファイルケーパビリティを設定するには特権が必要ですので、上の例でもsetcapコマンドだけはroot権限で実行しています。

まとめ

今回は、プロセスのケーパビリティについて簡単に説明しました。実際にケーパビリティを確認する方法を紹介し、ケーパビリティを削除したときにコマンドの実行が失敗することを確認しました。

また、最初から特定のケーパビリティを与えてコマンドを実行したいときのためのファイルケーパビリティについて説明しました。ファイルケーパビリティを設定することで、一般ユーザでも特権が必要なコマンドが実行できました。

実は今回の記事は、udzuraさんの記事ができるまでのつなぎとして、カーネルの新機能に関する軽い話題で1回記事を書こうかなと考えて書き始めた記事でした。

その前提知識として、ファイルケーパビリティについて軽く触れる必要あったので書き始めたのですが、必要と思うことを加えていくにつれ量が増えていき、軽く触れるだけでは済まない量になってしまいました。そこで、ケーパビリティについてきちっとまとめてみようと思ったわけです。

次回まででケーパビリティ全体について説明を終わらせ、3回目で最初に書こうと思ったケーパビリティ関連の新機能について書く予定です。

おすすめ記事

記事・ニュース一覧