第6章:セキュリティとベストプラクティス

はじめに

コンテナ環境のセキュリティは、従来の仮想マシンとは異なるアプローチが必要です。本章では、Inceptionプロジェクトで適用すべきセキュリティ対策とベストプラクティスを学びます。

---

1. コンテナセキュリティの基本

1.1 脅威モデル

コンテナ環境の脅威:

1. コンテナエスケープ
   - カーネル脆弱性を利用
   - ホストシステムへの侵入

2. 不正なイメージ
   - マルウェアを含むイメージ
   - 古い脆弱なパッケージ

3. ネットワーク攻撃
   - コンテナ間の不正アクセス
   - 外部からの攻撃

4. 設定ミス
   - 過剰な権限
   - シークレットの露出

5. サプライチェーン攻撃
   - ベースイメージの改ざん
   - ビルドパイプラインの侵害

1.2 防御の多層化

+------------------+
|   Application    | ← 入力検証、認証
+------------------+
|    Container     | ← 非root、read-only
+------------------+
|     Runtime      | ← seccomp、AppArmor
+------------------+
|   Host Kernel    | ← カーネル更新
+------------------+
|   Host System    | ← アクセス制御
+------------------+

原則:
- 最小権限の原則
- 防御の多層化
- 定期的な更新
- 監視とログ

---

2. イメージセキュリティ

2.1 ベースイメージの選択

# 良い例: 最小限のベースイメージ
FROM alpine:3.18

# 悪い例: フル機能のイメージ
FROM ubuntu:latest

# 比較
Alpine:
+ サイズ: 約5MB
+ 攻撃対象面が小さい
+ セキュリティフォーカス
- 一部互換性問題

Debian slim:
+ 互換性が高い
+ 約30MB
+ 安定性

Distroless:
+ 最小限(シェルなし)
+ 攻撃対象面が最小
- デバッグが困難

2.2 パッケージ管理

# 良い例: バージョン固定、キャッシュ削除
FROM alpine:3.18

