ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Linux Capability - ケーパビリティについての整理

Man page of CAPABILITIES

を読んで自分なりに整理する。メモ気分で書いているので言葉足らず/用語が曖昧 のところがあればご指摘ください。

3つのケーパビリティセット

  • Inheritable execve(2) (以下、単にexecveと呼ぶこともある) の前後で引き継がれるケーパビリティ
  • Permitted 実効ケーパビリティセットに追加することができるセット。ここから落ちたケーパビリティはどう頑張っても実効状態にできない
  • Effectve 実効ケーパビリティセット。実際にそのスレッドで利用できるケーパビリティの集合

Permittedのセットの中から、実際に有効になっているEffectveのセットをピックアップするイメージ。

また、Inheritableはexecveしなければ関係することはない。

スレッドケーパビリティセット

それぞれのスレッドは上述の三つのケーパビリティセット+バウンディングセットを持っている。 /proc/$$/status を覗いてみると以下のようになっている。

# root
[vagrant@localhost ~]$ sudo cat /proc/$(pgrep sshd | head -1)/status | grep -E ^Cap
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff
CapBnd: 0000001fffffffff
# 一般ユーザ
[vagrant@localhost ~]$ sudo cat /proc/$(pgrep qmgr | head -1)/status | grep -E ^Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff

ここで、一般ユーザで動作している chronyd のことを考えてみる。chronydは時刻を修正する必要があるが、時刻の修正は普通rootユーザでないとできない。

# Uid が 0 でないこと、CapPrm/CapPrmが普通の一般ユーザとは変わっていることに注目する
[vagrant@localhost ~]$ sudo cat /proc/$(pgrep chronyd | head -1)/status | grep -E -e ^Cap -e Uid
Uid:    995     995     995     995
CapInh: 0000000000000000
CapPrm: 0000000002000400
CapEff: 0000000002000400
CapBnd: 0000001fffffffff

0000000002000400 とは何番目のビットが立っているかというと、0始まりで10番目と25番目。

$ ruby -e "puts (0..31).map{|i|'%3d'%i}.reverse.join ;puts ('%032b' % '0000000002000400'.to_i(16)).chars.map{|c| '  ' + c }.join"
 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
  0  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0  0

<linux/capability.h> に定義されている通り、 CAP_NET_BIND_SERVICECAP_SYS_TIME が許可(そして実効)されている。

#define CAP_NET_BIND_SERVICE 10
...
#define CAP_SYS_TIME         25

ということでchronydは一般ユーザであるが、1024番未満のポートをリスンすることができ、また時刻を操作することができる。

ファイルケーパビリティ

一方でファイル自身にもケーパビリティが存在する。

  • Inheritable これはスレッドの時と同じように、 execve の前後で引き継がれるケーパビリティと考えてよい、ただし、ファイルの情報がスレッドに行き渡るイメージ。
  • Permitted スレッドの継承可能ケーパビリティに関わらず 許可されるケーパビリティ
  • Effectve 新しいプロセス(スレッド)で、許可ケーパビリティセットがそのまま実行になるかならないかを司っている。集合ではなく単一ビットとのこと。

この情報は、実行ファイルに事前にセットされていない場合、 cap_get_proc などでケーパビリティを取得しようとしてもできないので注意。

[root@localhost ~]# strace getcap /bin/bash 2>&1 | grep getxattr
getxattr("/bin/bash", "security.capability", 0x7ffdc3062a10, 20) = -1 ENODATA (No data available)

そして、事前にセットされていることは考えにくいのであった。デフォルトの際の挙動は後述。

ケーパビリティバウンディングセット

execveをした際に獲得できるケーパビリティを 制限するため に使われる(by man)。Linux 2.6.25 以降(実質、僕らがコンテナを作ろうと考えるようなLinuxは全て)では、バウンディングセットはスレッドごとに存在し、上で見たようにデフォルトはすべてのビットが立っている。

具体的には論理積でマスクをするような挙動をする。

バウンディング=境界。基本的には、execveの此岸と彼岸の境目でマスクをする役割で覚えると良い。

ここまでのケーパビリティが、一般ユーザにrootの権限の一部を割り当てるために設計されているように見えるのに対し、バウンディングセットはrootの権限の一部を落とすために使うこともできる(という印象を持っている。実際の議論はLKMLなどを追ったほうがいいのだろう)。

→ と思っていたが、 id:matsumoto_r さんのご指摘の通り、例えば非rootのプロセスが特権を与えられている場合を考慮していなかった。「境界」なのであくまでもexecveの時点で実行ファイルに与えられたPermittedからマスクをするという役割である。

execveをするとどうなるかという話

(個人の感想: この辺りが、 Linux namespace なんかと扱いが違う印象があり、引っかかった。)

