Dockerセキュリティ完全ガイド - 安全なコンテナ運用のベストプラクティス
Dockerセキュリティ完全ガイド
Dockerは開発から本番まで幅広く使われていますが、セキュリティ対策を怠ると重大な脆弱性につながります。本記事では、安全なコンテナ運用に必要なセキュリティ対策を包括的に解説します。
なぜDockerのセキュリティが重要なのか
Dockerコンテナは隔離されているように見えますが、適切な設定がなければホストシステムへの攻撃経路となり得ます。
主なセキュリティリスク
- 脆弱なベースイメージ - 既知の脆弱性を含むイメージの使用
- root権限での実行 - コンテナ脱出時の影響範囲拡大
- 機密情報の漏洩 - 環境変数やレイヤーに埋め込まれたシークレット
- 過剰な権限付与 - 不要なCapabilityやボリュームマウント
- ネットワーク露出 - 不適切なポート公開やネットワーク設定
イメージスキャン
Trivy による脆弱性スキャン
Trivyは包括的な脆弱性スキャナーです。
# Trivyのインストール(macOS)
brew install aquasecurity/trivy/trivy
# イメージのスキャン
trivy image node:20-alpine
# 高・重大な脆弱性のみ表示
trivy image --severity HIGH,CRITICAL nginx:latest
# JSON形式で出力
trivy image --format json --output results.json python:3.12
CI/CDパイプラインへの統合
GitHub Actionsでの自動スキャン例:
name: Container Security
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
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'
- name: Fail on high severity
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
exit-code: '1'
severity: 'CRITICAL,HIGH'
Docker Scout
Docker公式のセキュリティツール:
# Docker Scoutの有効化
docker scout enroll
# イメージの分析
docker scout cves node:20-alpine
# 推奨事項の取得
docker scout recommendations node:20-alpine
# CVEの詳細表示
docker scout cves --format only-packages node:20-alpine
スキャン結果の対処
# 悪い例:古いベースイメージ
FROM node:16
# 良い例:最新の安定版
FROM node:20-alpine
# さらに良い例:特定バージョンの指定
FROM node:20.11.0-alpine3.19
# ベストプラクティス:マルチステージビルドで最小化
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]
Rootless Docker
コンテナをroot以外のユーザーで実行することで、セキュリティを大幅に向上させます。
Rootless Dockerのセットアップ
# 既存のDockerを停止
sudo systemctl stop docker
# rootlessモードのインストール
curl -fsSL https://get.docker.com/rootless | sh
# 環境変数の設定
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# ~/.bashrcに追加
cat >> ~/.bashrc << 'EOF'
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
EOF
# サービスの起動
systemctl --user start docker
systemctl --user enable docker
コンテナ内でのユーザー指定
# 方法1: USERディレクティブ
FROM node:20-alpine
# アプリケーションディレクトリの作成
WORKDIR /app
# 依存関係のインストール(rootで実行)
COPY package*.json ./
RUN npm ci --only=production
# アプリケーションファイルのコピー
COPY . .
# 非rootユーザーへ切り替え
USER node
CMD ["node", "server.js"]
# 方法2: カスタムユーザーの作成
FROM ubuntu:22.04
# 専用ユーザーの作成
RUN groupadd -r appuser && \
useradd -r -g appuser -u 1001 appuser
WORKDIR /app
# ファイルのコピーと所有権の設定
COPY --chown=appuser:appuser . .
USER appuser
CMD ["./myapp"]
# 方法3: 数値UIDの使用(Kubernetes推奨)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 数値UIDで指定
USER 1000
CMD ["python", "app.py"]
rootが必要な処理の分離
FROM node:20-alpine AS setup
# root権限が必要な処理
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine
WORKDIR /app
# ビルド済みのnode_modulesをコピー
COPY --from=setup /app/node_modules ./node_modules
COPY --chown=node:node . .
# 最初から非rootユーザーで実行
USER node
CMD ["node", "server.js"]
Secrets管理
機密情報を安全に扱う方法を解説します。
Docker Secrets(Swarm/Compose)
# docker-compose.yml
version: '3.8'
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
アプリケーション側での読み込み:
// Node.js example
const fs = require('fs');
function getSecret(secretName) {
const secretPath = process.env[`${secretName.toUpperCase()}_FILE`];
if (secretPath) {
return fs.readFileSync(secretPath, 'utf8').trim();
}
// フォールバック: 環境変数から直接読み込み
return process.env[secretName.toUpperCase()];
}
const dbPassword = getSecret('db_password');
const apiKey = getSecret('api_key');
BuildKit Secrets
ビルド時のみ必要なシークレットの安全な利用:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# ビルド時のみNPM_TOKENを利用
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
npm ci && \
rm .npmrc
COPY . .
USER node
CMD ["node", "server.js"]
ビルドコマンド:
# シークレットを渡してビルド
docker build --secret id=npm_token,src=$HOME/.npm_token -t myapp .
# BuildKitの有効化(古いバージョン)
DOCKER_BUILDKIT=1 docker build --secret id=npm_token,src=./.npm_token -t myapp .
環境変数の安全な使用
# 悪い例:機密情報をレイヤーに残す
FROM node:20-alpine
ENV API_KEY=secret123
ENV DB_PASSWORD=pass456
# 良い例:実行時に渡す(Dockerfileには記載しない)
FROM node:20-alpine
# 環境変数は実行時に注入
実行時の注入:
# docker run
docker run -e API_KEY=$API_KEY -e DB_PASSWORD=$DB_PASSWORD myapp
# docker-compose.yml
services:
app:
image: myapp
environment:
- API_KEY=${API_KEY}
- DB_PASSWORD=${DB_PASSWORD}
.envファイルの管理
# .env.example(リポジトリにコミット)
API_KEY=your_api_key_here
DB_PASSWORD=your_db_password_here
# .env(.gitignoreに追加、コミットしない)
API_KEY=actual_secret_key
DB_PASSWORD=actual_password
# .gitignore
.env
.env.local
secrets/
*.key
*.pem
ネットワーク分離
カスタムネットワークの作成
# フロントエンド用ネットワーク
docker network create frontend
# バックエンド用ネットワーク
docker network create backend
# データベース用ネットワーク
docker network create database
docker-composeでの分離
version: '3.8'
services:
nginx:
image: nginx:alpine
networks:
- frontend
ports:
- "80:80"
api:
image: myapi:latest
networks:
- frontend
- backend
# 外部ポートは公開しない
worker:
image: myworker:latest
networks:
- backend
postgres:
image: postgres:16-alpine
networks:
- backend
# 外部からアクセス不可
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 外部インターネットへのアクセスを遮断
secrets:
db_password:
file: ./secrets/db_password.txt
ネットワークポリシーの検証
# コンテナのネットワーク接続確認
docker exec api ping -c 1 postgres
docker exec nginx ping -c 1 postgres # 失敗するはず
# ネットワークの詳細表示
docker network inspect backend
# コンテナのネットワーク一覧
docker inspect api | jq '.[0].NetworkSettings.Networks'
Capabilityの制限
Linuxの Capability を最小限にすることでセキュリティを向上させます。
デフォルトCapabilityの確認
# コンテナのCapability確認
docker run --rm alpine sh -c 'apk add -U libcap; capsh --print'
不要なCapabilityの削除
# docker-compose.yml
services:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 1024番未満のポートをバインド
- CHOWN # ファイル所有者の変更
security_opt:
- no-new-privileges:true
# docker run
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \
myapp:latest
特権モードの回避
# 悪い例:絶対に避けるべき
services:
app:
privileged: true
# 良い例:必要最小限の権限のみ
services:
app:
cap_drop:
- ALL
cap_add:
- NET_ADMIN # 本当に必要な場合のみ
Read-onlyファイルシステム
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /var/run
volumes:
- app-data:/app/data
volumes:
app-data:
# docker run
docker run \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
-v app-data:/app/data \
myapp:latest
コンテナリソース制限
DoS攻撃や暴走を防ぐためのリソース制限:
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
ulimits:
nofile:
soft: 65536
hard: 65536
nproc: 512
# docker run
docker run \
--cpus="0.5" \
--memory="512m" \
--memory-swap="512m" \
--pids-limit=100 \
myapp:latest
セキュリティスキャンとモニタリング
Dockerベンチセキュリティ
# Docker Bench Securityの実行
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh
# Docker コンテナとして実行
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /etc:/etc:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro \
-v /usr/bin/runc:/usr/bin/runc:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--label docker_bench_security \
docker/docker-bench-security
Falco によるランタイム監視
# docker-compose.yml
services:
falco:
image: falcosecurity/falco:latest
privileged: true
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
- /dev:/host/dev
- /proc:/host/proc:ro
- /boot:/host/boot:ro
- /lib/modules:/host/lib/modules:ro
- /usr:/host/usr:ro
- ./falco-rules.yaml:/etc/falco/falco_rules.local.yaml
カスタムルール例 (falco-rules.yaml):
- rule: Unauthorized Process in Container
desc: Detect unauthorized processes
condition: >
spawned_process and
container and
not proc.name in (node, npm, sh, bash)
output: >
Unauthorized process started
(user=%user.name command=%proc.cmdline container=%container.id)
priority: WARNING
- rule: Write below root
desc: Detect writes to root filesystem
condition: >
write and
container and
fd.name startswith /
output: >
File write below root
(user=%user.name file=%fd.name container=%container.id)
priority: ERROR
Content Trust(イメージ署名)
# Content Trustの有効化
export DOCKER_CONTENT_TRUST=1
# イメージのpush(自動的に署名される)
docker push myregistry.com/myapp:latest
# 署名付きイメージのpull(検証される)
docker pull myregistry.com/myapp:latest
# 署名情報の確認
docker trust inspect myregistry.com/myapp:latest
実践的なセキュアDockerfile
すべてのベストプラクティスを適用した例:
# syntax=docker/dockerfile:1
# ビルドステージ
FROM node:20.11.0-alpine3.19 AS builder
# セキュリティアップデート
RUN apk upgrade --no-cache
WORKDIR /app
# 依存関係のみ先にコピー(キャッシュ効率化)
COPY package*.json ./
# ビルド時のシークレット使用
RUN --mount=type=secret,id=npm_token \
if [ -f /run/secrets/npm_token ]; then \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc; \
fi && \
npm ci --only=production && \
rm -f .npmrc
# アプリケーションコード
COPY . .
# 本番ステージ
FROM node:20.11.0-alpine3.19
# セキュリティアップデート
RUN apk upgrade --no-cache && \
# 不要なパッケージの削除
apk del apk-tools && \
# 一時ファイルのクリーンアップ
rm -rf /var/cache/apk/* /tmp/*
WORKDIR /app
# ビルド成果物のみコピー
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node . .
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# 非rootユーザーで実行
USER node
# 最小限のポート公開
EXPOSE 3000
# シグナルハンドリング
STOPSIGNAL SIGTERM
CMD ["node", "server.js"]
対応する docker-compose.yml:
version: '3.8'
services:
app:
build:
context: .
secrets:
- npm_token
image: myapp:latest
# セキュリティ設定
read_only: true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
# リソース制限
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
# 一時ファイルシステム
tmpfs:
- /tmp
- /app/tmp
# ボリューム
volumes:
- app-logs:/app/logs:rw
# ネットワーク
networks:
- frontend
- backend
# 環境変数(機密情報はsecretsで)
environment:
NODE_ENV: production
PORT: 3000
secrets:
- db_password
- api_key
# ヘルスチェック
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
volumes:
app-logs:
driver: local
secrets:
npm_token:
file: ./secrets/npm_token.txt
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
セキュリティチェックリスト
本番環境デプロイ前の確認事項:
イメージ
- 最新の安定版ベースイメージを使用
- 脆弱性スキャン(Trivy/Scout)実施
- マルチステージビルドで最小化
- 不要なファイル・ツールの削除
- Content Trust有効化
ユーザー・権限
- 非rootユーザーで実行
- Capabilityを最小化
- 特権モード未使用
- Read-onlyファイルシステム
- no-new-privileges設定
機密情報
- Secrets機能の使用
- 環境変数に機密情報なし
- イメージレイヤーに機密情報なし
- .envファイルを.gitignore
ネットワーク
- 必要最小限のポート公開
- カスタムネットワーク使用
- サービス間の適切な分離
- ファイアウォール設定
リソース
- CPU/メモリ制限設定
- PID制限設定
- ディスクI/O制限
モニタリング
- ログ収集設定
- ヘルスチェック実装
- ランタイム監視(Falco等)
- アラート設定
まとめ
Dockerのセキュリティは、単一の対策ではなく、多層防御の考え方が重要です。
重要ポイント
- イメージセキュリティ: 定期的なスキャンと更新
- 最小権限の原則: rootless、Capability制限、read-only
- 機密情報保護: Secrets機能の活用、環境変数の適切な管理
- ネットワーク分離: カスタムネットワークによる適切な分離
- 継続的監視: スキャン自動化とランタイム監視
これらの対策を組み合わせることで、安全なコンテナ運用が実現できます。
次のステップ
- Kubernetesのセキュリティ(Pod Security Standards、Network Policies)
- サプライチェーンセキュリティ(SBOM、Sigstore)
- ゼロトラストアーキテクチャの実装
- コンプライアンス対応(CIS Benchmark、NIST)
セキュリティは一度設定して終わりではありません。継続的な改善と監視を心がけましょう。