Dockerセキュリティのベストプラクティス|安全なコンテナ運用10のルール

kento_morota 17分で読めます

Dockerの普及に伴い、コンテナのセキュリティは開発者にとって避けて通れないテーマとなりました。「コンテナだから安全」という認識は誤りであり、適切な対策を怠れば深刻な脆弱性を抱えることになります。

この記事では、Dockerコンテナを安全に運用するための10のルールを、具体的なコード例と設定方法とともに解説します。開発環境から本番環境まで、すぐに適用できる実践的なセキュリティ対策です。

ルール1:非rootユーザーでコンテナを実行する

Dockerコンテナはデフォルトでroot権限で実行されます。これはコンテナが侵害された場合、攻撃者にroot権限を与えることを意味します。必ず専用のユーザーを作成し、そのユーザーでアプリケーションを実行しましょう。

Dockerfileでのユーザー設定

# ユーザーの作成と切り替え
FROM node:20-alpine

# アプリケーション用ユーザーを作成
RUN addgroup --system appgroup && \
    adduser --system --ingroup appgroup appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --omit=dev

# ユーザーを切り替え
USER appuser

EXPOSE 3000
CMD ["node", "index.js"]

USER命令以降のすべてのコマンドは、指定したユーザーの権限で実行されます。--chownフラグでファイルの所有権も適切に設定することが重要です。

実行時にユーザーを指定する方法

# docker runで実行時にユーザーを指定
docker run --user 1000:1000 myapp

# docker-composeで指定
services:
  app:
    image: myapp
    user: "1000:1000"

DockerfileでUSERを指定していない場合でも、実行時に--userフラグで上書きできます。ただし、Dockerfileに組み込む方が確実です。

ルール2:信頼できるベースイメージを使用する

ベースイメージの選択は、コンテナセキュリティの基盤です。不明な出所のイメージや、メンテナンスされていないイメージを使うことは、未知の脆弱性を取り込むリスクがあります。

推奨されるベースイメージ

  • 公式イメージ:Docker Hubの「Official Image」バッジ付きイメージ
  • Verified Publisher:Docker Hubで検証済みの発行元のイメージ
  • distrolessイメージ:Google提供の最小構成イメージ
  • Chainguardイメージ:セキュリティに特化した最小イメージ
# 推奨: 公式イメージを使用
FROM node:20-alpine

# 推奨: distrolessイメージを使用
FROM gcr.io/distroless/nodejs20-debian12

# 非推奨: 不明なイメージ
FROM random-user/mysterious-node-image

イメージのダイジェストを固定する

タグだけでなく、ダイジェスト(SHA256ハッシュ)でイメージを固定すると、サプライチェーン攻撃を防げます。

# タグだけでは同じタグで別の内容に差し替えられる可能性がある
FROM node:20-alpine

# ダイジェストで固定すれば、イメージの内容が保証される
FROM node:20-alpine@sha256:abc123def456...

ダイジェストはdocker inspectコマンドで確認できます。

docker inspect --format='{{index .RepoDigests 0}}' node:20-alpine

ルール3:イメージの脆弱性スキャンを実施する

コンテナイメージには、ベースイメージやインストールしたパッケージに含まれる既知の脆弱性が潜んでいる可能性があります。定期的にスキャンを行い、脆弱性を検出・修正することが重要です。

Docker Scoutによるスキャン

# Docker Scout(Docker Desktop統合済み)
docker scout cves myapp:latest

# クリティカルと高レベルの脆弱性のみ表示
docker scout cves --only-severity critical,high myapp:latest

# SBOMの生成
docker scout sbom myapp:latest

Trivyによるスキャン

# Trivyのインストール(macOS)
brew install aquasecurity/trivy/trivy

# イメージスキャン
trivy image myapp:latest

# 高以上の脆弱性のみ表示
trivy image --severity HIGH,CRITICAL myapp:latest

