CyberFix Note
セキュアコーディング

コンテナ(Docker)セキュリティの基礎と実務で効く守り方

対象の目安: コンテナを使う開発者やインフラ運用者 / 実務

アオイ防御・運用担当
・ 約16分で読めます
コンテナ(Docker)セキュリティの基礎と実務で効く守り方

コンテナは、アプリと必要な依存をひとまとめにして、どの環境でも同じように動かせる便利な仕組みです。この手軽さの裏で、コンテナはホストのカーネルを共有しており、仮想マシンのように厚い壁で区切られているわけではありません。この記事では、コンテナが何を仮想化するのか、なぜ隔離が完全にならないのかを原理から示し、NIST SP 800-190が整理する構成要素別のリスクに沿って、開発と運用で実際に効く守り方を整理します。

先に全体像として、コンテナ技術を構成する要素と、そこに潜む主なリスク、実務で効く対策の対応を一覧にします。

構成要素主なリスク(NISTの分類)実務で効く対策
イメージ脆弱性、設定の不備、埋め込まれた平文の秘密情報、信頼できないイメージ最小の土台、脆弱性スキャン、Secretsを焼き込まない、出所の検証
レジストリ平文通信、古いイメージの残存、認証と認可の不足HTTPS通信、アクセス制御、古いタグの整理
オーケストレータ過剰な管理者権限、コンテナ間通信の分離不足最小権限、ネットワーク分離、機密度でのワークロード分離
コンテナランタイムの脆弱性、過剰なネットワーク到達、危険なランタイム設定capabilityの削減、no-new-privileges、読み取り専用、privilegedの回避
ホストOS広い攻撃面、共有カーネル、不適切なユーザー権限専用の軽量OS、カーネル更新、rootlessモード、docker操作権限の管理

以下、この対応関係を原理から順に見ていきます。

コンテナが仮想化するものと仮想マシンとの違い

仮想マシンは、ハイパーバイザーの上に仮想的なハードウェアを用意し、その中でゲストOSのカーネルを丸ごと動かします。1台のホストの上に、独立したOSがいくつも同居しているイメージです。境界はハードウェアの模倣という厚い層で、各ゲストは自前のカーネルを持ちます。

コンテナが仮想化するのは、ハードウェアではなくOSの見え方です。通常のLinuxコンテナは、ホスト(Docker Desktopの場合は内部のLinux VM)のカーネルを共有したまま、プロセスから見える世界だけを切り分けます。切り分けを担うのがLinuxカーネルのnamespaceで、プロセスID、ネットワーク、マウント、ユーザーといった資源ごとに別々の名前空間を割り当てます。あるコンテナの中からは、ほかのコンテナのプロセスもホストの全体像も見えません。使える資源の量を区切る土台になるのがcgroupで、メモリやCPU、ディスクI/Oの上限を設定できます。ただしDockerは既定ではコンテナに資源上限を設けず、ホストが許す限り使えてしまうため、--memory--cpusなどで明示して初めて、1つのコンテナの暴走がホスト全体を巻き込むのを抑えられます。

この違いから性質が決まります。コンテナはゲストOSを起動しない分だけ軽く、一般にVMより起動時のオーバーヘッドが小さく、同じホストに高密度で詰め込めます。その代わり、区切りはカーネルが提供するnamespaceとcgroupという薄い層になります。仮想マシンが「別々のOS」なら、コンテナは「同じOSの中で見える範囲を制限されたプロセス」だと捉えると、後の話がつながります。

Dockerの公式ドキュメントは、namespaceによって「あるコンテナ内のプロセスは他のコンテナのプロセスを見ることも影響を与えることもできない」こと、cgroupが各コンテナへメモリやCPU、ディスクI/Oを公平に配分することを、コンテナ隔離の土台として説明しています。

なぜコンテナの隔離は完全にならないのか

コンテナの隔離が仮想マシンほど強くならない理由は、たった1つのカーネルを全コンテナが共有しているところにあります。コンテナの中で発行されたシステムコールは、結局ホストと同じカーネルが処理します。したがって、そのカーネルに脆弱性があれば、コンテナの中から同じ脆弱性を突いてホストの権限を奪う道が理屈のうえで残ります。これがコンテナからホストへ抜ける、いわゆるコンテナエスケープです。

