–cap-dropオプションを使ったDockerコンテナの安全性を高める工夫

Going atomic with Linux containers

DockerにはLinuxのケーパビリティを削除するためのオプションがあるのをご存じでしたか? docker run --cap-dropオプションを使うと、コンテナのルートを隔離することができ、コンテナ内でのアクセス権を制限することができます。悲しいことに、ほとんどの人はコンテナやそれ以外の場所でも、セキュリティを強化していません。

翌日では手遅れ

ITの世界ではセキュリティへの配慮が遅すぎるという残念な傾向があります。セキュリティが破られた翌日に初めて、セキュリティ対策システムが購入されているのです

ケーパビリティを落とすことで、コンテナのセキュリティを大変手っ取り早く改善することができます。

Linuxのケーパビリティとは?

ケーパビリティのmanページによると、capabilitiesとは、個別に有効無効を設定することができる特権の集まりのことです。

私流に説明すると、多くの人がルートを万能なものとして考えますが、これが全体像ということではなく、全ケーパビリティを備えたrootユーザが万能だということです。ケーパビリティがカーネルに加えられたのは15年ほど前のことで、ルートの権限を分割するために追加されました

当初、カーネルが32ビットのビットマスクを割り当ててケーパビリティを定義していましたが、数年前に割り当てを64ビットまで拡張しました。現在では、大体38のケーパビリティが定義されています。

ケーパビリティとは、生のIPパケットを送る能力や、1024未満のポートにバインドする機能のようなものです。コンテナを実行する際に、大部分のコンテナ化されたアプリケーションの実行に影響を出すことなく大量のケーパビリティを削除することができます。

ほとんどのケーパビリティはカーネルまたはシステムを操作するためにあり、コンテナのフレームワーク(Docker)で使われるだけで、コンテナ内部で実行されるプロセスで使われることはめったにありません。しかし、コンテナによっては幾つかのケーパビリティを必要とするものもあります。例えば、特権を削除するためにsetuidやsetgidのようなケーパビリティを必要とするようなものがあります。コンテナの世界での大半のものと同じように、セキュリティと作業を完了させる能力との間で妥協が成立するように考えられています。

数年前、grsecurityのメンバーがケーパビリティについてある分析を行い、多くのケーパビリティがシステムへのフルアクセスに近い権限を与えることが分かっています。

幸いなことに、私たちはSELinux、seccompnamespacesのような追加的なツールも使いながらホストシステムをコンテナから保護することができます。

結論:コンテナから多くのケーパビリティを削除することは、セキュリティの観点で優れた発想です。

注:コンテナを立ち上げる際にコンテナのフレームワークがケーパビリティを削除しておけば、たとえコンテナ内のプロセスがsetuidを実行したとしても、ケーパビリティを元に戻すことができません。詳しくはケーパビリティのmanページ内のCapability Bounding Setのセクションを見てください。

Dockerがデフォルトで与える権限

Dockerのコンテナ内において特権モードのプロセスにデフォルトで許可されるケーパビリティのリストを見てましょう:

chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap

OCI/runcの仕様では、上記ケーパビリティがドラスティックに制限され
audit_writekill、およびnet_bind_serviceのみがデフォルトで許可される状態であり、これに加えてユーザはocitoolsを使って別のケーパビリティを付加することができます。ご想像のとおり、不要なケーパビリティを忘れずに外すという義務を負うよりも、必要なケーパビリティを追加するというアプローチのほうが私は好きです。

Deep Dive into Capabilities

では、上に挙げたケーパビリティの詳細を見ていきましょう。

chown

manページではchownのことを、ファイルのUIDとGIDに任意の変更を加える機能として説明しています。

これは、ルートがファイルシステムのオブジェクトの所有者またはグループを変更できることを意味しています。コンテナ内でシェルを実行してパッケージをコンテナにインストールするなどしていない場合、このケーパビリティは削除すべきです。

私に言わせれば、プロダクション環境では、このケーパビリティはまったく必要ありません。必要になった時だけchownを有効にし、作業が終わったら速やかに無効にしましょう。

dac_override

manページの説明では、dac_overrideを使うと、ファイルの読み出し、書き込み、実行の権限チェックをルートがバイパスできるようになる、とあります。DACは、”Discretionary Access Control(任意アクセス制御)”の略です。

これが意味するのは、たとえ権限と所有者のフィールドが許可していなくても、許可されたプロセスであれば、システム上のどんなファイルでも読み書きあるいは実行ができるということです。しかし、DAC_OVERRIDEを必要とするアプリはほとんどなく、仮にあったとしてもそれは恐らくアプリが正常に動いていない時だと思われます。ディストリビューション全体を見ても、これを実際に必要とするのは10に満たないのではないでしょうか。もちろん管理者用のシェルでは、ファイルシステムでの不正な権限を修正するためにDAC_OVERRIDEが必要になることはありますが。

Red Hatのセキュリティ標準の専門家であるSteve Grubbは次のように述べています。”この権限が必要になることはまずないはずです。もしコンテナがこれを必要とする場合、コンテナの不具合を疑った方がいいでしょう。”

fowner

