Docker 入門

Dockerイメージセキュリティ

強固なコンテナセキュリティを構築する根本的な原則は、ランタイム(実行時)から始まるのではなく、Dockerイメージが考案され、ビルドされた瞬間からすでに決定されています。安全でないDockerイメージは、地盤の緩い建物のようなものであり、後続のすべてのセキュリティ対策を台無しにし、アプリケーションを極めて攻撃されやすい状態にしてしまいます。

本章では、DockerイメージとDockerfileを堅牢化(ハードニング)するための重要なプラクティスと注意事項を深く掘り下げ、コンテナ化アプリケーションの安全なベースラインを確立します。イメージのビルドプロセスにおいて、綿密な設計上の選択を通じてアタックサーフェス(攻撃対象領域)を大幅に削減し、最小権限の原則を徹底し、センシティブなデータの偶発的な漏洩を防ぐことで、よりセキュアなDocker環境を構築するための土台を整える方法を一緒に探求していきましょう。

1. セキュアなイメージ設計の原則

Dockerイメージのセキュリティ保護は反復的なプロセスであり、その中核はボトムアップで潜在的なアタックサーフェスを最小化し、セキュリティのベストプラクティスに従うことにあります。これらの原則をDockerfileとビルドパイプラインに直接組み込むことで、アプリケーションによりレジリエンス(回復力)のある基盤を構築できます。

1.1 アタックサーフェスの最小化

イメージに含まれるコンポーネント、バイナリファイル、依存ライブラリが少ないほど、脆弱性が存在したり悪用されたりする機会が減ります。追加のパッケージ、ファイル、インストラクションの1つ1つが、潜在的なアタックサーフェスを暗黙のうちに増加させます。

1.2 スリムなベースイメージの使用

選択するベースイメージは、イメージのセキュリティを決定する最も重要な要因と言えます。一般的に、小規模なベースイメージはプリインストールされているパッケージやユーティリティが少ないため、アタックサーフェスが小さいことを意味します。

  • 公式イメージ (Official Images): 常にDocker Hubの公式イメージを優先して選択してください。これらのイメージはベンダーまたはコミュニティによってメンテナンスされており、頻繁に脆弱性スキャンが行われ、広くベストプラクティスに従っています。
  • Alpine Linux: alpine は極めてサイズが小さいため、非常に人気のあるベースイメージの選択肢です。musl libcとBusyBoxを使用しており、生成されるイメージは通常わずか数メガバイトです。
    • 実際の事例 1: 大手クラウドサービスプロバイダーは、様々な実行環境(Python, Node.js)向けに、スリムなAlpineイメージベースのサーバーレス(Serverless)関数を提供しています。基盤となる環境が極限までスリム化されているため、顧客がアプリケーションをデプロイする際のコールドスタート時間が大幅に短縮され、セキュリティフットプリントも小さくなります。
  • Distrolessイメージ: Googleによって開発されたDistrolessイメージには、アプリケーションとそのランタイムの依存関係のみが含まれます。パッケージマネージャー、シェルコンソール、その他の標準的なオペレーティングシステムコンポーネントは含まれていません。これらの一般的な攻撃ベクターを排除することで、セキュリティが劇的に向上します。
    • 実際の事例 2: センシティブな金融取引を扱うある機関は、決済データを処理するマイクロサービスをデプロイしました。最高レベルのイメージセキュリティを確保するため、Javaアプリケーションに gcr.io/distroless/java ベースイメージを使用しました。このイメージは lsbash コマンドすら存在しないほどスリム化されており、万が一脆弱性が悪用された場合でも、攻撃者がコンテナを探索したり操作したりする能力を極めて限定的にします。
  • アプリケーション固有の slim または onbuild イメージ: 多くの公式イメージでは、slim または onbuild バリアントが提供されています。例えば、python:3.9-slim-buster はDebianベースのPythonランタイム環境を提供しますが、完全なDebianイメージにある不要なドキュメント、開発ツール、システムユーティリティは削除されています。

1.3 不要なパッケージとファイルの削除

