T-CREATOR

Docker セキュアイメージ設計:非 root・最小ベース・Capabilities 削減の実装指針

Docker セキュアイメージ設計:非 root・最小ベース・Capabilities 削減の実装指針

コンテナセキュリティは、現代のアプリケーション開発において避けて通れない重要なテーマです。Docker イメージを本番環境にデプロイする際、適切なセキュリティ対策を講じなければ、システム全体が脆弱性にさらされる危険性があります。

本記事では、Docker イメージのセキュリティを飛躍的に向上させる 3 つの実装指針について、初心者の方にもわかりやすく解説していきます。非 root ユーザーでの実行、最小ベースイメージの選択、そして Linux Capabilities の削減という、実践的なアプローチを段階的に学んでいきましょう。

背景

Docker イメージにおけるセキュリティリスク

Docker コンテナは、デフォルトで root ユーザーとして実行されます。この設計は開発時の利便性を優先したものですが、本番環境では重大なセキュリティリスクとなるのです。

コンテナ内でアプリケーションが root 権限で動作すると、万が一攻撃者に侵入された場合、コンテナ内のすべてのリソースに自由にアクセスされてしまいます。さらに、コンテナエスケープという脆弱性を悪用されれば、ホストシステムへの侵入を許してしまう可能性も考えられるのです。

セキュリティ設計の重要性

近年、コンテナ環境を標的としたサイバー攻撃が増加傾向にあります。2023 年には、脆弱なコンテナイメージを狙った攻撃が前年比で 40%以上増加したという報告もあるほどです。

以下の図は、セキュアでない Docker イメージと、セキュア設計を施した Docker イメージの違いを示しています。

mermaidflowchart TB
    subgraph insecure["セキュアでないイメージ"]
        root1["root ユーザー実行"]
        full1["フル OS ベースイメージ"]
        cap1["全 Capabilities 保持"]
    end

    subgraph secure["セキュアなイメージ"]
        user2["非 root ユーザー実行"]
        mini2["最小ベースイメージ"]
        cap2["必要最小限の Capabilities"]
    end

    risk["セキュリティリスク"] -->|高| insecure
    risk -->|低| secure

この図から分かるように、セキュア設計を施すことで、攻撃対象領域を大幅に削減できます。

課題

デフォルト設定の危険性

Docker を使い始めたばかりの開発者の多くが、デフォルト設定のまま本番環境にデプロイしてしまいます。この状況には、以下のような複数の課題が潜んでいるのです。

課題 1:root ユーザーでの実行リスク

コンテナ内で root として実行されるプロセスは、コンテナ内のすべてのファイルやリソースに対して完全な権限を持ちます。攻撃者がアプリケーションの脆弱性を突いて任意のコードを実行できた場合、システム全体が危険にさらされてしまうでしょう。

課題 2:肥大化したベースイメージ

Ubuntu や CentOS などのフル OS イメージをベースにすると、イメージサイズが数百 MB から数 GB に達することがあります。イメージが大きいほど、含まれるパッケージやライブラリも多くなり、脆弱性の混入リスクが高まるのです。

課題 3:過剰な権限付与

Linux Capabilities は、従来の root 権限を細分化した仕組みですが、デフォルトでは多くの Capabilities がコンテナに付与されています。必要のない Capabilities を持つことで、攻撃者に悪用される可能性が広がってしまいます。

以下の図は、これら 3 つの課題がどのように連鎖してセキュリティリスクを高めるかを表しています。

mermaidflowchart LR
    A["デフォルト設定"] --> B["root ユーザー実行"]
    A --> C["フル OS イメージ"]
    A --> D["全 Capabilities 付与"]

    B --> E["権限昇格リスク"]
    C --> F["脆弱性混入リスク"]
    D --> G["悪用可能な機能増加"]

    E --> H["セキュリティ侵害"]
    F --> H
    G --> H

この図が示すように、デフォルト設定のまま運用すると、複数の経路からセキュリティ侵害のリスクが高まります。

実装の複雑さ

セキュリティ対策の必要性は理解していても、実装方法がわからないという声も多く聞かれます。特に、どのベースイメージを選ぶべきか、どの Capabilities を削減すべきかといった判断には、ある程度の知識と経験が必要とされるのです。