manページによれば、fownerは、通常プロセスのファイルシステムUIDがファイルのUIDとマッチすることが求められる操作において、権限チェックをバイパスする機能を提供する、とあります。例えばchmodutimeでの権限チェックはバイバスされます。但しcap_dac_overridecap_dac_read_searchによりチェックが行われる操作はバイパス対象外となります。manページによる詳細は次の通りです。

  • 任意のファイルに拡張ファイル属性(chattr(1)を参照)を設定する。
  • 任意のファイルのアクセス制御リスト(ACL)を設定する。
  • ファイル削除時にディレクトリのスティッキービットを無視する。
  • open(2)やfcntl(2)で、任意のファイルにO_NOATIMEを指定する。

DAC_OVERRIDE同様、例外的にソフトウェアインストールツールで使われる可能性はありますが、ほとんどのアプリケーションではこれは必要とされないでしょう。コンテナはこのケーパビリティがなくても問題なく動作すると思います。なお、docker buildで必要になることがあるかもしれませんが、コンテナをプロダクション環境で実行する際にはブロックすべきです。

fsetid

manページでは、”ファイルが変更された時に、set-user-IDとset-group-IDのモードビットをクリアするのではなく、GIDがファイルシステムや呼び出しプロセスの追加的なGIDと一致していないファイルに対してset-group-IDビットを設定する”とあります。

私の見解:インストールを実行中でなければ、恐らくこのケーパビリティは必要ないでしょう。私なら初期設定でこれを無効にします。

kill

プロセスにこのケーパビリティがある場合、”シグナルを送信するプロセスの実UIDまたは実効UIDは、シグナルを受信するプロセスの実UIDまたは実効UIDと一致しなければならない”という制限を無効にすることができます。

このケーパビリティが基本的に意味するところは、ルートを所有するプロセスが非ルートプロセスに対して、killシグナルを送ることができるということです。もしコンテナが全てのプロセスをルートとして実行していたり、ルートプロセスが非ルートとして実行中のプロセスを強制終了したりしないようであれば、このケーパビリティは必要ないでしょう。コンテナ内でsystemdをPID1として実行し、別のUIDで実行中のコンテナを停止したい場合、このケーパビリティが必要になることがあるかもしれません。

なお、危険度についても言及しておくことも価値があると思いますが、このケーパビリティの危険度は低い方に分類されます。

setgid

manページによると、setgidケーパビリティは、プロセスのGIDおよび追加のGIDリストに対して、プロセスが任意の操作を行えるようにします。また、UNIXドメインソケット経由でソケットの資格情報(credential)を渡す際に偽のGIDを作ったり、ユーザ名前空間にグループIDマッピングを書き込んだりもします。詳細については、user_namespaces(7)を参照してください。

つまり、このケーパビリティを有するプロセスは、自身のGIDを他の任意のGIDに変更できるというわけです。基本的に、システム上の全てのファイルに完全なグループアクセスを許可することになります。コンテナプロセスがUID/GIDを変更しない場合は、このケーパビリティは必要ないでしょう。

setuid

プロセスにsetuidケーパビリティがある場合、”プロセスのUIDに対して任意の操作(setuid(2)、setreuid(2)、setresuid(2)、setfsuid(2))を行うことができます。また、UNIXドメインソケット経由でソケットの資格情報(credential)を渡す際に偽のUIDを作ったり、ユーザ名前空間にユーザIDマッピングを書き込んだりもできます(user_namespaces(7)を参照)”

このケーパビリティを有するプロセスは、自身のUIDを他の任意のUIDに変更できます。基本的に、システム上の全てのファイルに完全なグループアクセスを許可することになります。コンテナプロセスがUID/GIDを常に同じUID(望ましくは非ルート)として変更しない場合は、このケーパビリティは必要ありません。1024未満のポートにバインドするためにルートとして起動されることが多いsetuidを必要とするアプリケーションなどでは、UIDを変更してケーパビリティを削除します。ポート80にバインドされたApacheには通常、ルートとして起動するnet_bind_serviceが必要です。その後、setuid/setgidを使用してapacheのユーザを切り替え、ケーパビリティを削除します。

ほとんどのコンテナでは、setuid/setgidケーパビリティを安全に削除できます。

setpcap

manページの説明を見てみましょう。”呼び出し元のスレッドのケーパビリティバウンディングセットに含まれる任意のケーパビリティを追加したり、(prctl(2) PR_CAPBSET_DROPを使って)ケーパビリティを削除したり、securebitsフラグを変更したりします”

平易に言ってしまうと、このケーパビリティを有するプロセスでは、バウンディングセット内の現在のケーパビリティセットを変更できます。つまり、バウンディングセットという制限内であれば、プロセスに必要/不要なケーパビリティを追加/削除できるというわけです。

net_bind_service

これは簡単です。このケーパビリティがあれば、特権ポート(例:1024未満のポート)にバインドすることができます。

1024未満のポートにバインドする場合はこのケーパビリティが必要です。一方、現在実行中のプロセスが1024以上のポートをリッスンするのであれば、このケーパビリティは削除してよいでしょう。

