Docker のセキュリティ

Docker のセキュリティを考えてみる上では、主要な観点が 4 つあります。

  • カーネルに元からあるセキュリティと名前空間や cgroup のサポート。

  • Docker デーモンそのものの攻撃領域。

  • コンテナ設定プロファイルにおける抜け穴。デフォルトの場合だけでなくユーザーによるカスタマイズ時も含む。

  • セキュリティ強化されたカーネル機能とそれがコンテナとやり取りする方法。

カーネルの名前空間

Docker コンテナは LXC コンテナによく似ています。 どちらも同じようなセキュリティ機能を持っています。 docker run によってコンテナを起動させると Docker の内部処理では、コンテナが利用する名前空間やコントロールグループが生成されます。

名前空間とは、初めて提供された最もストレートな形の分離技術のことです。 コンテナ内部にて起動されるプロセスからは、他のコンテナ内部やホストシステム内のプロセスを参照することはできず、また影響もほぼ及ぼしません。

各コンテナでは独自のネットワークスタックを用います。 これはつまり、別のコンテナのソケットやインターフェースへアクセスする際に、特権的なアクセス権限を有していないということです。 もちろんホストシステムが適切に設定されていれば、コンテナ間はそれぞれのネットワークインターフェースを介して通信を行うことができます。 外部にあるホストとの間で通信しているようなものです。 コンテナに対して公開ポートを指定するか、あるいは リンク機能 を利用すれば、コンテナ間での IP トラフィックが許可されます。 その場合コンテナ間にて互いに ping を行い、UDP パケットの送受信することで TCP コネクションが確立されます。 ただし状況に応じて制限がかけられることもあります。 ネットワークアーキテクチャの点でいうと、特定の Docker ホスト上にあるコンテナはすべて、ブリッジインターフェース上に置かれます。 これは各コンテナがあたかも実際に存在する物理的なマシンのようであり、共有するイーサネットスイッチにより通信を行っているようなものです。 これ以上でもなく、これ以下でもありません。

ではカーネルの名前空間やプライベートネットワーク機能のソースコードは、成熟したものになっているでしょうか。 カーネルの名前空間が導入されたのは カーネル 2.6.15 から 2.6.26 の間 です。 つまり 2008 年 6 月(2.6.26 のリリース日)以降、名前空間のソースコードは、数多くの本番環境システムを通じて検証が続いている状態です。 それだけではありません。 名前空間のソースコードの設計と発想は、もはや古いものになっています。 そもそも名前空間は OpenVZ の機能を再実装するという努力から生まれたものであり、カーネルのメインストリームにマージされることを目指したものです。 ちなみに OpenVZ が初めてリリースされたのは 2005 年であり、その設計と実装はともに十分成熟しています。

コントロール・グループ

コントロール・グループは、Linux コンテナ技術のもう一つの重要コンポーネントです。 これはリソース管理と利用制限を実装します。 これにより有用なメトリクスが数多く提供されます。 そしてこの機能はメモリ、CPU、ディスク I/O を各コンテナが共有して利用できるようにします。 さらに重要なのは、たった 1 つのコンテナがリソースを大量消費し、それがシステムダウンにつながるようなことはありません。

この機能の役割は、あるコンテナから別コンテナのデータやプロセスに対して、アクセスや変更を防ぐというものではありません。 これはサービス妨害攻撃を防ぐという重要な役割を持っています。 特に重要となるのが、公開あるいはプライベート PaaS のようなマルチテナント型プラットフォームにおいてです。 いずれかのアプリケーションが誤動作をし始めたとしても、安定した稼動(とパフォーマンス)を保証するものです。

コントロール・グループも同じく、登場してからさほど経過していません。 その開発は 2006 年に始まり、カーネルに初めてマージされたのは 2.6.24 のときです。

Docker デーモンの攻撃領域

コンテナ(およびアプリケーション)を Docker とともに動作させるということは、暗に Docker デーモンを動作させるということです。 デーモンの起動には rootless モード (試験的機能) を用いるのでない限りは root 権限を必要とします。 したがって重要な点をいくつか意識しておく必要があります。

まず第一に、Docker デーモンを制御できるのは信頼できるユーザーのみとすべき ということです。 Docker の強力な機能の中には、この問題が直接関係するものがあります。 特に Docker においては Docker ホストとゲストコンテナの間でのディレクトリ共有が可能であり、つまりコンテナのアクセス権拡大を許しているわけです。 ということは、コンテナの /host ディレクトリをホスト上の / ディレクトリに割り当ててコンテナを起動できることを意味し、それはコンテナが何ら制限なくホストのファイルシステムを変更できてしまうことになります。 ちょうど仮想化システムがファイルシステムというリソースをどのように共有するかという問題と同じです。 仮想マシンを使ってルートファイルシステムを(あるいはルートブロックデバイスでさえ)共有化できてしまうことは、防ぎようがありません。