解決策

セキュアイメージ設計の 3 本柱

Docker イメージのセキュリティを大幅に向上させるには、以下の 3 つのアプローチを組み合わせることが効果的です。

mermaidflowchart TB
    secure["セキュアイメージ設計"]

    secure --> approach1["非 root ユーザー実行"]
    secure --> approach2["最小ベースイメージ選択"]
    secure --> approach3["Capabilities 削減"]

    approach1 --> benefit1["権限昇格リスク低減"]
    approach2 --> benefit2["攻撃対象領域縮小"]
    approach3 --> benefit3["悪用可能機能制限"]

    benefit1 --> result["堅牢なコンテナ環境"]
    benefit2 --> result
    benefit3 --> result

それでは、それぞれのアプローチについて詳しく見ていきましょう。

解決策 1:非 root ユーザーでの実行

コンテナ内でアプリケーションを非 root ユーザーとして実行することで、権限昇格攻撃のリスクを大幅に削減できます。

実装のポイント

  1. Dockerfile 内で専用ユーザーを作成する
  2. アプリケーション実行前にユーザーを切り替える
  3. ファイルの所有権を適切に設定する

この対策により、万が一コンテナ内で任意のコード実行を許してしまっても、攻撃者が実行できる操作は非 root ユーザーの権限内に制限されます。

解決策 2:最小ベースイメージの選択

必要最小限のコンポーネントのみを含むベースイメージを選択することで、脆弱性の混入リスクを低減できます。

主要な最小ベースイメージ

#イメージ名サイズ特徴適用場面
1Alpine Linux約 5MB軽量、musl libc 使用一般的な用途
2Distroless約 2-20MBランタイムのみ、シェルなし本番環境
3Scratch0MB空のイメージ静的バイナリ

Alpine Linux は軽量でありながら必要なツールが揃っており、開発段階で使いやすいイメージです。一方、Distroless は不要なツールを一切含まないため、本番環境での使用に最適でしょう。

解決策 3:Linux Capabilities の削減

Linux Capabilities は、root 権限を細分化した機能単位の権限です。不要な Capabilities を削除することで、攻撃者が悪用できる機能を制限できます。

削減すべき主な Capabilities

#Capability 名機能削減推奨度
1CAP_NET_RAWRaw ソケット作成★★★
2CAP_SYS_ADMINシステム管理操作★★★
3CAP_SYS_MODULEカーネルモジュール操作★★★
4CAP_NET_BIND_SERVICE1024 未満のポートバインド★★☆

ほとんどのアプリケーションでは、これらの Capabilities は不要です。削減することで、攻撃者による悪用の可能性を大幅に減らせるでしょう。

具体例

実装例 1:非 root ユーザーでの Node.js アプリケーション実行

まずは、Node.js アプリケーションを非 root ユーザーで実行する Dockerfile の実装例を見ていきましょう。

ベースイメージの指定とユーザー作成

最初に、ベースイメージを指定し、アプリケーション専用のユーザーを作成します。

dockerfile# Alpine ベースの Node.js イメージを使用
FROM node:18-alpine

# アプリケーション用の非 root ユーザーを作成
# -D オプションでパスワードなしのユーザーを作成
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

このコードでは、appuser という非 root ユーザーと appgroup というグループを作成しています。UID と GID を明示的に指定することで、環境間での一貫性を保てるのです。

作業ディレクトリの設定と権限付与

次に、アプリケーションのファイルを配置するディレクトリを作成し、適切な権限を設定します。

dockerfile# 作業ディレクトリを作成
WORKDIR /app

# 作業ディレクトリの所有者を変更
# appuser がファイルを読み書きできるようにする
RUN chown -R appuser:appgroup /app

ここでは、​/​app ディレクトリの所有者を appuser に変更しています。これにより、後続の処理で非 root ユーザーがファイルにアクセスできるようになりますね。

依存関係のインストール

依存関係のインストールは、まだ root ユーザーのまま実行します。これは、一部のパッケージが root 権限を必要とする場合があるためです。

dockerfile# package.json と package-lock.json をコピー
COPY package*.json ./