このケーパビリティのリスクとして挙げられるのは、あるサービスを解釈しユーザのパスワードを収集するようなsshdのようなプロセスです。このリスクは、別のネットワーク名前空間でコンテナを実行することで軽減できます。ただし、コンテナプロセスがパブリックネットワークインターフェイスに到達することは困難でしょう。

net_raw

manページの説明は次の通りです。”RAWソケットとPACKETソケットを使用します。透過的プロキシでの任意のアドレスにバインドを許可します”

このアクセスにより、プロセスはネットワーク上のパケットを偵察することができます。あまりいいことではなさそうですよね。確かにそうです。ほとんどのコンテナプロセスはこのアクセスを必要としないため、恐らく削除すべきでしょう。ちなみに、これは実行中のコンテナプロセスと同じネットワークを共有するコンテナにのみ影響し、通常、実際のネットワークへのアクセスを阻止します。

RAWソケットもまた、攻撃者に対してネットワークを混乱に招く能力を与えてしまいます。pingコマンドも使うオプションによっては、このアクセスが必要になることもあります。

sys_chroot

このケーパビリティによりchroot()の使用が可能になります。言い換えると、異なるrootfsにプロセスがchrootできるようになります。chrootは恐らくコンテナ内で使われていないでしょうから、そういう場合は削除してください。

mknod

このケーパビリティがあれば、mknodを使ってスペシャルファイルを作成可能です。

これにより、プロセスはデバイスノードを作成できます。コンテナには通常、/devに必要な全てのデバイスノードが提供されており、デバイスノードの作成はデバイスノードcgroupによって制御されます。私の意見では、これはデフォルトの状態で削除すべきだと思います。これを行うコンテナはほとんどなく、ましてや本当にそれをすべきコンテナとなると更に少数です。

audit_write

これがあれば、カーネル監査のログにレコードを書き込むことができます。ただし、監査ログへの書き込みを試みるプロセスはほとんどありませんし(ログインプログラム、su、sudo)、コンテナ内部のプロセスは恐らく信頼できないでしょう。監査サブシステムは現在、名前空間を意識したものではないため、これはデフォルトで削除すべきでしょう。

setfcap

最後に、setfcapケーパビリティを使用すると、ファイルシステムのファイルケーパビリティを設定できます。ビルド中にインストールを行う場合に必要となることもありますが、プロダクション環境では恐らく削除した方がいいでしょう。

Dockerで、これらのケーパビリティを削除する方法

では、dockerを使って、これらのケーパビリティを削除するにはどうすればいいのでしょうか。まずは、プロセスにどんなケーパビリティが与えられているか見てみましょう。Linuxにはpscapと呼ばれる、プロセスが持つケーパビリティを見ることができる素晴らしいツールがあり、Fedoraのlibcap-ng-utilsパッケージで提供されています。

以下は、pscap | head -10によるサンプル出力になります。

ppid  pid   name        command         capabilities
1   1082  root      systemd-journal   chown, dac_override, dac_read_search, fowner, setgid, setuid, sys_ptrace, sys_admin, audit_control, mac_override, syslog, audit_read
1   1116  root      systemd-udevd   full
1   1760  root      auditd          full
1760  1778  root        audispd         full
1   1812  root      mcelog          full
1   1815  root      bluetoothd      net_bind_service, net_admin
1   1816  root      ModemManager    full
1   1817  root      systemd-logind  chown, dac_override, dac_read_search, fowner, kill, sys_admin, sys_tty_config, audit_control, mac_admin
1   1818  root      rngd            full

以下は、実行中の通常のコンテナのケーパビリティです。

#  docker run -d fedora sleep 5 >/dev/null; pscap | grep sleep
26358 26375 root        sleep           chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap

ここからsetfcapaudit_writeそれからmknodを削除したい場合、--cap-drop=setfcap --cap-drop=audit_write --cap-drop=mknodを使います。

#  docker run -d --cap-drop=setfcap --cap-drop=audit_write --cap-drop=mknod fedora sleep 5 > /dev/null; pscap | grep sleep
26555 26571 root        sleep           chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot

さらに発展して、例えばsetuidsetgidだけが必要な場合、全てのケーパビリティを削除してsetgidsetuidを追加してもいいでしょう。

#  docker run -d --cap-drop=all --cap-add=setuid --cap-add=setgid fedora sleep 5 > /dev/null; pscap | grep sleep
26767 26783 root        sleep           setgid, setuid

コンテナラベルと[atomic run](http://www.projectatomic.io/docs/usr-bin-atomic/)コマンドを使って、コンテナが実行するデフォルト実行コマンドを定義できます。

# cat Dockerfile
FROM fedora
LABEL RUN /usr/bin/docker run -d --cap-drop=all --cap-add=setuid --cap-add=setgid  \${IMAGE} sleep 10
# docker build -t sleep . >/dev/null
# atomic run  --quiet sleep > /dev/null; pscap | grep sleep
32119 32135 root        sleep           setgid, setuid

最後に

実行中のコンテナには、必要以上に多くの特権を持っているものが少なくありません。コンテナがプロダクション環境にある場合には、こうしたケーパビリティは削除した方がいいでしょう。