これはセキュリティに重大な影響を及ぼします。 たとえば Docker の API を通じて、ウェブ・サーバをコンテナにプロビジョニングするとします。 このときには、通常以上に十分なパラメータ・チェックを行う必要があります。 そして悪意のあるユーザーがパラメータに細工をしたとしても、Docker から任意のコンテナが生成されないようにすることが重要です。

このことから REST API のエンドポイント(Docker デーモンとやり取りするために Docker CLI により用いられるもの)が Docker 0.5.2 において変更され、127.0.0.1 にバインドされる TCP ソケットではなく UNIX ソケットを用いるようになりました。 (TCP ソケットは、VM の外にあるローカルマシン上に直接 Docker を起動したときに、CSRF (cross-site request forgery) 攻撃を受けやすくなります。) そこで従来からある Unix パーミッションチェックを利用して、制御ソケットへのアクセスを制限する必要があります。

また明確に意図するのであれば、REST API を HTTP を介して送ることもできます。 ただしこれを行った場合には、前述したセキュリティの脅威に関して注意しておくことが必要です。 ファイルウォールを利用していて、ネットワーク内の他ホストから REST API エンドポイントへのアクセスを制限しているとします。 それでもそのエンドポイントはコンテナからアクセスが可能であるため、アクセス権限を容易に昇格させることができてしまいます。 したがって HTTPS と 証明書 を用いたセキュアな API エンドポイントの利用が必須となります。 また信頼できるネットワークや VPN からのみ到達可能とするような対処も求められます。

SSH over TLS を実現したいのであれば、DOCKER_HOST=ssh://USER@HOSTssh -L /path/to/docker.sock:/var/run/docker.sock を用いることもできます。

デーモンへの入力として、たとえば docker load 実行時はディスクから、また docker pull 実行時はネットワークから、それぞれイメージロードが行われますが、こういった入力には潜在的にぜい弱性があります。 Docker 1.3.2 において、イメージの抽出は Linux/Unix プラットフォーム上の chroot によるサブプロセス内にて行われるようになりました。 これは権限を分離することを賢明に目指した第一歩でした。 Docker 1.10.0 になるとイメージはすべて、イメージデータの暗号化チェックサムによって保存されアクセスされるようになりました。 既存イメージに対して攻撃を仕掛けられる可能性を軽減するものです。

サーバ上に Docker を稼動させる際には、Docker だけを動かすようにすることをお勧めします。 さらに他のサービスは Docker によって管理されたコンテナ内に移動するようにしてください。 もちろんお気に入りの管理ツール(おそらく SSH サーバには最低必要なものでしょう)があれば、引き続き利用してください。 同様に NRPE や collectd のような既存の監視プロセスを利用してもかまいません。

Linux カーネルのケーパビリティ

デフォルトにおいて Docker は、ケーパビリティを限定的に利用してコンテナを起動します。 これはどういう意味でしょう。

ケーパビリティとは「ルートか非ルートか」という 2 値による区分けを、アクセス制御システム上に対してきめ細かく実現するものです。 1024 番ポート以下に割り当てさえすればよいプロセス(たとえばウェブ・サーバ)なら、root として実行する必要はありません。 代わりに net_bind_service ケーパビリティを与えるだけで十分です。 この他にも数多くのケーパビリティがあるので、root 権限が通常必要とされる場面のほとんどすべてに利用することができます。

コンテナーセキュリティにおいてこれは実に多くのことを意味します。 どういうことなのか見ていきます。

典型的なサーバであれば、プロセスの多くは root によって起動されています。 たとえば SSH デーモン、cron デーモン、ログデーモン、カーネルモジュール、ネットワーク設定ツールなどです。 ただコンテナでは話が違います。 そもそもこういったタスクのほぼすべては、コンテナ外部にあるインフラストラクチャによって取り扱われるものだからです。

  • SSH アクセスを管理するのは、通常は Docker ホスト上に稼動するサーバープロセスです。

  • cron は必要な場合は、ユーザ・プロセスとして起動させます。 スケジュール・サービスを必要とするアプリ向けに特化させるものであり、プラットフォーム全体の機能として用いるものではありません。

  • ログ管理も通常は Docker が取り扱います。 あるいは Loggly や Splunk といったサードパーティ製のサービスを利用することもあります。

  • ハードウェアを管理することは的はずれです。 コンテナ内部において udevd やそれに類するデーモンを起動させる必要はまったくありません。

上からわかるように、たいていの場合、コンテナが「本当の」root 権限を必要とすることは まったくない ということです。 つまりコンテナは、ケーパビリティを最小限にして実行可能であって、コンテナ内の「root」は、本当の「root」よりも少ない権限で済むことを意味します。 したがって以下のようなことが可能になります。

  • 「mount」操作はすべて許可しない。

  • 生の(raw)ソケットへのアクセスを許可しない。(パケット・スプーフィング防止のため)

  • ファイルシステムへの所定操作を許可しない。 デバイス・ノードの新規生成、ファイルの所有者変更、属性変更(変更不能フラグを含む)など。

  • モジュールロードを許可しない。

  • その他もろもろ。