# 依存関係をインストール
# --production フラグで本番用の依存関係のみインストール
RUN yarn install --frozen-lockfile --production && \
    yarn cache clean

--frozen-lockfile オプションを使用することで、lock ファイルと package.json の整合性を保証できます。また、yarn cache clean でキャッシュを削除し、イメージサイズを削減しています。

アプリケーションコードのコピーとユーザー切り替え

アプリケーションコードをコピーし、実行ユーザーを切り替えます。

dockerfile# アプリケーションのソースコードをコピー
COPY --chown=appuser:appgroup . .

# ここから非 root ユーザーに切り替え
# 以降のコマンドとコンテナ実行時は appuser として動作
USER appuser

# アプリケーションのポートを公開
EXPOSE 3000

COPY コマンドの --chown オプションを使用することで、コピーと同時に所有者を設定できます。USER ディレクティブ以降は、すべての処理が appuser として実行されるようになるのです。

アプリケーションの起動

最後に、アプリケーションの起動コマンドを定義します。

dockerfile# アプリケーションを起動
# CMD は配列形式で記述することでシェルを経由せず実行
# これによりシグナルハンドリングが適切に動作する
CMD ["node", "server.js"]

配列形式の CMD を使用することで、シェルを介さずに直接プロセスを起動できます。これにより、シグナルの処理が正しく動作し、グレースフルシャットダウンが可能になるでしょう。

実装例 2:Distroless イメージでの Go アプリケーション

次に、Google が提供する Distroless イメージを使用した、さらにセキュアな実装例を見ていきます。

マルチステージビルドの設定

Distroless イメージにはコンパイラやビルドツールが含まれていないため、マルチステージビルドを使用します。

dockerfile# ビルドステージ:Go のフルイメージを使用
FROM golang:1.21-alpine AS builder

# ビルドに必要なパッケージをインストール
# ca-certificates は HTTPS 通信に必要
RUN apk add --no-cache ca-certificates git

WORKDIR /build

ビルドステージでは、コンパイルに必要なすべてのツールが揃った golang:alpine イメージを使用します。ca-certificates は、外部 API との HTTPS 通信に必要な証明書です。

依存関係の取得とビルド

Go モジュールの依存関係を取得し、アプリケーションをビルドします。

dockerfile# Go モジュールの依存関係を先にコピー
# これによりレイヤーキャッシュが効率化される
COPY go.mod go.sum ./
RUN go mod download

# ソースコードをコピーしてビルド
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -a -installsuffix cgo \
    -ldflags="-w -s" \
    -o app .

CGO_ENABLED=0 を設定することで、静的リンクされたバイナリが生成されます。-ldflags="-w -s" でデバッグ情報を削除し、バイナリサイズを縮小しているのです。

Distroless イメージへのコピー

ビルドしたバイナリを Distroless イメージにコピーします。

dockerfile# 実行ステージ:Distroless イメージを使用
# nonroot バリアントは事前に非 root ユーザーが設定済み
FROM gcr.io/distroless/static-debian11:nonroot

# ビルドステージから必要なファイルのみコピー
# ca-certificates は HTTPS 通信に必要
COPY --from=builder /etc/ssl/certs/ca-certificates.crt \
     /etc/ssl/certs/

# ビルドしたバイナリをコピー
COPY --from=builder /build/app /app

# Distroless の nonroot イメージは UID 65532 で実行される
# USER ディレクティブは不要(既に設定済み)

EXPOSE 8080

Distroless の nonroot バリアントには、あらかじめ非 root ユーザー(UID: 65532)が設定されています。シェルやパッケージマネージャーが一切含まれていないため、攻撃者がコンテナ内で悪用できるツールがほとんどありません。

エントリーポイントの設定

最後に、アプリケーションの起動方法を定義します。

dockerfile# バイナリを直接実行
# Distroless にはシェルがないため、配列形式のみ使用可能
ENTRYPOINT ["/app"]

Distroless イメージにはシェルが存在しないため、ENTRYPOINTCMD は必ず配列形式で記述する必要があります。これにより、余計なプロセスが介在せず、セキュアかつ効率的に実行できるのです。

実装例 3:docker-compose での Capabilities 削減

docker-compose を使用して、コンテナの Capabilities を削減する設定を見ていきましょう。

基本的な docker-compose.yml の構成

まず、サービスの基本設定を定義します。

yaml# docker-compose.yml
version: '3.8'

services:
  web:
    # セキュアな Dockerfile からビルド
    build:
      context: .
      dockerfile: Dockerfile.secure

    # コンテナ名を明示的に指定
    container_name: secure-web-app

このコードでは、前述のセキュアな Dockerfile を使用してイメージをビルドする設定をしています。

セキュリティオプションの設定

次に、Capabilities の削減と読み取り専用ルートファイルシステムの設定を行います。

yaml# セキュリティ設定
security_opt:
  # AppArmor や SELinux などのセキュリティプロファイルを指定可能
  - no-new-privileges:true

# すべての Capabilities を削除
cap_drop:
  - ALL

# 必要最小限の Capabilities のみ追加
# NET_BIND_SERVICE: 1024 未満のポートをバインド可能に
cap_add:
  - NET_BIND_SERVICE

cap_drop: ALL ですべての Capabilities を削除してから、cap_add で必要なものだけを追加する方法が推奨されます。no-new-privileges オプションは、コンテナ内でのさらなる権限昇格を防ぐ設定です。

読み取り専用ファイルシステムとボリューム設定

ルートファイルシステムを読み取り専用にし、書き込みが必要な場所にのみボリュームをマウントします。

yaml# ルートファイルシステムを読み取り専用に設定
# これにより攻撃者がファイルを改ざんできなくなる
read_only: true

# 書き込みが必要なディレクトリにのみ tmpfs をマウント
tmpfs:
  - /tmp:size=10M,mode=1777
  - /app/logs:size=100M,mode=0755

読み取り専用ルートファイルシステムは、攻撃者がマルウェアを設置したり、設定ファイルを改ざんしたりすることを防ぎます。書き込みが必要な ​/​tmp​/​app​/​logs には、メモリ上のファイルシステム(tmpfs)をマウントしているのです。

リソース制限とヘルスチェック

最後に、リソース制限とヘルスチェックを設定します。

yaml# リソース制限でDoS攻撃を防ぐ
deploy:
  resources:
    limits:
      cpus: '0.5'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 256M

# ヘルスチェックの設定
healthcheck:
  test:
    [
      'CMD',
      'wget',
      '--quiet',
      '--tries=1',
      '--spider',
      'http://localhost:3000/health',
    ]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

リソース制限により、コンテナが無制限にリソースを消費することを防げます。ヘルスチェックを設定することで、アプリケーションの異常を早期に検出できるでしょう。

実装例 4:セキュリティスキャンの実施

Docker イメージのセキュリティを継続的に維持するには、定期的なスキャンが欠かせません。

Trivy を使用したイメージスキャン

Trivy は、Docker イメージの脆弱性を検出するオープンソースツールです。

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

# Docker イメージをスキャン
# --severity で検出する脆弱性の深刻度を指定
trivy image --severity HIGH,CRITICAL myapp:latest

# JSON 形式で出力して CI/CD に組み込む
trivy image --format json --output results.json myapp:latest

Trivy は、既知の脆弱性データベース(CVE)と照合して、イメージに含まれるパッケージの脆弱性を検出してくれます。CI/CD パイプラインに組み込むことで、脆弱性のあるイメージのデプロイを防げるのです。

Docker Bench Security の実行

Docker Bench Security は、Docker のベストプラクティスに準拠しているかをチェックするツールです。

bash# Docker Bench Security の実行
# コンテナとして実行し、ホストの Docker 環境をチェック
docker run -it --net host --pid host --userns host \
  --cap-add audit_control \
  -v /var/lib:/var/lib \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /etc:/etc --label docker_bench_security \
  docker/docker-bench-security

このツールは、CIS Docker Benchmark に基づいて、セキュリティ設定を自動的にチェックしてくれます。

よくあるエラーと対処法

エラー 1:Permission Denied エラー

非 root ユーザーに切り替えた後、ファイルへのアクセスで権限エラーが発生する場合があります。

エラーコード: Error: EACCES: permission denied

エラーメッセージ:

javascriptError: EACCES: permission denied, open '/app/config.json'
    at Object.openSync (fs.js:476:3)
    at Object.readFileSync (fs.js:377:35)

