ブログ

すべてのテナントに専用VMを割り当てるべきか?

リスクベースの意思決定フレームワークと、各オプションを防御可能にするハードニング手順を用いて、テナントごとのコンテナ、VM、ハイブリッド構成の選択肢を比較検討します。

概要

マルチテナントホスティングでは、テナントが互いにどこまで到達できるかを選択する必要があります。コンテナはLinux名前空間とcgroupを使用してプロセスとリソースを分離しますが、ホストカーネルを共有します。仮想マシンはハードウェアレベルの境界を追加しますが、速度と運用負荷のコストがかかります。ハイブリッドアプローチ(VM内のコンテナ)はその両方を実現できますが、パッチ適用が必要な対象領域が2倍になります。この記事では、リスクベースの意思決定、比較表、そしてVM内でも重要なDockerのハードニング手順を説明します。最後には、どの分離モデルがテナントに適しているか、起動前に何を設定すべきかがわかります。

マルチテナントアプリはほぼ完成しています。顧客ごとにスタックを起動するDocker Composeファイルがあり、高速です。ある時、ホスティング会社を経営する友人がこう尋ねます。「各テナントに専用VMを割り当てていますか?」あなたは固まります。その質問は想定していませんでした。この記事は、今日、セキュリティチームなしでそれに答える方法を提供します。あなたは一人でこれを行うので、その決定は午前2時に弁護できるほどシンプルである必要があります。

「最善の」モデルを見つけようとするのはやめましょう。まず、テナントのコードがホストを乗っ取った場合に何が起こるかを書き出してください。ツールを選ぶ前に爆発半径を定義してください。その演習は、どのベンチマークよりも多くのことを教えてくれます。

カーネルは追い出せないルームメイト

コンテナはホストカーネルを共有するため効率的です。その共有こそが全てのトリックであり、全てのリスクです。Linux名前空間は各コンテナにプロセス、ネットワーク、ファイルシステムの独自のビューを提供します。制御グループ(cgroups)はCPU、メモリ、ディスクI/Oを制限できるため、1つのテナントが他のテナントを枯渇させることはありません。しかし、どちらもハードウェアの壁を作り出すわけではありません。

コンテナを、非常に優れた偽のIDを持つプロセスと考えてください。それは自分が専用マシン上にいると信じています。しかし、カーネルはホスト上で動作するLinuxの単一コピーです。テナントがカーネルの脆弱性を悪用すると、名前空間は単なるメタデータに過ぎなくなります。カーネル関数を呼び出せる攻撃者は、同じカーネル上の他の名前空間に到達できます。それがあなたがよく耳にするコンテナエスケープです。

あなたがクライアントごとに1つのコンテナを持つ小さなB2Bツールをホストしているとします。クライアントがリモートコード実行バグのある怪しいプラグインをインストールしました。デフォルトのDocker設定では、そのプロセスはコンテナ内でrootとして実行されています。コンテナ内のrootは依然としてUID 0であり、明示的にユーザーをマッピングしない限り、カーネルはそのUIDをホストのrootと区別しません。攻撃者は脱出を試みることができ、共有カーネルが彼らの標的です。

障害は劇的である必要はありません。単一のテナントがメモリをリークすると、ホストがスワップに追いやられ、他のすべてのテナントが遅くなる可能性があります。cgroup制限がない場合、1つの誤動作ループが可用性攻撃になります。制限があれば、それはブロックされたプロセスとアラートです。

これはコンテナが安全でないという意味ですか?いいえ。カーネルを共有信頼ゾーンとして扱わなければならないという意味です。選択する前に、1段落のリスクステートメントを書いてください:「テナントのコンテナが侵害された場合、攻撃者は[リスト]にアクセスできます。ビジネスコストは[金額または影響]になります。」その段落があなたを怖がらせるなら、あなたは妄想家ではありません。正直者です。

共有コンテナから完全に分離されたスタックまでの分離スペクトラムについて詳しくは、マルチテナントDockerアーキテクチャの設計に関するガイドをご覧ください。

3つの分割方法(デプロイ前に1つ選ぶ)

マルチテナント分離には実際に3つのアーキテクチャがあります。すべての「ベストプラクティス」はこれらの組み合わせです。