イメージのビルドプロセス中、アプリケーションの実行に絶対的に必要ではないパッケージのインストールは避けてください。特定のパッケージがビルドプロセスでのみ必要な場合(コンパイラなど)は、最終的なイメージの一部にならないように、同じ RUN レイヤーで確実に削除するか、マルチステージビルド(Multi-stage Build)を活用してください。

  • 想定シナリオ: 「EcoWebApp」チームはPython Webアプリケーションを開発しました。利便性を優先し、最初のDockerfileでは1つの RUN コマンドで gitmakegcc および多くのPython開発ライブラリをインストールしていました。その結果、イメージサイズは800MBに達しました。セキュリティ監査後、アプリケーションのランタイムにはこれらのツールが全く不要であることが判明しました。Dockerfileをリファクタリングし、ランタイムの依存関係のみをインストールし、一時ファイルを確実にクリーンアップすることで、イメージサイズは150MBに縮小され、同時に数百の潜在的な脆弱性が排除されました。

1.4 .dockerignore の活用

.dockerignore ファイルは .gitignore と似た役割を果たし、指定したファイルやディレクトリがビルドコンテキストにコピーされるのを防ぎます。これは、センシティブなファイル、開発用アーティファクト、または巨大で不要なディレクトリがイメージレイヤーに含まれるのを防ぐために極めて重要です。

.dockerignore に含まれる一般的な項目の例:

.git/
.vscode/
node_modules/ (コンテナ内でインストールし、プリコンパイル済みのものをコピーしない場合)
*.log
*.env (またはセンシティブなシークレットを含むその他の設定ファイル)
tmp/
dist/ (最終的なアーティファクトではない場合)
README.md

2. 最小権限の原則の徹底

コンテナは、その機能を実行するために必要な最小限の権限で実行されるべきです。コンテナ内でアプリケーションを root ユーザーとして実行することは一般的なアンチパターン(Anti-pattern)であり、システムが侵害された際の影響を著しく増大させます。

2.1 非Rootユーザーでの実行

デフォルトでは、Dockerコンテナ内のプロセスは root として実行されます。Dockerfileの USER インストラクションを使用すると、以降のコマンドを実行するユーザーを指定できます。ベストプラクティスは、専用の非rootユーザーを作成し、アプリケーションを実行する前にそのユーザーに切り替えることです。

  • 実際の事例 1: Nginx WebサーバーのDockerイメージは通常、nginx または www-data ユーザーとして nginx プロセスを実行します。攻撃者がNginxの脆弱性を悪用したとしても、侵害されたプロセスのシステム権限は制限されているため、攻撃者が重要なシステムファイルを容易に変更したり、権限を昇格してエスケープしたりするのを防ぐことができます。
  • 実際の事例 2: コンテナ内で実行されるJenkins CI/CDエージェントは、ビルドタスクを実行する必要があります。このエージェントのDockerfileでは、一意のUIDとGIDを持つ特定の jenkins ユーザーを作成しています。USER jenkins インストラクションにより、すべてのビルドステップがこの低権限ユーザーの下で実行されることが保証され、悪意のあるビルドスクリプトがシステムレベルの操作を実行しようとした際のブラストラジアス(影響範囲)を縮小できます。

2.2 適切なファイルパーミッションの設定

非rootユーザーポリシーと厳格なファイルパーミッションを組み合わせてください。アプリケーションファイルが非rootユーザーに所有されていることを確認し、書き込みアクセスが必要なディレクトリには合理的なパーミッションを設定します。通常、誰でも書き込み可能(world-writable)にするべきではありません。

  • RUN chown -R <user>:<group> /app を使用して、アプリケーションディレクトリの所有権を変更します。
  • RUN chmod -R go-w /app を使用して、全体書き込み権限(world-writable)が不要な場所からその権限を削除します。

3. 信頼性とイミュータビリティ(不変性)

イメージのインテグリティ(完全性)と出所を保証し、センシティブなデータの偶発的な漏洩を防ぐことは、セキュアなソフトウェアサプライチェーンにとって不可欠です。

3.1 公式および信頼できるベースイメージの選択