上記のようなことをすれば、たとえ侵入者がコンテナ内の root 権限を得ようとしても、重大なダメージを及ぼすことはまず困難であり、またホストの権限まで奪うようなことにはなりません。

普通のウェブ・アプリに対しての影響はありません。 しかも悪意あるユーザからの攻撃はかなり抑えられることになります。 デフォルトで Dockerは全ケーパビリティを拒否した上で、必要となるケーパビリティ を用います。 つまりブラックリスト方式ではなくホワイトリスト方式をとるものです。 利用可能なケーパビリティの一覧は Linux man ページ を参照してください。

Docker コンテナ実行時の主なリスクと言えば、コンテナに与えられるデフォルトのケーパビリティやマウント状況だけでは、完全なコンテナ分離にはならないことです。 独立となっていない場合や、カーネルのぜい弱性との組み合わせによることも考えられます。

Docker のデフォルトにはないプロファイルを使えば、ケーパビリティの追加および削除が可能になります。 これを使ってケーパビリティを削除すれば、Docker は一層安全な状態になり、ケーパビリティを加えれば、それだけ安全性は低下することになります。 ユーザにとってのベストプラクティスは、全ケーパビリティは削除した上で、実行するプロセスに必要となるもののみを明示的に利用する方法をとることでしょう。

Docker Content Trust の署名認証

Docker Engine では、署名されているイメージだけを実行するように設定することができます。 Docker Content Trust における署名認証は dockerd 実行モジュール内に直接ビルドされています。 この機能は dockerd の設定ファイルを通じて設定することができます。

この機能を有効にするには daemon.json において trust-pinning により設定します。 これにより、ユーザが指定したルート鍵によって署名されたリポジトリに対してのみ、イメージをプルして実行できるようになります。

以前は CLI においてイメージに対する署名認証を実現していましたが、この機能によって管理者の理解がより深く浸透しました。

Docker Content Trust の署名認証方法の詳細は Docker における Content trust に進んでください。

その他のカーネル・セキュリティ機能

ケーパビリティは、最近の Linux カーネルが提供する多くのセキュリティ機能の一つです。 もちろんよく知られた既存のシステムとして、TOMOYO、AppArmor、SELinux、GRSEC を利用するのでもかまいません。

現時点において Docker はケーパビリティを有効にするだけであって、他のシステムを妨害するものではありません。 そこで Docker ホストのセキュリティ向上には、いくらでも方法が残されています。 以下に数例を示します。

  • GRSEC や PAX を利用してカーネルを起動することができます。 これにより、コンパイル時や実行時に多くの安全性チェックを行うことができます。 またアドレスのランダム化のような技術のおかげで、悪用の機会を大きく減らすことができます。 これに対して Docker 固有の設定は不要です。 なぜならこのセキュリティ機能はシステム全体に適用されるものであって、コンテナーからは切り離されているものだからです。

  • 利用しているディストリビューションに Docker コンテナ用のセキュリティ・モデル・テンプレートが用意されているなら、それをそのまま利用することができます。 たとえば AppArmor にて動作するテンプレートを我々は提供しています。 また Red Hat は Docker 向けの SELinux ポリシーを提供しています。 こういったテンプレートは追加の安全策となるものです。 (もっともケーパビリティとかなりの部分で重複するところがあります。)

  • 好みのアクセス管理メカニズムを使って、独自にセキュリティポリシーを定義することができます。

同じように Docker 機能を増強させるサードバーティ製ツールを利用することもできます。 特別なネットワーク・トポロジーや共有ファイルシステムの構築が可能です。 このようなツールは Docker 自体を修正することなく Docker 機能を強力にするためのものです。

Docker 1.10 から Docker デーモンが直接、ユーザー名前空間をサポートするようになりました。 この機能を使えば、コンテナー内の root ユーザーを、コンテナー外部の uid がゼロではないユーザーに対して割り当てできるようになります。 コンテナーからのブレイクアウトのリスクを軽減することにつながります。 利用可能な機能ではあるのですが、デフォルトでは有効化されていません。

この機能に関しての詳細は、コマンドライン・リファレンス内の daemon コマンド を参照してください。 Docker 内のユーザ名前空間に対する実装については こちらのブログ投稿 に詳細が示されています。

まとめ

Docker コンテナはデフォルトにおいて十分に安全なものです。 コンテナ内部にて非特権ユーザによりプロセスを稼動させていれば、より安全です。

AppArmor、SELinux、GRSEC、あるいはセキュリティを堅牢にする適切なシステムを用いれば、安全性をさらに高めることができます。

Docker をより安全にする方法を検討している方は、Docker コミュニティ・フォーラムにおいて、機能リクエスト、プル・リクエスト、コメントをお寄せください。