アプローチ分離バリア最適な場面最も難しい注意点
テナントごとのコンテナカーネル名前空間 + cgroup多数の小規模テナント、テナントあたりのリスクが低い、密度が必要1つのカーネルエクスプロイトがそのホスト上のすべてのテナントを破壊する可能性がある
テナントごとに1つのVMハイパーバイザー/ハードウェア仮想化規制対象データ、敵対的なテナント、テナントあたりの価値が高いより重く、プロビジョニングが遅く、テナントごとにOSにパッチを当てる
VM内のコンテナコンテナ化されたワークロードを囲むVM境界密度とグループ間の強固なシェルコストと運用オーバーヘッドがほぼ2倍

テナントごとのコンテナ。 これはほとんどのSaaS創業者にとってのデフォルトです。各テナントは独自のコンテナまたは小さなComposeスタックを取得します。プロビジョニングは即座で、イメージは小さく、CI/CDは簡単です。リソース制限により、騒がしい隣人がサーバーを食い尽くすのを防ぎます。トレードオフは共有カーネルです。ワークロードを非特権に保ち、ホストに定期的にパッチを当てられるなら、これがしばしば正しい最初の一手です。

2つのテナントを同じコンテナに入れないでください。それは共有カーネルに加え、共有ランタイム、共有ファイルシステムを意味します。一方のテナントがプロセスを生成するファイルをアップロードした場合、他方のテナントはすでに同じプロセステーブルにいます。コンテナは分離の単位です。テナントごとに1つのコンテナにしてください。

データベースはどうでしょうか?すべてのテナントが同じ資格情報で1つのMongoDBまたはPostgreSQLインスタンスに接続する場合、すでに巨大な共有コンポーネントを追加しています。各テナントに別々の資格情報を付与し、理想的には別々のデータベースまたはスキーマを提供します。コンテナはアプリを分離します。データベースは攻撃者が最初にテストする漏洩経路であることがよくあります。

テナントごとに1つのVM。 各テナントに完全な仮想マシンを提供します。ハイパーバイザーはハードウェアレベルの境界を追加します。これはまさにカーネルエクスプロイトがホストに到達するために越えなければならないものです。これは規制対象環境やテナントが信頼できない場合に重要です。コストは密度と時間です。コンテナだけでなく、オペレーティングシステムのフリートを管理することになります。各VMにはアップデート、セキュリティエージェント、監視が必要です。個人の創業者にとっては、これは本物の作業です。

この層で機能するパターン:インフラストラクチャ・アズ・コードを使用して同じベースイメージからVMを作成し、ライブシステムにパッチを当てる代わりに新しいイメージにアップデートを焼き込み、認識できないワークロードを終了します。VMの管理ポートはインターネットに閉じておきます。

VM内のコンテナ。 このハイブリッドは初心者向けチュートリアルではほとんど議論されません。各テナント(または小さなテナントグループ)の周りに小さなVMを置き、そのVM内でコンテナを実行します。VMは爆発半径の容器であり、コンテナは単なるデプロイ可能な単位です。これにより、仮想化のハードエッジとイメージの再現性が得られます。仮想化オーバーヘッドとコンテナの柔軟性に支払うため、コストは高くなりますが、テナントを完全に信頼できない場合には、長期的には最も賢明なモデルになり得ます。

一般的なマイクロ例:テナントがNode APIとバックグラウンドワーカーを実行しています。両方のプロセスを持つ1つの大きなコンテナの代わりに、1つのVMを使用し、異なるリソース制限、共有ネットワーク、ワーカーの直接インターネット公開なしで2つのコンテナを実行します。VMがハードエッジを提供し、コンテナが構造を提供します。

どれを選ぶべきですか?表がショートリストです。次のセクションで決定を具体的にします。

コンテナを選ぶなら、次の6つのことを実行するか、やめるか

テナントごとのコンテナは、すべてのコンテナを潜在的な攻撃者として扱うなら問題ありません。それは願望ではなく、設定から始まります。

0. 誰も信頼する前にリソースを制限する。 cgroupは公平性のメカニズムであり、可用性の防御です。コンテナごとに--memory--cpusを設定します。メモリをリークするテナントは、サーバーではなく自分の制限に達するべきです。これはセキュリティ境界ではありませんが、騒がしい隣人はコードを一行も書かずに攻撃になります。実践的な出発点:--memory 512m --cpus 0.5。ワーカープロセスの場合は低く始めてスケールアップします。