設定の甘さも同じ抜け道を広げます。たとえば--privilegedで起動したコンテナはほぼすべてのcapabilityとデバイスへの到達を得るため、隔離が大きく弱まり、カーネルの脆弱性を待つまでもなくホスト侵害に極めて近い状態になります。コンテナの中にホストの/var/run/docker.sockをマウントしてしまうと、その中からDockerデーモンを呼び出せるので、標準構成(rootで動くdaemon)では実質的にホスト上でroot相当で新しいコンテナを起動できます。NIST SP 800-190も、ホストOSのリスクの1つとして共有カーネル(Shared kernel)を明示し、単一のカーネルへの攻撃が同居する全コンテナに波及しうると整理しています。

仮想マシンでも脱出はゼロではありませんが、破るべき相手はハイパーバイザーという追加の仮想化境界で、別カーネルを挟む分だけ一般にコンテナの共有カーネルより強い隔離になります。コンテナはより薄い境界を大量に共有するため、隔離は「壁」ではなく「絞り込みの積み重ね」だと考えるほうが実態に合います。だからこそ、後述する権限の削減や読み取り専用といった対策を重ねて、万一エスケープの糸口ができても被害を最小にする設計が効いてきます。

注意

--privilegedでの常用と、コンテナへのdocker.sockマウントは、コンテナの壁を自ら取り払う操作です。どちらもコンテナの中からホストをroot相当で操作できる状態を作ります。Dockerデーモンを操作できるユーザーはホストのrootと同等の力を持つため、デーモンの操作権限は信頼できる担当者だけに限定してください。

NIST SP 800-190が示す構成要素別のリスク

コンテナのリスクを漠然と語ると対策の抜けが生まれます。米国NISTが2017年9月に公開したSP 800-190「Application Container Security Guide」は、リスクを技術の構成要素ごとに分けて棚卸しする枠組みを示しました。執筆時点でも有効なこの整理は、イメージ、レジストリ、オーケストレータ、コンテナ、ホストOSという5つの構成要素に沿っています。

同ガイドが挙げる代表的なリスクは次のとおりです。

  • イメージのリスクは、イメージに含まれる脆弱性、設定の不備、埋め込まれたマルウェア、埋め込まれた平文の秘密情報、信頼できないイメージの利用に整理されます。
  • レジストリのリスクは、レジストリへの安全でない接続、古いイメージが残り続けること、認証と認可の制限が不十分なことです。
  • オーケストレータのリスクは、際限のない管理者権限、認可されていないアクセス、コンテナ間ネットワークの分離不足、機密度が異なるワークロードの混在、ノードの信頼の問題です。
  • コンテナのリスクは、ランタイムソフトウェアの脆弱性、コンテナからの際限のないネットワーク到達、安全でないランタイム設定、アプリ自体の脆弱性、不正なコンテナの混入です。
  • ホストOSのリスクは、広い攻撃面、共有カーネル、ホストOS部品の脆弱性、不適切なユーザー権限、ホストのファイルシステム改ざんです。

この枠組みの利点は、対策を要素ごとに割り当てられるところにあります。オーケストレータを使わない小さな構成でも、イメージ、レジストリ、コンテナ、ホストOSの4要素はそのまま当てはまるので、まず自分の環境にどの要素があるかを並べ、要素ごとに対策の有無を確認する使い方ができます。

NIST SP 800-190は2017年9月公開のガイドで、コンテナ技術のリスクをイメージ、レジストリ、オーケストレータ、コンテナ、ホストOSの構成要素別に分類し、それぞれに対応する緩和策を示しています。

イメージとビルドを守る勘所

守りの起点はイメージです。土台となるベースイメージが大きいほど、同梱されるパッケージが増え、その分だけ既知の脆弱性を抱え込みやすくなります。Docker公式のビルドの手引きも、要件に合う最小のベースイメージを選ぶことで、イメージが小さくなり依存経由で持ち込む脆弱性が減ると述べています。ビルド用の道具と最終成果物を分けるマルチステージビルドを使えば、コンパイラやパッケージマネージャを最終イメージから追い出せます。

