Docker のセキュリティ

読む時間の目安: 2 分

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

  • カーネルに元からあるセキュリティと名前空間や cgroup のサポート。
  • Docker デーモンそのものの攻撃領域。
  • コンテナー設定プロファイルにおける抜け穴。デフォルトの場合だけでなくユーザーによるカスタマイズ時も含む。
  • セキュリティ強化されたカーネル機能とそれがコンテナーとやり取りする方法。

カーネルの名前空間

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

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

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

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

コントロールグループ

コントロールグループ(control group)は、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やそれに類するデーモンを起動させる必要はまったくありません。
  • ネットワーク管理はコンテナー外部で行われるものです。 可能な限り考えなくても済むものです。 つまり ifconfigroute、ip コマンドは実行する必要がありません。 (ただしコンテナーがルーターやファイアウォールとして動作するように構築しているのであれば別です。)

上からわかるように、たいていの場合、コンテナーが「本当の」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 コミュニティフォーラムにおいて、機能リクエスト、プルリクエスト、コメントをお寄せください。

Docker, Docker documentation, security