ベースイメージのビルドは、常に信頼できるソース、主にDocker Hubの公式イメージから開始してください。プライベートイメージレジストリを使用する場合は、誰がイメージを公開できるかについて厳格なアクセス制御を確保し、イメージのスキャンと署名を実施してください。

3.2 イメージバージョンの固定 (Pinning)

本番環境(プロダクション)のイメージでは、絶対に latest タグを使用しないでください。latest タグはミュータブル(可変)であり、時間の経過とともに変更されるため、ビルドの再現性が失われます。基礎となるイメージに破壊的変更(Breaking changes)や新たな脆弱性が更新された場合、予期せぬ動作やセキュリティリスクをもたらす原因にもなります。

FROM node:latest の代わりに、FROM node:16.14.2-alpineFROM node:lts-alpine を使用してください。これにより、ビルドの決定性が保証され、依存関係をいつアップデートするかをコントロールできるようになります。

  • 実際の事例: ある企業では、バックエンドが特定バージョンのJavaアプリケーションサーバーに依存していました。イメージを tomcat:9.0.58-jdk11-openjdk-slim-buster に固定することで、本番環境へのデプロイが常にこの正確でテスト済みのバージョンを使用することを保証しました。これにより、自動的なベースイメージの更新によって引き起こされる可能性のある互換性の問題や、テスト環境には存在しなかった脆弱性の混入を防ぎました。

3.3 イメージへのセンシティブなデータのハードコーディング回避

シークレット情報(APIキー、データベースのクレデンシャル、秘密鍵など)をDockerイメージに直接埋め込むことは絶対に避けてください。一度イメージがビルドされると、そこに含まれるすべてのコンテンツはレイヤーの一部となり、完全な削除が困難になります。その結果、イメージにアクセスできるすべての人に情報が露出する可能性があります。

シークレット情報は、実行時(ランタイム)に環境変数、Docker Secrets(Swarmクラスター用)、Kubernetes Secrets、または外部のシークレット管理ツール(HashiCorp Vaultなど)を通じてコンテナに渡す必要があります。

  • 想定シナリオ: ある開発チームが誤ってAWSアクセスキーを .env ファイルに含めてしまい、そのファイルがDockerイメージにコピーされました。その後、このイメージは公開されているDocker Hubレジストリにプッシュされました。攻撃者がこのイメージを発見してAWSキーを抽出し、それを利用して悪意のあるEC2インスタンスをデプロイした結果、巨額の金銭的損失とセンシティブなデータへのアクセス被害が発生しました。

4. ビルドプロセスのセキュリティ

Dockerfileのインストラクションの記述方法もセキュリティに影響を与えます。これは最終的なイメージに何が含まれるかだけの問題ではありません。

4.1 ADDの代わりにCOPYを使用する

COPY と比較して、ADD インストラクションには追加の機能があります。ファイルを自動的に解凍(アーカイブ展開)したり、URLからファイルを取得したりできます。便利ではありますが、これは複雑さと潜在的な攻撃ベクターを増加させます。

  • URLから ADD を実行すると、信頼できないコンテンツが混入する可能性があります。
  • 圧縮ファイルが悪意のあるものであった場合、自動展開によって「Zip爆弾(Zip bomb)」攻撃やパストラバーサル(Path traversal)問題を引き起こす可能性があります。

ベストプラクティス: ローカルファイルには COPY を使用してください。リモートファイルを取得する必要がある場合は、RUN コマンド内で curl または wget を使用し、必要に応じて明示的なセキュリティ検証を実行してください。

4.2 Dockerfileインストラクションの戦略的な順序配置

Dockerのレイヤーキャッシュ(Layer caching)メカニズムを有効に活用してください。システム依存のインストールなど、頻繁に変更されないインストラクションはDockerfileの前方に配置します。後続のレイヤーの変更は、それ以降のレイヤーのキャッシュを無効にするだけなので、ビルド速度が向上します。セキュリティの観点から見ると、これは以下のことを意味します:

  • 最初に安定したベースイメージを配置: FROM インストラクション。
  • 次にシステム依存関係: RUN apt-get update && apt-get install -y ...
  • アプリケーションの依存関係: COPY requirements.txt . の後に RUN pip install -r requirements.txt
  • 最後にアプリケーションコード: COPY . .