RUN apk update && \
    apk add --no-cache \
    nginx=1.24.0-r6 \
    openssl=3.1.2-r0 && \
    rm -rf /var/cache/apk/*

# 悪い例: バージョン未指定
FROM alpine
RUN apk add nginx

2.3 イメージスキャン

# Trivyによるスキャン
trivy image nginx:alpine

# 出力例
nginx:alpine (alpine 3.18.3)
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 1, CRITICAL: 0)

┌──────────────┬───────────────┬──────────┬────────────────────────────┐
│   Library    │ Vulnerability │ Severity │       Fixed Version        │
├──────────────┼───────────────┼──────────┼────────────────────────────┤
│ libcrypto3   │ CVE-2023-XXXX │ HIGH     │ 3.1.2-r1                   │
│ libssl3      │ CVE-2023-YYYY │ MEDIUM   │ 3.1.2-r1                   │
└──────────────┴───────────────┴──────────┴────────────────────────────┘

# CI/CDでの使用
trivy image --exit-code 1 --severity HIGH,CRITICAL myimage:latest

2.4 マルチステージビルド

# ビルドステージ(開発ツール含む)
FROM alpine:3.18 AS builder
RUN apk add --no-cache curl wget make gcc
WORKDIR /build
COPY . .
RUN make

# 実行ステージ(最小限)
FROM alpine:3.18
RUN apk add --no-cache libc6-compat
COPY --from=builder /build/app /app
USER nobody
CMD ["/app"]

# 効果:
# - ビルドツールが最終イメージに含まれない
# - 攻撃対象面の削減
# - イメージサイズの縮小

---

3. 実行時セキュリティ

3.1 非rootユーザー

FROM alpine:3.18

# ユーザー作成
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# 必要なディレクトリのパーミッション
RUN mkdir -p /app/data && \
    chown -R appuser:appgroup /app

# ユーザー切り替え
USER appuser

WORKDIR /app
CMD ["./myapp"]

# docker-compose.yml での指定
services:
  wordpress:
    user: "1000:1000"
    # または
    user: nobody:nogroup

3.2 Read-onlyファイルシステム

services:
  nginx:
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
      - /var/cache/nginx
    volumes:
      - wordpress_data:/var/www/html:ro  # 読み取り専用

# Dockerfile での対応
FROM alpine:3.18

# 書き込みが必要なディレクトリを作成
RUN mkdir -p /tmp /var/run /var/cache/nginx && \
    chmod 1777 /tmp

# read-onlyで起動可能に

3.3 Capabilities制限

services:
  nginx:
    cap_drop:
      - ALL              # すべてのcapabilityを削除
    cap_add:
      - NET_BIND_SERVICE # 1024以下のポートにバインド
      - CHOWN            # 必要に応じて

# 必要なcapabilityの確認
docker run --rm -it --cap-drop ALL nginx
# エラーが出たら必要なcapabilityを追加

# 一般的なサービスに必要なcapability
Nginx:  NET_BIND_SERVICE, CHOWN, SETGID, SETUID
PHP-FPM: CHOWN, SETGID, SETUID
MariaDB: CHOWN, SETGID, SETUID, DAC_OVERRIDE

3.4 Seccompプロファイル

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "close", "stat", "fstat",
        "mmap", "mprotect", "munmap", "brk",
        "accept", "bind", "listen", "socket",
        "epoll_create", "epoll_wait", "epoll_ctl"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

services:
  nginx:
    security_opt:
      - seccomp:./nginx-seccomp.json

---

4. シークレット管理

4.1 環境変数の問題

# 悪い例: docker-compose.ymlにハードコード
services:
  mariadb:
    environment:
      - MYSQL_ROOT_PASSWORD=secretpassword123  # NG!

# 良い例: .envファイルから読み込み
services:
  mariadb:
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}

4.2 .envファイルの保護

# .env ファイル
MYSQL_ROOT_PASSWORD=your_secure_password
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
MYSQL_PASSWORD=wp_secure_password

# パーミッション設定
chmod 600 .env

# .gitignore に追加
echo ".env" >> .gitignore
echo "*.env" >> .gitignore

4.3 Docker Secrets(Swarmモード)

# docker-compose.yml
version: "3.8"

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password.txt
  mysql_password:
    file: ./secrets/mysql_password.txt

services:
  mariadb:
    secrets:
      - mysql_root_password
      - mysql_password
    environment:
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password
      - MYSQL_PASSWORD_FILE=/run/secrets/mysql_password

4.4 Inceptionでのシークレット管理

# ディレクトリ構造
srcs/
├── .env                    # 環境変数(gitignore)
├── .env.example            # テンプレート(コミット可)
└── secrets/                # シークレットファイル
    └── .gitkeep

# .env.example
DOMAIN_NAME=login.42.fr
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
# パスワードは空欄
MYSQL_PASSWORD=
MYSQL_ROOT_PASSWORD=
WP_ADMIN_USER=admin
WP_ADMIN_PASSWORD=
WP_ADMIN_EMAIL=admin@42.fr

# 初回セットアップスクリプト
#!/bin/bash
# setup_secrets.sh

if [ ! -f .env ]; then
    cp .env.example .env

    # ランダムパスワード生成
    MYSQL_PASSWORD=$(openssl rand -base64 24)
    MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24)
    WP_ADMIN_PASSWORD=$(openssl rand -base64 24)

    sed -i "s/^MYSQL_PASSWORD=.*/MYSQL_PASSWORD=${MYSQL_PASSWORD}/" .env
    sed -i "s/^MYSQL_ROOT_PASSWORD=.*/MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}/" .env
    sed -i "s/^WP_ADMIN_PASSWORD=.*/WP_ADMIN_PASSWORD=${WP_ADMIN_PASSWORD}/" .env

    chmod 600 .env
    echo "Secrets generated in .env"
fi

---

5. ネットワークセキュリティ

5.1 ネットワーク分離

networks:
  frontend:
    driver: bridge
    name: inception_frontend
  backend:
    driver: bridge
    name: inception_backend
    internal: true  # 外部アクセス不可

services:
  nginx:
    networks:
      - frontend
      - backend
    ports:
      - "443:443"

  wordpress:
    networks:
      - backend  # frontendには接続しない

  mariadb:
    networks:
      - backend  # 内部ネットワークのみ

5.2 ポート制限

services:
  nginx:
    ports:
      - "443:443"        # HTTPS のみ
      # - "80:80"        # HTTP 禁止(Inception要件)

  wordpress:
    # ports なし(内部通信のみ)
    expose:
      - "9000"           # コンテナ間のみ

  mariadb:
    # ports なし(外部公開しない)
    expose:
      - "3306"

5.3 TLS設定の強化

# nginx.conf

ssl_protocols TLSv1.2 TLSv1.3;  # 古いプロトコル無効化
ssl_prefer_server_ciphers on;

# 強力な暗号スイートのみ
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;

# HSTS(本番環境向け)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# セキュリティヘッダー
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self';" always;

5.4 ファイアウォール設定

# UFW(Ubuntu Firewall)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 443/tcp
sudo ufw enable

# Docker との連携
# /etc/docker/daemon.json
{
  "iptables": true
}

# Docker が iptables を管理
# 明示的なポート公開のみ許可

---

6. ログとモニタリング

6.1 ログドライバ設定

services:
  nginx:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        labels: "service"
        tag: "{{.Name}}"

6.2 ログの集約

# 本番環境向け
services:
  nginx:
    logging:
      driver: syslog
      options:
        syslog-address: "udp://logserver:514"
        tag: "nginx"

6.3 ヘルスチェック