1. 非rootユーザーとして実行する。 どうしても必要な場合を除き、コンテナプロセスにUID 0を使用させないでください。Dockerfileでユーザーを設定し、追加のガードとして--userを渡します。非特権ユーザーとして実行されるエクスプロイトは、カーネルへの経路がはるかに少なくなります。Dockerfileでユーザーを作成します:RUN useradd -u 10001 appUSER app。時間を節約するためにこれをスキップしないでください。

2. 不要なケーパビリティをすべてドロップする。 Linuxケーパビリティはrootの権限を小さな断片に分割します。ほとんどのWebアプリはほとんど必要としません。--cap-drop=ALLから始めて、必要なものだけを追加してください。CAP_SYS_ADMINのないコンテナは名前空間トリックに使用するのがはるかに困難です。アプリが特権ポートにバインドしようとする場合は、NET_BIND_SERVICEを付与する代わりに、高いポートで実行し、プロキシを前面に配置します。

3. ファイルシステムを読み取り専用にする。 アプリは自身のコンテナレイヤーに書き込むべきではありません。状態のためにtmpfsをマウントします。ディスクに書き込めない攻撃者は、永続化を仕込むのがはるかに難しくなります。侵害されたPHPアプリがウェブシェルを書き込もうとしても、ルートファイルシステムが読み取り専用なら失敗します。アプリが本当に必要とする書き込み可能なディレクトリには名前付きボリュームをマウントできます。

4. seccompとAppArmorまたはSELinuxを適用する。 これらはリスクの高いシステムコールを捨て山に送ります。Dockerにはデフォルトのseccompプロファイルが同梱されています。それを使用してください。別のレイヤーとしてAppArmorプロファイルを追加します。すべてのシステムコールをマスターする必要はありません。通常のWebワーカーが決して必要としないものを拒否するだけでよいのです。--privilegedで実行しないでください。そのフラグは、設定したほとんどすべての防御を無効にします。

5. ネットワークをセグメント化する。 すべてのコンテナに他のすべてのコンテナへのルートを与えないでください。デフォルトで拒否し、必要なポートだけを開きます。侵害されたデータベースコンテナが管理パネルをスキャンできないようにします。テナントが別々のネットワークにいる場合、あるネットワークの侵害は横方向に広がりません。

実践的な出発点:

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

同じフラグをComposeファイルに入れ、すべてのテナントに適用します。これは完全ではありませんが、docker runがそのまま提供するものよりもはるかに強力なデフォルトです。

詳細なウォークスルーについては、マルチテナントホスティングにおけるDockerコンテナのステップバイステップハードニングガイドをご覧ください。

Dockerの拡張コンテナ分離は、知っておくべき例外です

マネージドDocker環境内で実行している場合は、DockerのEnhanced Container Isolation(ECI)を探してください。これは、内部でユーザー名前空間分離とセキュアなコンテナランタイムを使用します。コンテナ内のrootはホスト上の非特権ユーザーにマッピングされるため、rootとして実行されているコンテナでもホストのroot権限を取得できません。また、危険なケーパビリティとシステムコールをデフォルトでブロックします。これはバニラDockerのいくつかのフラグで再現できるものではありません。プラットフォームがサポートしている場合は有効にしてください。非rootユーザーとリソース制限の必要性はなくなりませんが、リスクの計算が変わります。

Dockerデーモンでのユーザー名前空間再マッピング(userns-remap)を使用して、この一部を近似できます。これはセキュアランタイムほど完全ではありませんが、何もしないよりはましです。使用する場合は、信頼する前にUIDマッピングが機能することを確認してください。

VMの誤謬:仮想マシンへの移行はハードニングではない

ここからが逆説的な部分であり、ほとんどの人がスキップする部分です。テナントごとに1つのVMに移行し、その中に通常のコンテナをデプロイしても、コンテナのセキュリティ問題は解決していません。広い檻を追加しただけです。コンテナエスケープは依然として機能します。攻撃者はホストではなくVMに着地するだけです。それは本当の改善ですが、それでも6つのステップが必要です。

もう1つの罠は、VM自体が安全であると仮定することです。弱いSSHパスワード、未パッチの基本パッケージ、または開いた管理ポートを持つデフォルトイメージは贈り物です。ハイパーバイザー境界は、ゲストがハードニングされ更新されている場合にのみ意味があります。そうでなければ、あなたの「安全なVM」は妥協へのより速い道です。安全だと感じてチェックをやめるからです。

VMがもたらすものは、爆発半径を縮小できることです。1つのテナントの災害は1つのVMに留まります。VMが費やすものはあなたの時間です。あなたはテナントの数と同じ数のオペレーティングシステムのシステム管理者になります。製品を出荷する個人の創業者なら、フリートにパッチを当てて監視する時間があるかどうか自問してください。あるなら、テナントごとのVMは正しい選択になり得ます。ないなら、強力なハードニングを備えたコンテナの方が正直かもしれません。

また、ハイパーバイザーホストは重要な標的であることを忘れないでください。侵害されたハイパーバイザーはすべてのゲストを見ることができます。ゲストだけでなくホストにもパッチを当ててください。VMはホストのパッチ適用を免除しません。それを怠った場合の賭け金を引き上げるだけです。

ハイブリッドに関する注意:VM内のコンテナが無料で「2層のセキュリティ」を提供すると想定しないでください。VMは境界を追加します。コンテナは依然として非root、ケーパビリティ、seccompを必要とします。そうでなければ、最初の層は最も弱いコンテナと同程度の強さしかありません。

10分で議論を決着させる4つの質問

抽象的に最適化しないでください。次の4つの質問に順番に答えてください。答えを書き留めてください。

1. テナントは何にアクセスできますか? テナントが自分のWebアプリとデータベースにしか到達できない場合、厳格なネットワークルールを持つテナントごとのコンテナは弁護可能です。テナントのデータが規制対象または金銭的に機密性が高い場合は、VMに移行します。

2. 1つのテナントの侵害はどれくらいのコストになりますか? 失われた顧客、法的エクスポージャー、信頼を合計してください。その数字がVMを実行するコストよりも大きいなら、お金を使います。そうでなければ、コンテナは合理的な選択です。

3. テナントは何人いて、どれくらい支払っていますか? 多数の小規模サブスクライバー:コンテナ密度が重要です。少数の大規模アカウント:それぞれにVMを提供し、それに応じて請求します。コーヒー1杯分未満しか支払わないテナントは、それぞれにOSの管理を要求すべきではありません。

4. 定期的にパッチを当てられますか? コンテナは1つのホストカーネルを共有するため、ホストにパッチを当てると全員が保護されます。VMはパッチターゲットを増やします。アップデートをスキップすることがわかっているなら、可動部分が少なく、デフォルトがより厳しいアーキテクチャを選んでください。

答えは集約されます。VMに焦点を当てた答えが2つ以上ある場合、テナントごとのコンテナをデフォルトにすべきではありません。コンテナに焦点を当てた答えが3つ以上ある場合、VMは時期尚早です。1つの直感に反する結果:低収益のテナントでも機密データにアクセスできる場合はVMが必要です。規制コストは支払い額とは関係ないからです。

信頼できる最小限を出荷し、その後さらに分離を獲得する

最初のアーキテクチャが最終的なものである必要はありません。実際に維持できる最も厳しい設定から始め、テナント基盤が正当化するにつれて分離を追加します。ほとんどの個人事業者にとって、それは非root、制限されたケーパビリティ、読み取り専用ファイルシステム、seccomp、ネットワークセグメンテーションを備えたテナントごとのコンテナを意味します。規制対象または高価値のテナントには、テナントごとに1つのVMに直接ジャンプし、コンテナは内部のパッケージングレイヤーとしてのみ使用します。

何を選んでも、決定を書き留めて四半期ごとに再検討してください。「このテナントをVMに移行すべきですか?」という最初の質問を受けたとき、あなたには答えがあり、それを裏付けるチェックリストがあります。それが分離の実際の意味です:購入するテクノロジーではなく、管理するトレードオフです。

起動前に、実用的なDocker分離セキュリティチェックリストを実行してください。これにより、顧客にページを表示する前に検証できるリストにこれらの決定が変わります。

Sources (5)