出所の確認も土台づくりの一部です。ベースイメージはタグではなくダイジェスト(sha256のハッシュ)で固定すると、同じタグに別物がすり替わっても同じイメージを使い続けられます。信頼できるレジストリの正規イメージを起点にし、署名の検証を運用に組み込むと、信頼できないイメージのリスクを下げられます。ビルドに不要なファイルは.dockerignoreで除外し、ビルド文脈に秘密情報や余計なソースを送り込まないようにします。

秘密情報の扱いは事故になりやすい部分です。APIキーやパスワードをイメージに焼き込むと、たとえ後のレイヤーで消しても前のレイヤーに残り、イメージを取得した相手に読み取られます。秘密情報はイメージに入れず、実行時に外部から注入する前提で設計してください。注入先はDockerやKubernetesのSecretsによるファイルや専用マウントを優先します。環境変数は手軽ですが、docker inspectやプロセス情報、診断ログなどから露出し得るため、長期の秘密情報には露出経路を評価したうえで使います。秘密情報の受け渡しは別記事で扱っています。

あわせて読みたい

シークレット管理の実務。APIキー・認証情報をハードコードせず、Vaultやマネージドサービスで守りローテーションする

さらに、イメージには既知の脆弱性が紛れ込むため、公開や配布の前にスキャンをかけます。スキャナはイメージ内のパッケージ一覧を脆弱性データベースと突き合わせ、危険なバージョンを洗い出します。これをCIに組み込み、ビルドのたびに自動で回します。ただしスキャンは検出にすぎないため、重大度や悪用可能性や修正版の有無で判断基準を決め、基準を超えたらCIを失敗させてデプロイを止めるゲートまで用意して初めて、脆弱なイメージが本番へ流れるのを防げます。何を使っているかの把握とスキャンの考え方は、依存管理の記事とあわせて読むと理解が進みます。

あわせて読みたい

依存ライブラリ管理とSCA。SBOMで攻撃面を把握する

ビルドの勘所を短くまとめると、次のようなDockerfileになります。

# 最小の土台を選び、依存だけを入れ、非rootで実行する
FROM python:3.12-slim

# 実行専用の非rootユーザーをあらかじめ用意する
RUN useradd --create-home --uid 10001 appuser

WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# ここから先はrootをやめて動かす
USER appuser
CMD ["python", "main.py"]

Docker公式も、サービスが特権なしで動くならUSERで非rootへ切り替えることを勧めています。コンテナの中のプロセスがrootのままだと、万一コンテナを乗っ取られたときに扱える範囲が広がるためです。

実行時に権限と隔離を絞り込む

Dockerはもともと、コンテナを限られたcapabilityの集合で起動し、不要な権限を落とした状態から始めます。実行時の絞り込みは、この初期状態をさらに厳しくして、乗っ取られたときに使える手札を減らす作業です。OWASPのDocker Security Cheat Sheetが挙げるルールが実務の道しるべになります。

要点は次のとおりです。まず--cap-drop=ALLですべてのcapabilityを外し、そのアプリに本当に必要なものだけを--cap-addで足します。--security-opt=no-new-privilegesを付けると、コンテナ内でsetuidを使った権限の再取得を封じられます。ファイルシステムは--read-onlyで読み取り専用にし、書き込みが必要な場所だけをtmpfsやボリュームに逃がすと、改ざんの余地が縮みます。プロセスは--userで非rootに固定し、--privilegedは使わないのが原則です。メモリやCPUに上限を設けると、暴走したコンテナがホストの資源を食い尽くす事態も避けられます。

# 権限を絞って起動する例。まず全capabilityを外し、必要なものだけ足す
docker run \
  --user 10001:10001 \
  --cap-drop=ALL \
  --security-opt=no-new-privileges \
  --read-only \
  --tmpfs /tmp \
  myapp:1.4.2