これにより、アプリケーションの特定の依存関係に脆弱性が見つかりリビルドが必要になった場合でも、最初のより安定したレイヤー(ベースOS、コアシステムツール)はキャッシュされたまま手付かずの状態を保てる可能性が高く、これらのレイヤーから不用意に新しい脆弱性を持ち込む確率を下げることができます。

5. 実践デモンストレーション:セキュアなイメージのビルド

これらの原則を説明するために、シンプルなPython Webアプリケーションを使用してみましょう。

5.1 安全でないDockerfileの例

以下は、基本的なFlaskアプリケーション向けの安全でないDockerfileの例です:

# 安全でない Dockerfile
FROM python:latest

# システム依存関係のインストール(不要なビルドツールが含まれている)
RUN apt-get update && \
    apt-get install -y build-essential curl git && \
    rm -rf /var/lib/apt/lists/*

# 作業ディレクトリの設定
WORKDIR /app

# アプリケーションコードのコピー(センシティブなファイルが一緒に含まれる可能性あり)
ADD . /app

# Python依存関係のインストール
RUN pip install flask gunicorn

# ポートの公開
EXPOSE 8000

# rootユーザーとして実行
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

シンプルな app.py があると仮定します:

# app.py
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello, Docker! Your request was from {request.remote_addr}\n"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

同じディレクトリに secrets.env ファイルが存在する可能性があります:

DB_PASSWORD=mysecretpassword
API_KEY=anothersecretkey

このイメージをビルドし (docker build -t insecure-app .)、実行する (docker run -p 8000:8000 insecure-app) と、アプリケーションは起動するかもしれませんが、いくつかの深刻なセキュリティ上の欠陥が存在します:

  • FROM python:latest: ミュータブル(可変)な latest タグを使用しています。
  • build-essential curl git: ランタイムには不要な開発ツールやユーティリティがインストールされており、イメージサイズとアタックサーフェスを大幅に増加させています。
  • ADD . /app: secrets.env のようなセンシティブなファイルがイメージにコピーされる危険性が極めて高いです。
  • root での実行: デフォルトでは、アプリケーションは昇格された特権で実行されます。

最後の点を実証するために、実行中のコンテナ内に入ると (docker exec -it <container_id> bash)、自身が root であることがわかります。

docker build -t insecure-app .
# ... 出力情報 ...
docker run -d -p 8000:8000 insecure-app
# ... コンテナ ID の出力 ...
docker exec -it <container_id> bash
root@<container_id>:/app# whoami
root
root@<container_id>:/app# ls -la secrets.env # もし secrets.env が存在する場合
-rw-r--r-- 1 root root 38 Feb 1 10:00 secrets.env
root@<container_id>:/app# exit

5.2 セキュアなDockerfileの例

ベストプラクティスを適用して、前の例をリファクタリングしてみましょう。

まず、同じディレクトリに .dockerignore ファイルを作成します:

# .dockerignore
.git
__pycache__
*.pyc
*.env
*.log

そして、これが改善されたDockerfileです:

# Flaskアプリケーション用のセキュアなDockerfile

# --- ビルドステージ ---
FROM python:3.9.10-slim-buster AS build_deps

# Pythonパッケージのインストールに必要なビルド依存関係
# このレイヤーは `build_deps` を利用してマルチステージビルドの利点を実現しています。
# マルチステージビルドについては後続の章で詳しく解説しますが、
# 現時点では、「依存関係の最小化」という概念にのみ注目してください。
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libpq-dev && \
    rm -rf /var/lib/apt/lists/*

# 依存関係インストールのための作業ディレクトリを設定
WORKDIR /app

# 依存関係のインストール用にrequirementsファイルのみをコピー
COPY requirements.txt .

# Python依存関係のインストール
RUN pip install --no-cache-dir -r requirements.txt

# --- 最終イメージステージ ---
FROM python:3.9.10-slim-buster

# 非rootユーザーとユーザーグループの作成
# 一貫性とセキュリティを向上させるためにUIDとGIDを定義
ARG APP_USER=appuser
ARG APP_UID=1000
ARG APP_GID=1000

RUN groupadd -r -g ${APP_GID} ${APP_USER} && \
    useradd -r -g ${APP_USER} -u ${APP_UID} -s /sbin/nologin -c "Application User" ${APP_USER}

# 作業ディレクトリの設定
WORKDIR /app

# アプリケーションファイルのコピー(.dockerignoreによるフィルタリング後)
# ADDの代わりにCOPYを使用
COPY app.py .
COPY --from=build_deps /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages

# アプリケーションディレクトリの所有権とパーミッションの設定
# appuserが適切な権限を持っていることを確認
RUN chown -R ${APP_USER}:${APP_USER} /app && \
    chmod -R 755 /app && \
    chmod 644 /app/app.py

# 非rootユーザーへの切り替え
USER ${APP_USER}

# ポートの公開(実際のネットワーク開放ではなく、メタデータとして)
EXPOSE 8000

# アプリケーションを実行するコマンド
# センシティブな情報用の環境変数はイメージ内ではなく、実行時(ランタイム)に渡す必要があります。
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

そして requirements.txt

Flask==2.0.2
gunicorn==20.1.0

このセキュアなイメージをビルドしてみましょう:

docker build -t secure-app .
# ... 出力情報 ...
docker run -d -p 8000:8000 secure-app
# ... コンテナ ID の出力 ...
docker exec -it <container_id> bash
# ターミナルのプロンプトにrootが表示されないか、nologinが設定されている場合は権限拒否(Permission denied)のメッセージが表示される可能性があります。
# もしシェルを取得できた場合は、以下を入力してみてください:
whoami
# appuser
ls -la secrets.env # .dockerignore が存在するため、このファイルはイメージ内に存在しないはずです
# ls: cannot access 'secrets.env': No such file or directory
exit

重要な改善点と原理の解説:

  • FROM python:3.9.10-slim-buster: 特定の、イミュータブル(不変)で極めて最小限のベースイメージバージョンを使用しています。これはDebian (Buster) の slim バリアントであり、完全なDebianイメージよりもはるかに軽量です。
  • マルチステージビルドの概念 (FROM ... AS build_deps): 一時的なステージでビルド時の依存関係をインストールし、アーティファクト(この例ではインストール済みのPythonパッケージ)のみを最終ステージにコピーすることで、不要なツールを削除する原則を実証しています。最終イメージには gcclibpq-dev は含まれていません。
  • build-essential なし: Pythonパッケージのコンパイルに必要なビルド時パッケージ(gcc, libpq-dev)のみをインストールし、これらは最終イメージから除外されます。
  • COPY app.py . の前に COPY requirements.txt .: Dockerキャッシュを活用しています。requirements.txt が変更されない場合、pip install レイヤーはキャッシュされるため、app.py のみを変更した際のリビルド速度が向上します。
  • pip install --no-cache-dir: pipがキャッシュファイルを保存するのを防ぎ、イメージサイズをさらに縮小します。
  • 非Rootユーザー (APP_USER, USER appuser): 専用の非rootユーザー appuser を作成し、アプリケーションをそのユーザー下で実行しています。これにより、潜在的なセキュリティインシデントによる被害(ブラストラジアス)を大幅に軽減できます。
  • chownchmod: 適切なパーミッションにより、アプリケーションユーザーは必要なファイルの所有権のみを持ち、不必要な全体の書き込み権限(World-writable)が存在しないことを保証します。
  • .dockerignore: センシティブなファイル(.env)や開発用アーティファクトがイメージにコピーされるのを防ぎます。
  • COPY vs ADD: ローカルファイルに対して明確に COPY を使用しています。
  • ランタイムのシークレット: センシティブなデータは埋め込むのではなく、実行時(ランタイム)に渡されなければならないことを明確に規定しています。