# CI/CDで使えるJSON出力
trivy image --format json --output report.json myapp:latest

CI/CDパイプラインへの統合

# GitHub Actionsでの脆弱性スキャン
name: Security Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

ルール4:最小限のイメージを構築する

イメージに含まれるパッケージやファイルが少ないほど、攻撃対象領域が狭くなります。マルチステージビルドと最小ベースイメージを組み合わせて、本番イメージを極限まで削ぎ落としましょう。

不要なパッケージを除外する

# 悪い例: 不要なツールがインストールされる
RUN apt-get update && apt-get install -y \
    curl \
    wget \
    vim \
    net-tools \
    python3

# 良い例: 必要最小限のパッケージのみ
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

--no-install-recommendsフラグは、推奨パッケージの自動インストールを防ぎます。また、rm -rf /var/lib/apt/lists/*でパッケージリストのキャッシュを削除し、イメージサイズを削減します。

シェルを含まないイメージを使う

# distrolessやscratchイメージにはシェルがない
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

シェルが含まれないイメージでは、攻撃者がコンテナに侵入してもshbashを実行できないため、被害の拡大を防げます。

ルール5:機密情報をイメージに含めない

APIキー、データベースパスワード、秘密鍵などの機密情報をDockerイメージに埋め込んではいけません。イメージはレジストリに保存され、チーム全体でアクセス可能なため、機密情報が漏洩するリスクがあります。

やってはいけないパターン

# 危険: 環境変数にハードコード
ENV DATABASE_URL=postgres://user:password@db:5432/mydb
ENV API_KEY=sk-1234567890abcdef

# 危険: ファイルをコピー
COPY .env /app/.env
COPY credentials.json /app/credentials.json

安全な機密情報の渡し方

# 方法1: 実行時に環境変数として渡す
docker run -e DATABASE_URL="postgres://user:pass@db/mydb" myapp

# 方法2: docker-composeでenv_fileを使う
services:
  app:
    image: myapp
    env_file:
      - .env  # .envファイルはイメージに含めない

# 方法3: Docker Secretsを使う(Docker Swarm)
echo "my-secret-password" | docker secret create db_password -
services:
  app:
    image: myapp
    secrets:
      - db_password
secrets:
  db_password:
    external: true

ビルド時に必要な機密情報

ビルド時にプライベートリポジトリへのアクセスが必要な場合は、BuildKitのシークレットマウントを使います。

# syntax=docker/dockerfile:1

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./

# シークレットをマウント(イメージレイヤーに残らない)
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc \
    npm ci

COPY . .
RUN npm run build
# ビルド時にシークレットを渡す
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

ルール6:リソースを制限する

コンテナのCPU、メモリ、PIDなどのリソースを制限することで、1つのコンテナが暴走してホスト全体に影響を与えることを防ぎます。

docker runでのリソース制限

# メモリ制限
docker run --memory=512m --memory-swap=512m myapp

# CPU制限
docker run --cpus=1.5 myapp

# PID数制限(フォーク爆弾対策)
docker run --pids-limit=100 myapp

# 読み取り専用ファイルシステム
docker run --read-only --tmpfs /tmp myapp

# 組み合わせ
docker run \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1.5 \
  --pids-limit=100 \
  --read-only \
  --tmpfs /tmp \
  myapp

docker-composeでのリソース制限

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    read_only: true
    tmpfs:
      - /tmp
    pids_limit: 100

ルール7:ネットワークを適切に分離する

Dockerのデフォルトブリッジネットワークでは、すべてのコンテナが互いに通信可能です。マイクロサービスアーキテクチャでは、サービスごとにネットワークを分離し、必要な通信だけを許可しましょう。

カスタムネットワークの設計

# docker-compose.yml
services:
  frontend:
    image: nginx
    networks:
      - frontend-net
    ports:
      - "80:80"

  api:
    image: myapi
    networks:
      - frontend-net  # フロントエンドからのアクセスを許可
      - backend-net   # データベースへのアクセスを許可

  db:
    image: postgres:16
    networks:
      - backend-net   # APIからのアクセスのみ許可
    # ポートを公開しない(外部からアクセス不可)

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge
    internal: true  # 外部への通信を遮断

この構成では、frontendからdbに直接アクセスすることはできません。apiを経由する必要があり、データベースへの不正アクセスリスクを低減します。

ルール8:Linuxケーパビリティを最小化する

Dockerコンテナにはデフォルトで複数のLinuxケーパビリティが付与されています。不要なケーパビリティを削除し、必要なものだけを追加することで、攻撃対象を限定できます。

ケーパビリティの制御

# すべてのケーパビリティを削除し、必要なものだけ追加
docker run \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  myapp
# docker-compose.yml
services:
  app:
    image: myapp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # 1024未満のポートにバインド
    security_opt:
      - no-new-privileges:true  # 特権昇格を防止

no-new-privilegesオプションは、コンテナ内のプロセスがsetuidなどで特権を昇格することを防ぎます。

ルール9:ログとモニタリングを実装する

セキュリティインシデントの早期発見と事後分析のために、適切なログ収集とモニタリングの仕組みを構築しましょう。

ログドライバーの設定

# JSON形式でログサイズを制限
docker run \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  myapp
# docker-compose.yml
services:
  app:
    image: myapp
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
        tag: "{{.Name}}/{{.ID}}"

ヘルスチェックの設定

# Dockerfile内でヘルスチェックを定義
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# docker-compose.yml
services:
  app:
    image: myapp
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 5s

ルール10:定期的にイメージを更新する

コンテナセキュリティは継続的な取り組みです。ベースイメージや依存関係を定期的に更新し、既知の脆弱性を修正しましょう。

自動更新の仕組み

# GitHub Actionsで週次の自動更新チェック
name: Weekly Security Update
on:
  schedule:
    - cron: '0 9 * * 1'  # 毎週月曜9時
  workflow_dispatch:

jobs:
  update-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build with latest base image
        run: docker build --pull --no-cache -t myapp:check .

      - name: Scan for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:check
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Create issue if vulnerabilities found
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: 'Security: Base image vulnerabilities detected',
              body: 'Weekly scan detected vulnerabilities. Please update base images.',
              labels: ['security', 'automated']
            })

Dependabotによるベースイメージ更新

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: docker
    directory: "/"
    schedule:
      interval: weekly
    reviewers:
      - "security-team"

まとめ:セキュリティチェックリスト

Dockerコンテナのセキュリティは、一度設定すれば終わりではなく、継続的に見直し改善していくプロセスです。最後に、本記事で紹介した10のルールをチェックリストとしてまとめます。

  • ルール1:非rootユーザーでコンテナを実行しているか
  • ルール2:信頼できるベースイメージを使用しているか
  • ルール3:イメージの脆弱性スキャンをCI/CDに組み込んでいるか
  • ルール4:最小限のパッケージのみ含むイメージを構築しているか
  • ルール5:機密情報をイメージに含めていないか
  • ルール6:CPU・メモリ・PIDのリソース制限を設定しているか
  • ルール7:コンテナ間のネットワークを適切に分離しているか
  • ルール8:Linuxケーパビリティを最小化しているか
  • ルール9:ログとヘルスチェックを設定しているか
  • ルール10:イメージの定期的な更新体制があるか

すべてを一度に導入する必要はありません。まずはルール1(非root実行)とルール3(脆弱性スキャン)から始め、段階的に対策を広げていくことをおすすめします。セキュリティは「やりすぎ」ということはなく、一つひとつの対策が積み重なって堅牢なコンテナ運用環境を実現します。

#Docker#セキュリティ#コンテナ
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

AI活用のヒントをお探しですか?お気軽にご相談ください。

まずは話だけ聞いてもらう