Linux namespaceはforkをしてもexecveをしても基本的には受け継がれる(PID namespaceだけちょっと動きが違うのでこれもややこしいが、また別途)。一方でケーパビリティにおいても、forkの場合はそのまま引き継がれるが、execveは実行ファイルが変わってしまう、つまりファイルケーパビリティとの関わりが出てくるので特殊である。

まずは、execve後のプロセスの新しい「許可ケーパビリティセット」を見てみる。

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

これは重要な公式である。ひとまずファイルケーパビリティを無視すると、プロセスの継承ケーパビリティセットとケーパビリティの論理和(OR)となる。ここは重要なポイントで、 バウンディングセットは継承セットの中身を制限することはできない

一方で、プロセスの継承ケーパビリティセットは最初で見たとおりデフォルトの場合全てゼロビットなので、実質バウンディングセットの中身がそのまま新しい許可セットになる(ことが多い。デフォルトでない時どうなるかは最後に検証しよう)。

で、ファイルのデフォルトケーパビリティセットは、ファイルのsetuidあるいは実行ユーザがrootかそうでないかで決まる。

  • ファイルがsetuid=0されているまたはrootで実行した場合、ファイルの継承可能セットと許可セットを全て 1 にする
  • ファイルがsetuid=0されている場合、ファイルの実効ケーパビリティビットを 1 にする [*]

ということでrootの場合、論理積であるので、ファイルケーパビリティセットの状態もバウンディングセットに影響を与えない。

以上のことにより、rootにおいては、デフォルトではバウンディングセットの中身がそのままexecveした後の新しい許可ケーパビリティセットになることがわかる。

そして許可が決まれば、実効ケーパビリティセットはその中から選ばれるだけである。

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

(* これ、先述の ファイルの実効ケーパビリティビットを 1 にする がroot実行時もそうなっているっぽいので、rootでexecveした場合は新しいバイナリでも許可=実効になっている、というかならないとおかしい。manの英語版を見ると If a set-user-ID-root program is being executed, or the real user ID of the process is 0 (root) って言っていて、コンマが入っているので、このorは「すなわち」ということなんじゃないかという気がしているが、実装を見ようかな...)

追記: 最新の仕様では ambient capabilityが絡む。 capabilities(7) - Linux manual page 再度整理しないといけない...

なお、ここまでの話は、プロセスが単一スレッドで動いているという前提である。

バウンディングセットの制約

上述したとおり、多くの場合rootでexecveを実行した場合においてはバウンディングセットは有効に働く。そうでない場合はあるだろうか?

まず、rootでバウンディングセットからケーパビリティを落としてexecveをしてみる。実験には mruby の対話環境(mirb)と、 mruby-capabilitymruby-exec の二つのmgemを用いる。

[vagrant@localhost vagrant]$ sudo env LANG=C ./mruby/bin/mirb 
mirb - Embeddable Interactive Ruby Shell

> Capability.drop_bound Capability::CAP_SYS_TIME
 => 0
> exec "/bin/bash"
[root@localhost vagrant]# date -s 09:00
date: cannot set date: Operation not permitted

この場合は確かに CAP_SYS_TIME 権限が落とされて、rootであるが時刻の設定ができない。

一方で継承セットに CAP_SYS_TIME が加わっている場合はどうなるか?

vagrant@localhost vagrant]$ sudo env LANG=C ./mruby/bin/mirb 
mirb - Embeddable Interactive Ruby Shell

> c = Capability.get_proc
pid: 0 => #<Capability = cap_chown,cap_dac_override,cap_dac_read_search,...,cap_sys_time,...+ep>
> c.set Capability::CAP_INHERITABLE, [Capability::CAP_SYS_TIME]
 => #<Capability = cap_sys_time+eip cap_chown,cap_dac_override,...+ep>
> Capability.drop_bound Capability::CAP_SYS_TIME
 => 0
> exec "/bin/bash"
[root@localhost vagrant]# date -s 09:00
Thu Jun 23 09:00:00 UTC 2016

時刻操作が可能になってしまう

つまり、バウンディングセットからケーパビリティを落とすことでrootの権限の一部を剥奪したい場合、継承ケーパビリティセットのビットが一つも立っていないことを確認する必要がある。そうでないと、意図しない許可を与えてしまう場合が考えられる。

その他参考

ケーパビリティ で権限を少しだけ与える - いますぐ実践! Linuxシステム管理 / Vol.183 を、実質CentOS 7で書き直してみただけの記事になっている気がします(特に最初の下り)...。元の記事も是非お読みください。

最後に

最後の実験の通り、mrubyはLinuxのコア機能の実験をする上で便利っぽい。