コンテナセキュリティの一つとして、不要なケーパビリティ(要はroot権限を細分化したもの)を落として動かすことができる。
デフォルトではCHOWNやDAC_OVERRIDEなど、ほぼ要らない権限が含まれているので、アプリ側での防御を抜かれたときのために設定で落としておきたい。
・・・という説明は無限に世にあるものの、具体的に何を設定すれば良いかの例が見つからなかったので、いくつか試したものをまとめる。
ケーパビリティ(Capability)とは
rootユーザの、ファイルオーナー無視して読み書きしたり、ポート1024以下を開いたり、誰のプロセスでもKILLできたり、という特権を細分化したもの。
ポート80開くためだけにroot権限やるのはやり過ぎ、という時に、そのための権限NET_BIND_SERVICE
だけ付与するということができる。
例えばpingにはrawソケットが必要なので、NET_RAW
が付与されていた。(ubuntu 22.04の場合、ただ実際は無くても動くらしい→ping を実行するのに CAP_NET_RAW は必要なくなっていた)
$ getcap /usr/bin/ping
/usr/bin/ping cap_net_raw=ep
#SUIDがついているわけではない
$ ll /usr/bin/ping
-rwxr-xr-x 1 root root 76672 Feb 5 2022 /usr/bin/ping*
ケーパビリティを落とすと何が良いか
dockerではデフォルトで複数のケーパビリティが付与されているので、そのままだとアプリケーションが本来意図してない操作ができてしまう。
これを落とすことで、攻撃を受けた際にも被害を抑えることができる。
最小特権の原則というヤツ。
Dockerでデフォルト付与されるケーパビリティ
まずデフォルトでどうなっているかを確認する。
リファレンスに記載があるとおり、何も指定しなければ以下がデフォルトで設定される。
AUDIT_WRITE, CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, MKNOD,
NET_BIND_SERVICE, NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT
しかしながら、Webサービスのdockerコンテナで以下のような権限は99割要らないはず。
- AUDIT_WRITE : カーネル監査ログに記録
- DAC_OVERRIDE : ファイル読み書き実行の権限チェックバイパス (要はrootが何でも読み書きできるアレ)
rootユーザ実行でのデフォルトケーパビリティ
コンテナ内 PID=1プロセスのケーパをcat /proc/1/status
で調べると、以下のようになっている。
$ cat /proc/1/status | grep Cap
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
有効無効のビット列が16進数で表示されており、これをcapsh
を使ってデコードすると、上記のデフォルト設定のとおりになっている。
$ capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
またCapXxxと何種類か種類が表示されているが、CapEffが有効なケーパ、CapBndが取得可能なケーパ。上記のデフォルト設定が有効で、またそれ以上は取得できないことが分かる。
なお、/procではなくgetpcaps
で調べると少ない有効ケーパが表示される場合があるが、これはプロセス起動後にプロセス内で不要なケーパを落としているもの。ケーパを設定するケーパも放棄していれば再度取得することもできない。例えばapacheのマスタープロセス(root)を見ると、cap_kill,cap_setgid,cap_setuid,cap_net_bind_service,cap_sys_chroot+ep
だけになっていた
non rootユーザ実行でのデフォルトケーパビリティ
(実行ファイルにcapabilityやSIDが設定されていない限り)同様にPID=1プロセスのケーパを調べると以下のようになっている。
有効ケーパCapEffは無し(すべて0)。CapBndには上記のデフォルト設定が入っている。
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
CapBnd内の権限昇格
CapEff全部0だし良いのでは?ケーパの取得も付与もできないし上げる方法無くない?と思えるが、予めファイルケーパビリティやSUIDが設定されている実行ファイルを起動すると、CapBndの範囲で権限昇格できてしまう。
例えば以下。
#Dockerfile内でchownをmychownにコピーしてケーパをつけておく(実行時には無理)
RUN useradd -m dockerman
RUN cp /usr/bin/chown /home/dockerman/mychown
RUN setcap 'cap_chown+ep' /usr/bin/chown
USER dockerman
#dockerコンテナ内で
# 状態は以下
# CapEff: 0000000000000000
# CapBnd: 00000000a80425fb
$ touch hoge ← 適当なファイルを用意
$ ls -al hoge
-rw-r--r-- 1 dockerman dockerman 0 Jul 31 11:42 hoge
$ chown 1:1 hoge ←CapEffにケーパがないのでchown失敗
chown: changing ownership of 'hoge': Operation not permitted
./mychown 1:1 hoge ←ファイルにケーパがあるので成功(CapBnd内)
$ ls -al hoge
-rw-r--r-- 1 daemon daemon 0 Jul 31 11:39 hoge
間違ってコンテナ内に変な実行ファイルが混入する等があっても、docker起動オプションでcapabilityを落としておけばこのような権限昇格が防げるので、non rootユーザでもcapabilityを落とす価値がある。
なおこのような権限昇格は--security-opt=no-new-privileges
でも防げる。冗長になるが、念のためcapabilityを落とす設定と合わせて両方設定しておきたい。
設定例
rootユーザ実行コンテナ
必要なcapは動かすアプリケーション次第なので調べるしか無い。
nginx, php-fpm (root実行)
nginxやphp-fpmのように、PID=1の親プロセスはrootユーザで実行、子プロセスはnginxユーザ等で、ワーカーを作ったり消したりするアプリケーションの場合は、以下3つが必要になる。
- KILL : 子プロセスをKILLするのに必要(ユーザIDが異なるのでcapが要る)
- SETUID : 子プロセスのユーザIDを設定するのに必要
- SETGID : 子プロセスのグループIDを設定するのに必要
また、80番ポートなど、1024以下のポートを使う場合は以下も必要。
- NET_BIND_SERVICE : 1024番以下のポート開くのに必要
docker起動オプションに指定する場合、docker run --cap-drop=ALL --cap-add=KILL --cap-add=SETUID --cap-add=SETGID
と全drop後に必要なものだけaddという書き方が出来る。(docker-composeでも同じ)
おまけで--security-opt=no-new-privileges
も付けておく。
non-rootユーザ実行コンテナ(全般)
nginx-unprivilegedやphp-fpmをnon rootで動かす場合など。
この場合はどうせ有効ケーパ無しになので、何も考えずに設定も--cap-drop=ALL
としておいてOK。
ファイルケーパやSUIDを活用しているとエラーになるが、コンテナでコレを使うのは稀だと思う(多分)。
なお--cap-drop=ALL
を指定すると--security-opt=no-new-privileges
とは完全に冗長となるが、逆に悪影響もないので、将来のミス防止のためにも両方つけて置く。
必要なケーパビリティの調べ方
- とりあえず動かしてエラー見る
-
chown: changing ownership of 'hoge': Operation not permitted
のようにエラーが出るので割りと分かる
-
-
https://github.com/aquasecurity/tracee で確認できるらしい
- コンテナセキュリティより。必要なコンテナが上の例で上手く動いてしまったので試してない
※07/31 全体的に修正