これらは一度に全部を入れる必要はありません。まず非root化と--cap-drop=ALLno-new-privilegesから始め、アプリが動くことを確かめながら読み取り専用や資源上限を足していくと、運用を止めずに堅くできます。

OWASPのDocker Security Cheat Sheetは、非rootユーザーでの実行、--cap-drop allと必要な権限だけの付与、--security-opt=no-new-privileges、ファイルシステムの読み取り専用化、--privilegedの回避などを実行時の推奨として挙げています。

ホストとレジストリの堅牢化

コンテナの土台はホストです。カーネルを共有する以上、ホストのカーネルとDocker Engineを最新に保つことが、コンテナエスケープの糸口をふさぐ最初の一手になります。ホストOS自体は、コンテナ実行に必要な機能だけを持つ軽量な構成にして攻撃面を狭め、余計なサービスは動かさないようにします。Dockerをrootlessモードで動かせば、デーモンとコンテナを非rootのユーザー権限で実行でき、デーモンが侵害された際に直ちにホストのroot権限を握られるリスクを下げられます(共有カーネルの脆弱性や別経路の権限昇格まで消えるわけではありません)。

Dockerデーモンを操作できることはホストのrootと同等の力を意味するため、その操作権限は信頼できる担当者に限定します。コンテナにdocker.sockを渡さないこと、Linuxのセキュリティ機構を有効にしておくことも、被害の範囲を狭める効き目があります。Dockerには既定のseccompプロファイルがあり、対応ホストではAppArmorのdocker-defaultが使われます。SELinuxは有効化やラベル付けがディストリビューションやデーモン設定に依存するため、利用できる機構が実際に適用されているかを確認します。設定項目が多く迷いやすい領域なので、CIS Docker Benchmarkのような点検基準を使い、ホストとデーモンの設定を一つずつ確認していく進め方が現実的です。ホスト単体の堅牢化の考え方は、サーバー堅牢化の記事も参考になります。

あわせて読みたい

サーバーのハードニング基礎。最小化・最小権限・更新・設定堅牢化をCISベンチマークから学ぶ

レジストリ側では、イメージの送受信を必ずHTTPSにして平文通信を避け、誰がどのイメージを取得できるかをアクセス制御で絞ります。古いイメージは脆弱なまま取得され続ける温床になるため、使わなくなったタグは定期的に整理します。可能なら信頼できるプライベートレジストリを起点にし、公開レジストリのイメージをそのまま本番へ流さない運用にすると、信頼できないイメージのリスクを抑えられます。

まとめ

コンテナのセキュリティは、コンテナがホストのカーネルを共有する薄い境界であるという原理から出発すると筋が通ります。隔離は完全な壁ではないので、イメージを小さく正しく作り、非rootと権限の絞り込みで実行時の手札を減らし、ホストとレジストリを堅くする対策を重ねて、万一の抜け道ができても被害を最小にする設計を目指します。NIST SP 800-190の構成要素別の枠組みは、自分の環境で何を守り忘れているかを点検する地図として使えます。完璧な防御はないという前提に立ち、各ツールやガイドの最新の公式情報を確認しながら、優先度の高いところから手を付けてください。

コンテナセキュリティ点検リスト

  • ベースイメージは要件に合う最小の土台を選び、ダイジェストで固定しているか
  • マルチステージビルドでビルド用の道具を最終イメージから外しているか
  • 秘密情報をイメージに焼き込まず、実行時に注入する設計になっているか
  • イメージの脆弱性スキャンをCIに組み込み、ビルドのたびに回しているか
  • コンテナを非rootで実行し、USERで専用ユーザーに切り替えているか
  • --cap-drop=ALLで権限を外し、必要なcapabilityだけ足しているか
  • no-new-privilegesと読み取り専用を有効にし、--privilegedを避けているか
  • docker.sockをコンテナに渡さず、デーモンの操作権限を信頼できる担当者に限定しているか
  • ホストのカーネルとDocker Engineを最新に保ち、rootlessやLSMの既定プロファイルを使っているか
  • レジストリはHTTPSとアクセス制御で守り、古いイメージを整理しているか

出典・参考

この記事をシェア

関連する記事