2017年2月3日
–cap-dropオプションを使ったDockerコンテナの安全性を高める工夫
(2016-10-17)by Dan Walsh
本記事は、原著者の許諾のもとに翻訳・掲載しております。
DockerにはLinuxのケーパビリティを削除するためのオプションがあるのをご存じでしたか? docker run --cap-drop
オプションを使うと、コンテナのルートを隔離することができ、コンテナ内でのアクセス権を制限することができます。悲しいことに、ほとんどの人はコンテナやそれ以外の場所でも、セキュリティを強化していません。
翌日では手遅れ
ITの世界ではセキュリティへの配慮が遅すぎるという残念な傾向があります。 セキュリティが破られた翌日に初めて、セキュリティ対策システムが購入されているのです 。
ケーパビリティを落とすことで、コンテナのセキュリティを大変手っ取り早く改善することができます。
Linuxのケーパビリティとは?
ケーパビリティのmanページ によると、 capabilities
とは、個別に有効無効を設定することができる特権の集まりのことです。
私流に説明すると、多くの人がルートを万能なものとして考えますが、これが全体像ということではなく、全ケーパビリティを備えた root
ユーザが万能だということです。 ケーパビリティがカーネルに加えられたのは15年ほど前のことで、ルートの権限を分割するために追加されました 。
当初、カーネルが32ビットのビットマスクを割り当ててケーパビリティを定義していましたが、数年前に割り当てを64ビットまで拡張しました。現在では、大体38のケーパビリティが定義されています。
ケーパビリティとは、生のIPパケットを送る能力や、1024未満のポートにバインドする機能のようなものです。コンテナを実行する際に、大部分のコンテナ化されたアプリケーションの実行に影響を出すことなく大量のケーパビリティを削除することができます。
ほとんどのケーパビリティはカーネルまたはシステムを操作するためにあり、コンテナのフレームワーク(Docker)で使われるだけで、コンテナ内部で実行されるプロセスで使われることはめったにありません。しかし、コンテナによっては幾つかのケーパビリティを必要とするものもあります。例えば、特権を削除するためにsetuidやsetgidのようなケーパビリティを必要とするようなものがあります。コンテナの世界での大半のものと同じように、セキュリティと作業を完了させる能力との間で妥協が成立するように考えられています。
数年前、 grsecurityのメンバーがケーパビリティについてある分析を行い 、多くのケーパビリティがシステムへのフルアクセスに近い権限を与えることが分かっています。
幸いなことに、私たちはSELinux、 seccomp や namespaces のような追加的なツールも使いながらホストシステムをコンテナから保護することができます。
結論:コンテナから多くのケーパビリティを削除することは、セキュリティの観点で優れた発想です。
注:コンテナを立ち上げる際にコンテナのフレームワークがケーパビリティを削除しておけば、たとえコンテナ内のプロセスが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_write
、 kill
、および 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とマッチすることが求められる操作において、権限チェックをバイパスする機能を提供する、とあります。例えば chmod
や utime
での権限チェックはバイバスされます。但し cap_dac_override
と cap_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
ここから setfcap
、 audit_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
さらに発展して、例えば setuid
と setgid
だけが必要な場合、全てのケーパビリティを削除して setgid
と setuid
を追加してもいいでしょう。
# 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
最後に
実行中のコンテナには、必要以上に多くの特権を持っているものが少なくありません。コンテナがプロダクション環境にある場合には、こうしたケーパビリティは削除した方がいいでしょう。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa