Linuxケーパビリティによる特権分割
root権限を全か無かではなく必要な一片だけに絞れます。5つのケーパビリティ集合とファイルケーパビリティの原理から、setuidより安全で壊れにくい特権設計を解き明かします。
- 1.ケーパビリティはroot(UID 0)が持つ特権を約40種へ分解し、プロセスは5つのビット集合(permitted/effective/inheritable/ambient/bounding)でどれを保持・行使できるかを管理する。
- 2.ファイルケーパビリティは実行ファイルの拡張属性security.capabilityに格納され、execve時の遷移式に従って新プロセスの各集合を計算する。setuid rootの全権付与を一片の権限へ置き換える。
- 3.ambient集合はファイルケーパビリティを持たないインタプリタ経由でも権限を継承させ、bounding集合は祖先が子孫へ与えられる権限の上限を不可逆に絞る天井として働く。
なぜrootを「分割」する必要があるのか
伝統的なUNIXの権限判定は二分法でした。実効UIDが0(root)なら、カーネルの大半の権限チェックを丸ごと素通りする。ping が生のICMPソケットを開きたいだけでも、setuid root にして すべての特権 を与えるしかありませんでした。問題は、そのプログラムに脆弱性があれば、攻撃者も同じ全権を握る点です。必要なのは「生ソケットを開く」一片だけなのに、ファイルを上書きする権限も、カーネルモジュールを読み込む権限も、ついてきてしまう。
Linuxケーパビリティ(capabilities)は、この UID 0が持つ単一特権を、約40個の独立したケーパビリティへ分解 する仕組みです。CAP_NET_RAW(生ソケット)、CAP_NET_BIND_SERVICE(1024未満のポートへのbind)、CAP_SYS_ADMIN(多数の管理操作)、CAP_DAC_OVERRIDE(ファイルパーミッション無視)のように、特権が機能単位の名前を持ちます。プロセスは必要な一片だけを保持し、カーネルの権限チェックは「UID == 0 か」ではなく「該当ケーパビリティを実効集合に持つか」を見るようになります。これはカーネルモードとユーザーモードの権限境界を、より細かい粒度で再定義する試みです。
5つのケーパビリティ集合
各スレッドは、ケーパビリティの状態を5つのビット集合で保持します。task_struct の資格情報(struct cred)内に置かれ、それぞれ役割が異なります。
| 集合 | 略号 | 役割 |
|---|---|---|
| Permitted | P | そのスレッドが行使可能なケーパビリティの上限。ここに無いものはeffectiveへ上げられない |
| Effective | E | 実際に権限チェックで参照される現役の集合。カーネルが「持つか」を見るのはここ |
| Inheritable | I | execve越しに引き継げる候補。ファイル側のIとANDされて次のPへ寄与 |
| Ambient | A | ファイルケーパビリティ無しのexecveでも保持される非特権向けの継承経路 |
| Bounding | BND | 祖先が課す天井。PやInheritableが越えられない上限で、不可逆に縮むのみ |
基本の運用は、PにありEに無いケーパビリティは「持っているが今は休眠中」という状態です。プログラムは capset(2)(や libcap の cap_set_proc)で、必要な瞬間だけPからEへ上げ、用が済めばEから落とす。権限を行使する区間を最小化する 特権分離(privilege separation) の基本動作です。Permittedにあるものは、自分の判断でいつでもEffectiveへ出し入れできます。逆にPに無いものは、execveを経ない限り後から増やせません。
スレッドのEffectiveは集合(各ケーパビリティ1ビット)ですが、ファイルケーパビリティのEffectiveは 単一のフラグ です。ファイル側のEが立っていると、execve後に「許可された全ケーパビリティをそのままEffectiveへ昇格」します。setuid root バイナリと同じく、起動直後から権限が有効な「レガシー互換」の挙動を再現するためです。Eフラグが立っていなければ、新プロセスは起動後に自分でPからEへ上げる必要があります。
ファイルケーパビリティとexecveの遷移式
ファイルケーパビリティは、実行ファイルの 拡張属性 security.capability に格納されます。setcap 'cap_net_raw+ep' /bin/ping を実行すると、この属性にPermitted集合・Inheritable集合・Effectiveフラグが書き込まれます。これはELFローダが解釈するファイル内容とは別の、ファイルシステムのメタデータです。属性に含まれる3要素を、慣例で F(permitted)、F(inheritable)、F(effective) と表記します。
execve(2) の瞬間、カーネルは旧スレッドの集合とファイル属性から、新スレッドの集合を計算します。中核となる遷移式は次の擬似コードのとおりです(P' が新プロセス側、P が呼び出し元側)。
P'(ambient) = (file caps or setuid/setgid なら) 空集合 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable))
| (F(permitted) & P(bounding))
| P'(ambient)
P'(effective) = F(effective) が真なら P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable) # 変化しない
P'(bounding) = P(bounding) # 変化しない
ここで読み取るべき原理は3つあります。第一に、新しいPermittedは2系統から得られます——呼び出し元のInheritableとファイルのInheritableの 論理積(I & F(inheritable))、そしてファイルのPermitted。前者が「呼び出し元が渡そうとし、かつファイルが受け取りを許可した」交差点で、特権の受け渡しに 両者の合意 を要求する設計です。第二に、どちらの経路も最後に bounding(天井)とANDされ、上限を超えられません。第三に、Effectiveフラグがファイル側で立っていれば、起動直後に全Permittedが有効化されます。
setcap の指定子は集合の頭文字です。cap_net_raw+ep は「permittedとeffectiveに CAP_NET_RAW を立てる」、+eip なら inheritable も加えます。+p だけだと、起動後にプログラム自身がPからEへ上げる前提になります。libcap対応していない単純なツールには +ep を、自前で特権区間を制御する堅牢なデーモンには +p を、という使い分けになります。
setuid rootの代替としての設計
ケーパビリティの最大の動機は、setuid root の置き換えです。両者の振る舞いの差を整理します。
| 観点 | setuid root | ファイルケーパビリティ |
|---|---|---|
| 付与される権限 | rootの全特権(約40種すべて) | 指定した一片だけ |
| 実効UID | 0に変わる(ファイル所有者やログにも影響) | 呼び出し元UIDのまま変わらない |
| 脆弱性悪用時の被害 | 全権限奪取につながりやすい | その一片の範囲に限定される |
| 権限の上限制御 | boundingの概念なし | bounding集合で祖先が天井を課せる |
setuid root が実効UIDを0へ変える副作用に対し、ファイルケーパビリティは UIDを一切変えずに特定の権限ビットだけ を渡します。ping を cap_net_raw+ep で配れば、実効UIDは一般ユーザーのまま、ファイルを書き換える CAP_DAC_OVERRIDE も持たない。万一 ping に脆弱性があっても、攻撃者が得るのは生ソケットを開く能力に限られます。攻撃面(attack surface)を最小化する 最小権限の原則 の、具体的な実装手段です。
分解したはずの特権でも、CAP_SYS_ADMIN だけは別格です。mount、setns、pivot_root、多くの ioctl、swap操作など、極めて多数の操作がこの1つに紐づいており、これを与えることは事実上 root に近い権限を渡すことに等しい。「ケーパビリティで絞った」つもりでも CAP_SYS_ADMIN を含めれば台無しです。新しいケーパビリティを定義せず安易に CAP_SYS_ADMIN へ追加してきた歴史的経緯への反省から、近年は CAP_BPF や CAP_PERFMON のように機能を切り出す分割が進んでいます。
ambient集合とboundingの実務
初期のケーパビリティ設計には穴がありました。Inheritableは「ファイル側のInheritableとのAND」を要求するため、ファイルケーパビリティを持たないスクリプトやインタプリタ には権限が一切渡りませんでした。シェルスクリプト経由で特権を子へ引き継ぎたい非特権ユースケースが書けなかったのです。これを埋めるのが ambient集合 です。
ambientにあるケーパビリティは、ファイルケーパビリティ無しのexecveでも P'(permitted) と P'(effective) の両方へ生き残ります(前掲の遷移式の P'(ambient) 経由)。ただし安全のため厳しい制約があります——ambientへ立てられるのは「PermittedかつInheritableの両方にあるケーパビリティ」だけで、setuid/setgidバイナリやファイルケーパビリティ付きバイナリをexecveすると ambientは即座に空へクリア されます。特権昇格バイナリと継承経路が混ざって意図せぬ権限漏れが起きるのを防ぐためです。
一方 bounding集合 は天井として働きます。prctl(PR_CAPBSET_DROP, ...) でケーパビリティを落とせますが、これは 不可逆 で、一度落とすと自分も子孫も二度とそのケーパビリティをPermittedへ得られません。コンテナランタイムが起動時に不要なケーパビリティをboundingから一括ドロップするのは、たとえコンテナ内のプロセスがファイルケーパビリティ付きバイナリをexecveしても、天井を超える権限は決して得られない保証を作るためです。名前空間の内部実装で見たユーザー名前空間と組み合わせると、内側のrootが持つケーパビリティはその名前空間内の資源にしか効かず、boundingがさらに上限を縛る、という二重の封じ込めになります。
集合の役割を一文で言えるかが分水嶺です。Permitted=行使可能な上限、Effective=今チェックされる現役、Inheritable=execve越しの候補(ファイル側とAND)、Ambient=ファイルケーパビリティ無しでも継承する非特権経路、Bounding=不可逆に縮む天井。遷移式の核は「新Permitted = (I & F(inh)) | (F(perm) & bounding) | ambient」。setuid rootとの違いは「UIDを変えず一片の権限だけ渡す」「boundingで上限制御できる」。CAP_SYS_ADMIN が実質新rootである点も頻出です。
まとめ
ケーパビリティはUID 0の全特権を約40の機能単位へ分解し、各スレッドが permitted/effective/inheritable/ambient/bounding の5集合で保持・行使・継承・上限を管理します。Permittedが行使可能な上限、Effectiveが現役、両者の出し入れで特権区間を最小化します。ファイルケーパビリティは security.capability 拡張属性に格納され、execveの遷移式「新Permitted = (I & F(inh)) | (F(perm) & bounding) | ambient」に従って新プロセスの集合を決めます。これにより setuid root の全権付与を一片の権限へ置き換え、UIDを変えずに最小権限を実現できます。ambientはインタプリタ経由の継承を補い、boundingは不可逆な天井で子孫の権限上限を縛る——システムコールのABIで見た特権境界の上に、機能粒度の権限モデルを重ねたのがケーパビリティの本質です。
OS Article
Linuxケーパビリティによる特権分割を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ケーパビリティ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ファイルケーパビリティは実行ファイルの拡張属性security.capabilityに格納され、execve時の遷移式に従って新プロセスの各集合を計算する。setuid rootの全権付与を一片の権限へ置き換える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ケーパビリティ / Linux」に近いか確認する。
- 強みである「ケーパビリティはroot(UID 0)が持つ特権を約40種へ分解し、プロセスは5つのビット集合(permitted/effective/inheritable/ambient/bounding)でどれを保持・行使できるかを管理する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。