明日使えない Linux の capabilities の話

(この記事は KMC アドベントカレンダー 2016 の3日目の記事です)

はじめに

みなさん以下のようなことで困ったことはないでしょうか?

ポート80を listen したいけど特権ポートなので、一般ユーザの権限で動くデーモンでは bind できない。

1024未満のポートは特権ポートと呼ばれ、一般ユーザの権限では bind することはできません。 この問題の解決策を考えてみます。

(なお、長々と説明を書いていますが、結論だけ知りたい人は一番下だけ読んで下さい)

root で起動

まず、root であれば特権ポートを自由に bind できるので、root で対象デーモンを起動すれば、特権ポートを bind できます。 しかし、デーモンを root として動作させるのは一般にリスクが大きいです。 もしそのデーモンに脆弱性があった場合、root 権限を悪用される可能性があるわけです。

したがって、このやり方は却下です。

プロセス分割

プロセスを分割することでこの問題に対処する場合もあります。 nginx は、まず root として起動し、特権ポートを bind します。 そして、子プロセスを fork し、それらを一般ユーザとして実行します。 この際に、listening socket を open したままの状態にしておくことで、子プロセスでもその socket を利用することができます。

しかし、プロセス分割は結構大掛かりです。 Java や Go みたいにランタイムの都合上 fork できない場合もあります。 できればプログラム本体には手を入れないで対処できる方法が欲しいわけです。

capability

今までの話をまとめると

  • root は使いたくないけど
  • 特権ポートを bind したい

ということになります。 今回は Linux の capabilities という機能を用いてこれを実現していきます。

capabilities とは、root が持っている特権をいくつかのグループに分割し、それぞれを独立に enable, disable できるようにしたものです。

capabilities には例えば以下のようなものがあります。

  • CAP_DAC_OVERRIDE
  • CAP_DAC_READ_SEARCH
  • CAP_KILL
    • シグナルを送るときの権限チェックをバイパスする。
  • CAP_NET_BIND_SERVICE
    • 特権ポートにソケットをバインドできる。
  • CAP_SYS_TIME
    • システムの時刻を設定できる。

他にもいっぱい capability はあるので、興味のある人は man capabilities してみましょう。 今回の用途では CAP_NET_BIND_SERVICE を利用すればよさそうです。

File capabilities

プロセスに capabilities を持たせるひとつの方法として、実行ファイルに対して capabilities を設定する方法があります。 この方法を使うと、そのファイルを 誰が実行しても 指定した capabilities を持ってプロセスが起動するようになります。

ここでは例として、パーミッションを無視して任意のファイルを見れる超脆弱 cat を作ってみます。

# まず /bin/cat を自分用にコピー
$ cp /bin/cat ./mycat

# CAP_DAC_READ_SEARCH を付与する
$ sudo setcap cap_dac_read_search=eip ./mycat

# 確認
$ getcap ./mycat
mycat = cap_dac_read_search+eip

# まず普通の cat で /etc/shadow を見ようとしてみる (当然見れない)
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

# 次に mycat で /etc/shadow を表示!!
$ ./mycat /etc/shadow
root:!:17042:0:99999:7:::
daemon:*:17001:0:99999:7:::
bin:*:17001:0:99999:7:::
...

このように catCAP_DAC_READ_SEARCH を付与することでパーミッションを無視して任意のファイルを表示できるようになりました。

sudo setcap cap_dac_read_search=eip ./mycatmycat に file capabilities を付加している部分です。 eip は立てるフラグの種類をしていしています、と言ってもこの時点では意味不明だと思いますが説明すると長くなるので省略します。

さて、冒頭の問題に戻りましょう。

特権ポートを bind する権限を与えたいので、CAP_NET_BIND_SERVICEsetcap を使って対象サービスの実行ファイルに付与すれば良さそうです。

しかし、この方法には問題があります。

file capabilities を使った方法では 誰が実行しても その特権を持ってしまいます。 一般ユーザは通常何の特権の持っていないので、一般ユーザにとっては setcap されたファイルを実行すると特権が増えることになります。 一般に、特権が増えるような系を作ってしまうと、脆弱性のリスクが増えるので、できるだけ避けたいです。

ということで、file capabilities を使わずに、デーモンに CAP_NET_BIND_SERVICE を付与する方法を考えてみます。

Ambient capabilities

ここまでをまとめると、

  • root として動いている親プロセス(今回の場合は init)が存在し、
  • この親プロセスから一部の capabilities (今回の場合は CAP_NET_BIND_SERVICE)だけを継承して、非rootユーザの子プロセスを spawn したい。
  • File capabilities は利用しない。

という要件になります。

実は、最近まで capabilities の継承ルールがクソなせいで、file capabilities を使わないと非rootユーザに capabilities を付与することはできませんでした(多分)。 しかし、Linux 4.3 で Ambient capabilities という機能が追加され、これが簡単にできるようになりました。

Ambient capabilities の説明に行く前に、簡単にプロセスの capabilities を説明します。

各プロセス(厳密にはスレッド)は、Permitted, Effective, Inheritable の3つのフラグセットを持っています。

  • Permitted, Effective, Inheritable はそれぞれ capability の集合を表すフラグセットです。
  • Effectiveカーネルによってパーミッションチェックをされるときに使われる capability の集合を表します。
  • PermittedEffectiveInheritable の superset を表します。Permitted に含まれていない capability を EffectiveInheritable に加えることはできません。

そして、Inheritableexecve(2) で他の実行ファイルを起動するときに継承される capability の集合を表し……ていたら何も問題なかったのですが、Inheritable に含まれる capabilities は条件が揃わないと子プロセスの PermittedEffective に継承されません。 特に、今回のように「非rootユーザ」で「file capabilitiesが設定されていないファイルを実行」する場合、PermittedEffective は常に空になります。

そこで、ambient capabilities を利用します。

ambient capabilities とは、簡単に言うと「子プロセスに継承される capabilities」を表すフラグセットです。 より厳密に言うと、これから実行するファイルに setuid ビットや setgid ビットがセットされておらず、かつ file capabilities も設定されていない場合、ambient capabilities が子プロセスに継承されます。

つまり、子プロセスに継承したい capability があるときは、ambient capabilities にそれをセットしてから exec すればいいわけです。

どうして最近まで存在しなかったのか疑問に思うぐらい、シンプルで自然な機能ですね。

systemd

以上より、冒頭の問題を解決するためには、「init が対象のデーモンを起動するときに ambient capabilities に CAP_NET_BIND_SERVICE を加えておけばよい」ということになります。

systemd はまさにその機能を持っているので、以下の指定をユニットファイルに書くことで簡単に CAP_NET_BIND_SERVICE を持った状態でデーモンを起動することができます。

AmbientCapabilities=CAP_NET_BIND_SERVICE

おわりに

一方ロシアは8080を使った。