前回からだいぶ間隔が空いてしまいました。前回の最後に案内したudzuraさんのCRIUに関する記事はもう少し時間がかかるようですので、もうしばらく私が担当したいと思います。
今回から数回は、Linuxカーネルに実装されているケーパビリティについて説明します。ケーパビリティは2.2カーネルのころから実装されてきているかなり古くからある機能で、コンテナ向けの機能ではなく一般的に使われている機能です。もちろん、コンテナの安全性を高めるための重要な機能でもあります。
setuid
一般的にはUNIX系のOSでは、プロセスはroot権限(実効ユーザIDが0)で実行される特権プロセスと、一般ユーザ権限で実行される(実効ユーザIDが0以外の)非特権プロセスに分けられます。
一般ユーザは、通常はそのユーザの権限でプログラムを実行すれば良いのですが、一般ユーザが実行するプログラムであっても処理の内容によっては特権が必要な場合があります。例えば、ネットワークの疎通確認に良く使うping
コマンドは特権が必要で、Linuxディストリビューションによっては次のようにsetuid
されており、root権限で実行するようになっています(例はUbuntu 18.04)。
ケーパビリティ
先に紹介したping
コマンドは、パケット送信にRawソケットを使います。このRawソケットを使う場合には特権が必要ですのでsetuid
されており、root権限で実行されていたのでした。
逆に言うとRawソケットを使うのに必要な権限のみが必要なのに、広範囲に渡るrootが持つ強い権限すべてをping
コマンドに渡していることになります。これはセキュリティ上の観点から望ましいことではありません。
Linuxにはこのような場合に使える、rootが持っている絶対的な権限を細かく分け、必要な権限だけを与える仕組みが存在します。これがケーパビリティ(capability)です。ケーパビリティはスレッドごとに設定され、スレッドは自身が持つケーパビリティを変更できます。
例えば、CAP_NET_RAW
というケーパビリティがあればRawソケットを使用できます。Linuxカーネルでは、このような細分化されたケーパビリティが多数定義されており、/usr/include/linux/capability.h
で定義されています。
ケーパビリティの実装は結構古くて、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
のPermitted(CapPrm
)、Effective(CapEff
)、バウンディングセット(CapBnd
)は、次のようにすべて1が設定された状態になっています。
特定のケーパビリティだけが設定されている、ホストの時刻合わせを行うためのデーモンであるntpd
は次のようになっています。
上記の例のように status
ファイルでケーパビリティを確認できます。しかし、このファイルを見ただけでは何かのビットが立っていることはわかっても、それぞれのビットが意味するケーパビリティを覚えていないと、プロセスが何のケーパビリティを持っているかはわかりません。
もう少しわかりやすくプロセスのケーパビリティセットを確認したい場合はlibcap
に含まれるgetpcaps
コマンドが使えます(Ubuntuの場合はlibcap2-bin
パッケージに含まれています)。
ポート番号1024番未満を使うには特権が必要です。しかし、ntpd
は一般ユーザであるntp
ユーザ権限で動作しており、本来であればntpd
で使うポートである123番ポートは使えないはずです。しかし、特権ポートが使えるケーパビリティであるcap_net_bind_service
を持っているので123番ポートを使えます。
また、システムクロックを設定できるケーパビリティであるcap_sys_time
を持っていますので、本来は一般ユーザではできないシステムクロックの設定ができるわけです。
つまりntpd
は必要最低限のケーパビリティのみを持った状態で実行されているということです。不要な特権を持たずにデーモンが実行されていますので、より安全性が高まるというわけです。
プロセスのケーパビリティを操作する
実際にケーパビリティによって必要な特権のみを持って動く例を見ましたので、この後は実行中のプロセスが持っているケーパビリティを操作した場合の例を見てみましょう。
特権を持っていないプロセスがいきなり特権を取得できませんので、実行中のプロセスが自身のケーパビリティを操作する場合は、持っているケーパビリティを減らしていくことになります。
ケーパビリティを持っていなければ、たとえrootであってもコマンドは実行できません。試してみましょう。libcap
に含まれるcapsh
コマンドで簡単に試せます。ここで指定するケーパビリティは、マニュアルやcapability.h
で定義されているケーパビリティを指定します(小文字でも構いません)。
cap_net_raw
ケーパビリティを削除したときにケーパビリティがどのようになっているのかを確認してみましょう。まずはrootで普通に実行しているbash
が持つケーパビリティです。
サポートされているケーパビリティすべてが有効になっているのがわかります。ここでcap_net_raw
を削除すると次のようになります。
/usr/include/linux/capability.h
を見ると、CAP_NET_RAW
は13ですので、13ビットシフトした下位から14ビット目が0になっているのがわかります。
ファイルケーパビリティ
プロセスは、先に述べたようにケーパビリティセットを持っています。実行中にケーパビリティを変更できますが、今持っていないケーパビリティをいきなりプロセス実行中に増やすことはできません。基本的にケーパビリティは実行中に操作すると減っていくだけです。
セキュリティ的には不要な特権が減っていくわけですから意味があるわけですが、最初から特定の特権を与えておきたいケースには対応できません。先のping
コマンドに設定されていたsetuid
のようなケースです。
そこでsetuid
のように、あらかじめファイルにケーパビリティを設定しておくことができます。これがファイルケーパビリティです。
ファイルケーパビリティも、プロセスのケーパビリティと同様にPermitted、Inheritable、Effectiveという3つのケーパビリティセットがあります。ただし、プロセスのケーパビリティセットと違い、ファイルケーパビリティのEffectiveケーパビリティは0 or 1の単一の値です。
次の例は CentOS 8にインストールされているping
コマンドです。
このようにsetuid
されていませんがping
コマンドは実行できています。これはファイルケーパビリティが設定されているからです。
ファイルケーパビリティを確認するにはlibcap
に含まれるgetcap
コマンドを使います。
これは、/bin/ping
にはファイルケーパビリティのPermittedケーパビリティとして、cap_net_admin
とcap_net_raw
ケーパビリティが設定されているという意味です(※1)。
ファイルケーパビリティは安全のためにコピーすると設定が外れます(※2)。
このようにファイルケーパビリティが設定されていないので、コピーしたping
コマンドは一般ユーザーでは実行できません。
ここでコピーしたping
コマンドを一般ユーザ権限で実行できるようにファイルケーパビリティでcap_net_raw
を付与してみましょう。同じくCentOS 8で実行しています。
ファイルケーパビリティを設定するには特権が必要ですので、上の例でもsetcap
コマンドだけはroot権限で実行しています。
まとめ
今回は、プロセスのケーパビリティについて簡単に説明しました。実際にケーパビリティを確認する方法を紹介し、ケーパビリティを削除したときにコマンドの実行が失敗することを確認しました。
また、最初から特定のケーパビリティを与えてコマンドを実行したいときのためのファイルケーパビリティについて説明しました。ファイルケーパビリティを設定することで、一般ユーザでも特権が必要なコマンドが実行できました。
実は今回の記事は、udzuraさんの記事ができるまでのつなぎとして、カーネルの新機能に関する軽い話題で1回記事を書こうかなと考えて書き始めた記事でした。
その前提知識として、ファイルケーパビリティについて軽く触れる必要あったので書き始めたのですが、必要と思うことを加えていくにつれ量が増えていき、軽く触れるだけでは済まない量になってしまいました。そこで、ケーパビリティについてきちっとまとめてみようと思ったわけです。
次回まででケーパビリティ全体について説明を終わらせ、3回目で最初に書こうと思ったケーパビリティ関連の新機能について書く予定です。