services:
  nginx:
    healthcheck:
      test: ["CMD", "curl", "-f", "https://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  mariadb:
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s

6.4 リソース監視

# コンテナリソース使用状況
docker stats

# 出力例
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT   NET I/O       BLOCK I/O
a1b2c3d4e5f6   nginx       0.10%     10MiB / 100MiB      1.5kB / 1kB   0B / 0B
b2c3d4e5f6a1   wordpress   0.50%     50MiB / 256MiB      2kB / 1.5kB   0B / 0B
c3d4e5f6a1b2   mariadb     1.20%     100MiB / 512MiB     1kB / 2kB     0B / 4kB

---

7. リソース制限

7.1 メモリ制限

services:
  nginx:
    deploy:
      resources:
        limits:
          memory: 128M
        reservations:
          memory: 64M

  wordpress:
    deploy:
      resources:
        limits:
          memory: 256M
        reservations:
          memory: 128M

  mariadb:
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

7.2 CPU制限

services:
  nginx:
    deploy:
      resources:
        limits:
          cpus: '0.5'
        reservations:
          cpus: '0.25'

7.3 プロセス数制限

services:
  wordpress:
    ulimits:
      nproc: 100
      nofile:
        soft: 20000
        hard: 40000

---

8. Inceptionセキュリティチェックリスト

8.1 イメージ

□ ベースイメージにタグを指定(:latest禁止)
□ 不要なパッケージを削除
□ キャッシュをクリア
□ 脆弱性スキャンを実行
□ マルチステージビルドを検討

8.2 実行時

□ 非rootユーザーで実行
□ 不要なcapabilityを削除
□ read-onlyファイルシステムを検討
□ tmpfsで一時ファイル
□ リソース制限を設定

8.3 ネットワーク

□ 443ポートのみ公開
□ TLSv1.2/1.3のみ許可
□ 内部ネットワークを分離
□ network: host 未使用
□ --link 未使用

8.4 シークレット

□ .envファイルのパーミッション(600)
□ .gitignoreに.envを追加
□ 強力なパスワードを使用
□ パスワードをログに出力しない

8.5 監視

□ ヘルスチェックを設定
□ ログローテーションを設定
□ リソース使用状況を監視

---

9. セキュリティ強化版docker-compose.yml

services:
  nginx:
    build:
      context: ./requirements/nginx
      dockerfile: Dockerfile
    container_name: nginx
    ports:
      - "443:443"
    volumes:
      - wordpress_data:/var/www/html:ro
    networks:
      - inception
    depends_on:
      wordpress:
        condition: service_healthy
    restart: always
    # セキュリティ強化
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
      - /var/cache/nginx
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
      - CHOWN
      - SETGID
      - SETUID
    deploy:
      resources:
        limits:
          memory: 128M
          cpus: '0.5'
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "https://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  wordpress:
    build:
      context: ./requirements/wordpress
      dockerfile: Dockerfile
    container_name: wordpress
    volumes:
      - wordpress_data:/var/www/html
    networks:
      - inception
    depends_on:
      mariadb:
        condition: service_healthy
    environment:
      - DOMAIN_NAME=${DOMAIN_NAME}
      - WORDPRESS_DB_HOST=mariadb
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - WP_TITLE=${WP_TITLE}
      - WP_ADMIN_USER=${WP_ADMIN_USER}
      - WP_ADMIN_PASSWORD=${WP_ADMIN_PASSWORD}
      - WP_ADMIN_EMAIL=${WP_ADMIN_EMAIL}
      - WP_USER=${WP_USER}
      - WP_USER_EMAIL=${WP_USER_EMAIL}
      - WP_USER_PASSWORD=${WP_USER_PASSWORD}
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '1.0'
    healthcheck:
      test: ["CMD", "php-fpm82", "-t"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  mariadb:
    build:
      context: ./requirements/mariadb
      dockerfile: Dockerfile
    container_name: mariadb
    volumes:
      - mariadb_data:/var/lib/mysql
    networks:
      - inception
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    restart: always
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
      - DAC_OVERRIDE
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  wordpress_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /home/${USER}/data/wordpress

  mariadb_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /home/${USER}/data/mariadb

networks:
  inception:
    driver: bridge
    name: inception

---

まとめ

本章で学んだこと:

  • 脅威モデル: コンテナ環境特有の脅威
  • イメージセキュリティ: スキャン、マルチステージ
  • 実行時セキュリティ: 非root、capability、seccomp
  • シークレット管理: .env、Docker Secrets
  • ネットワーク: 分離、TLS強化
  • 監視: ログ、ヘルスチェック
  • リソース制限: メモリ、CPU
  • チェックリスト: 評価前の確認事項

これでInceptionプロジェクトのセキュリティ対策は完了です。次はAppendixで、コンテナ技術の歴史と現代的な応用を学びます。