前回はプロセスに設定されているケーパビリティと、ファイルにあらかじめ設定しておくファイルケーパビリティについて説明しました。
今回はまず、execve(2)
システムコールを使ってプログラムを実行する際にケーパビリティがどのように変化するのかを説明したあと、Ambientケーパビリティとケーパビリティバウンディングセットについて説明します。
プログラム実行時のケーパビリティ
Linux上で実行されるプログラムは、fork(2)
やclone(2)
システムコールを使って親プロセスを複製して生成し、複製したあとにexecve(2)
システムコールで目的のプログラムを実行します。
このexecve(2)
でプログラムを実行する際に、カーネルは実行後のプロセスが持つケーパビリティを計算します。このときの計算は次のアルゴリズムが使われます。
ここで、
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ケーパビリティで許可されます。
Ambient(P'(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がないアルゴリズムでした。
このアルゴリズムでもInheritableケーパビリティがありますので、必要最小限の任意のケーパビリティを持った子プロセスを生成できそうな気がします。しかし、実はいくら特権を持っていたとしても、このように誰にでもケーパビリティを与えたくないという要件は満たせません。
なぜなら、先に説明したようにexecve(2)
で生成したプロセスにケーパビリティを与えるには、ファイルケーパビリティを設定しないとP'(permitted)
で目的のケーパビリティセットを有効にできません。その結果、生成するプロセスでのケーパビリティセットP'(effective)
でもケーパビリティを有効にできません。
ファイルケーパビリティを設定してしまえば、先に述べたような、与える特権を必要最小限にしたいという要求を満たせません。
そこで登場したのがAmbientケーパビリティです。Ambientケーパビリティは特権を持たないプロセスのexecve(2)
の前後で継承されるケーパビリティです。
Ambientケーパビリティは、設定する時点でPermittedケーパビリティとInheritableケーパビリティの両方で目的のケーパビリティが有効にされていなければ設定できません。
また、PermittedケーパビリティとInheritableケーパビリティから目的のケーパビリティが削除されると、Ambientケーパビリティからもそのケーパビリティは削除されます。
そして、アルゴリズムの(1)のように、setuid
、setgid
、ファイルケーパビリティといった、ファイル自体に特権を与えるような設定がされていない場合にのみ、Ambientケーパビリティが子プロセスに継承されます。
そして(2)、(3)の式のようにファイルケーパビリティに関わらず、Ambientケーパビリティは継承されます。
先に書いたような目的の場合に、誰もが期待するように動作するすっきりとした機能です。
Ambientケーパビリティは説明だけでも比較的わかりやすい機能かもしれませんが、一応実行例を示しておきましょう。
前回の実行例のように、ファイルケーパビリティもsetuid
も設定されていないping
コマンドをAmbientケーパビリティを使って実行してみましょう。
UbuntuやCentOS 8でインストールされるlibcap 2.25にはAmbientケーパビリティを操作する機能がありませんので、次の例はPlamo Linux 7.1上でlibcap 2.27を使っています。
引数も実行結果も長いのでかえってわかりづらいかもしれませんね(^^)。何をやっているのかを簡単に説明しましょう。capsh
コマンドは指定した順でオプションが処理されますので、指定する順番が変わるとエラーになる可能性があります。
オプションを与えた順に処理されますので、この例のように実行すると、
- Ambientケーパビリティを設定するために
cap_setpcap
を親プロセス(capsh
コマンド)に設定(このケーパビリティがないとIhneritableにケーパビリティを設定できません)
- uid=1000でコマンドを実行するために
cap_setuid,cap_setgid
を親プロセスに設定
- Permitted、InheritableケーパビリティがないとAmbientに設定できないので
cap_net_raw
を親プロセスに設定
--keep=1
はあとで説明するsecurebitsフラグを設定(このフラグを設定する際にもcap_setpcap
が必要)
- 一般ユーザ権限で実行するために
--uid=1000
を指定
--addamb=cap_net_raw
でping
コマンドの実行に必要なcap_net_raw
をAmbientケーパビリティに設定
- 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ケーパビリティが指定されていても、バウンディングセットで許可されていないケーパビリティは許可されません。
上の例はコピーしたping
に設定するファイルケーパビリティで、Permittedケーパビリティにcap_net_raw
を設定して実行している例です。Permittedケーパビリティが有効になっているので一般ユーザーでも実行できていますが、次のようにバウンディングセットでcap_net_raw
を落としたシェルから実行すると実行できません。
このようにexecve(2)
でプログラムを実行する際に取得できるケーパビリティを制限できます。
しかし先に説明した通り、同じ式(2)にある"P(inheritable) & F(inheritable)
"とのORですので、この式で許可されていれば、バウンディングセットで許可されていなくてもケーパビリティを獲得できてしまいます。
また同様にAmbient(P'(Ambient)
)ケーパビリティセットともORですので、Ambientケーパビリティで許可されているケーパビリティも獲得できます。
ping
に対してファイルケーパビリティでInheritableケーパビリティを設定して、プロセスのInheritableケーパビリティを設定して試してみましょう。
以上のようにバウンディングセットでcap_net_raw
を無効にしたにも関わらず、ファイルとプロセスでInheritableケーパビリティで有効になっているためにping
コマンドは実行できました。
これではプロセスが取得できるケーパビリティを制限できないではないか、と思われるかもしれません。しかし、バウンディングセットにはもうひとつ役割があります。
もうひとつの役割は、capset(2)
システムコールでスレッドが自身でケーパビリティを設定する際に制限をかける役割です。capset(2)
でInheritableケーパビリティを追加する場合、バウンディングセットで設定されているケーパビリティのみ、Inheritableケーパビリティに追加できます。
先の実行例のcapsh
に指定するオプションの順を少し変えて、--inh
でInheritableケーパビリティを設定する前に、--drop
でバウンディングセットからcap_net_raw
を削除してみましょう。
このようにバウンディングセットで許可していないと、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 さんの記事