発生条件:

  • ファイルやディレクトリの所有者が root のまま
  • USER ディレクティブで非 root ユーザーに切り替え後にアクセス

解決方法:

  1. Dockerfile で COPY 時に --chown オプションを使用する
dockerfile# 正しい方法:コピー時に所有者を指定
COPY --chown=appuser:appgroup . /app
  1. すでにコピー済みのファイルには chown を実行する
dockerfile# ディレクトリ全体の所有者を変更
RUN chown -R appuser:appgroup /app
  1. 特定のファイルのみ権限を変更する場合
dockerfile# 必要なファイルのみ権限変更
RUN chown appuser:appgroup /app/config.json && \
    chmod 644 /app/config.json

エラー 2:ポートバインドエラー

1024 未満のポートを非 root ユーザーでバインドしようとするとエラーが発生します。

エラーコード: Error: listen EACCES: permission denied 0.0.0.0:80

エラーメッセージ:

arduinoError: listen EACCES: permission denied 0.0.0.0:80
    at Server.setupListenHandle [as _listen2] (net.js:1318:21)
    at listenInCluster (net.js:1383:12)

発生条件:

  • 非 root ユーザーが 1024 未満のポート(80, 443 など)にバインドを試行

解決方法:

  1. アプリケーションで 1024 以上のポートを使用する(推奨)
javascript// server.js
const PORT = process.env.PORT || 3000; // 1024 以上のポート
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
  1. docker-compose で CAP_NET_BIND_SERVICE を追加する
yamlservices:
  web:
    cap_add:
      - NET_BIND_SERVICE
    ports:
      - '80:80'
  1. リバースプロキシを使用する(本番環境推奨)
yamlservices:
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'

  app:
    # 非 root で 3000 ポートで起動
    expose:
      - '3000'

エラー 3:Distroless イメージでのデバッグ困難

Distroless イメージにはシェルが含まれないため、通常の方法でデバッグできません。

エラー状況: コンテナ内に入れず、ログも出力されない

解決方法:

  1. 開発時は debug バリアントを使用する
dockerfile# デバッグ用の Dockerfile.debug
FROM gcr.io/distroless/base-debian11:debug-nonroot

# debug バリアントには busybox が含まれている
# /busybox/sh でシェルが使用可能
  1. docker exec でデバッグシェルに入る
bash# debug バリアントのコンテナに入る
docker exec -it container_name /busybox/sh
  1. ログ出力を標準出力に送る
javascript// アプリケーションのログは必ず stdout/stderr に出力
console.log('Application started');
console.error('Error occurred:', error);
  1. 本番環境では外部ログ収集を使用する
yamlservices:
  app:
    logging:
      driver: 'json-file'
      options:
        max-size: '10m'
        max-file: '3'

まとめ

Docker イメージのセキュリティ設計は、現代のアプリケーション開発において必須のスキルとなりました。本記事では、セキュアな Docker イメージを構築するための 3 つの重要な実装指針をご紹介しました。

非 root ユーザーでの実行は、権限昇格攻撃のリスクを大幅に削減します。Dockerfile 内で専用ユーザーを作成し、USER ディレクティブで切り替えるだけで実装できるため、すぐにでも取り入れられるでしょう。

最小ベースイメージの選択により、攻撃対象領域を縮小できます。Alpine Linux は開発段階で使いやすく、Distroless は本番環境で最高レベルのセキュリティを提供してくれます。マルチステージビルドを活用することで、両者の利点を組み合わせられるのです。

Linux Capabilities の削減は、攻撃者が悪用できる機能を制限します。docker-compose で cap_drop: ALL を設定し、必要最小限の Capabilities のみを追加する方法が効果的です。

これら 3 つのアプローチを組み合わせることで、Docker コンテナのセキュリティは飛躍的に向上します。最初は少し手間に感じるかもしれませんが、一度テンプレート化してしまえば、すべてのプロジェクトで再利用できるでしょう。

セキュアな Docker イメージの構築は、アプリケーションとユーザーを守る第一歩です。ぜひ今日から実践して、より安全なコンテナ環境を構築してください。

関連リンク

;