【実務・中級編】 イメージの最小化(Distroless/Alpine)による攻撃対象領域の削減 – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

「シェルもパッケージマネージャも捨てろ」— Distroless / Alpineで実現する最強のコンテナハーデニング実践ガイド

現場のみなさん、お疲れ様です。セキュリティチーフの某です。

日々Webアプリケーションの開発やインフラ運用に追われていると、「コンテナ化してCI/CDでデプロイできているからOK」と安心してしまいがちですよね。ですが、セキュリティインシデントの現場に何度も立ち会ってきた私から言わせてもらうと、「デプロイされたコンテナの内部に何が入っているか」を把握していないシステムは、攻撃者にとって格好の「おもちゃ箱」です。

Webアプリに万が一の脆弱性(RCE:リモートコード実行など)があったとき、コンテナ内に /bin/sh や curl、apt が転がっていたらどうなるか? 攻撃者は一瞬でそれを悪用し、C2サーバーからマルウェアをダウンロードし、内部ネットワークの偵察を開始します。

今回は、攻撃者の攻撃チェーンを根本から断ち切る「イメージの最小化(Distroless / Alpine)」による攻撃対象領域(Attack Surface)の削減について、攻撃シナリオ(PoC)と現場で今すぐ使える完全な実装コードを交えて解説します。

—

1. 攻撃者はコンテナの中で何をするのか?(攻撃シナリオとリスク)

まず、なぜコンテナイメージを極限まで小さくする必要があるのか、攻撃者の視点(TTPs: Tactics, Techniques, and Procedures)から理解しましょう。

従来の「重いコンテナ(Ubuntu/Debianベース)」の場合

アプリケーションにOSコマンドインジェクションや任意コード実行(RCE)の脆弱性が存在したとします。攻撃者は以下のようなステップで侵入を拡大します。

1. 初期侵入: アプリの脆弱性を突き、コンテナ内で任意コードを実行させる。
2. 環境確認: /bin/sh や bash を起動し、whoami や id で権限を確認。
3. ツールの調達: コンテナ内に存在する curl や wget を使い、外部の悪意あるサーバーからコインハイブ(暗号資産採掘ツール)やリバースシェル用バイナリを落としてくる。
4. パッケージ追加: ツールが足りなければ apt-get update && apt-get install -y netcat などを実行して攻撃環境を整備する。
5. 横展開(Lateral Movement): コンテナを足がかりに、AWSのメタデータAPI(http://169.254.169.254)を叩いてIAMロールのクレデンシャルを窃取する。

攻撃者が使っているのは、私たちが「便利だから」という理由で残しておいた標準ツール(Living off the Land)に他なりません。

最小化された「Distrolessコンテナ」の場合

同じ脆弱性を使って攻撃者が execve("/bin/sh") や system("curl ...") を発行したとします。

結果は「No such file or directory(ファイルが存在しません)」で即座にエラーとなり、攻撃は失敗します。

シェルが存在しないため、攻撃者は対話的セッションを確立できず、curl も apt も存在しないため、ペロードのダウンロードも不可能です。これこそが「攻撃対象領域の削減」の真価です。

—

2. コンテナイメージの選択肢:Debian vs Alpine vs Distroless

ベースイメージを選ぶ際、セキュリティエンジニアとして押さえておくべき特性を比較表にまとめました。

| イメージ種別 | サイズ | シェル (/bin/sh) | パッケージマネージャ | 主な用途・特徴 |
| :— | :— | :— | :— | :— |
| Debian / Ubuntu | 100MB〜 | 存在 (bash/sh) | 存在 (apt) | 開発環境。攻撃ツールが全て揃ってしまうリスク。 |
| Alpine Linux | 5MB〜 | 存在 (ash) | 存在 (apk) | 超軽量。ただし musl libc を使用するため、glibc依存のC拡張モジュール(Python/Node等)で非互換問題が発生することがある。 |
| Distroless | 20MB〜 | 不完全/非存在 | 不存在 | Googleが提供。アプリと実行環境(Node/Python/Java等)のみを包含。シェルもパッケージマネージャも排除。 |

結論として、本番環境(Production)においては「Distroless」が最も堅牢な選択肢となります。Alpineも非常に優れていますが、シェルが残る点と musl ライブラリの互換性検証が必要です。

—

3. コピペで動く実装サンプル:マルチステージビルド + Distroless

ここからは、Node.jsアプリケーションを例に、開発環境の利便性を損なわずに、本番環境だけを完全要塞化する「マルチステージビルド」の具体的な Dockerfile を紹介します。

本番用 Dockerfile(Node.js + Distroless)

# ==========================================
# Stage 1: ビルド&依存関係解決ステージ (Build Stage)
# ==========================================
FROM node:20-slim AS builder

# 堅牢化: 作業ディレクトリの設定
WORKDIR /usr/src/app

# パッケージ定義のみを先にコピーしてキャッシュを効率化
COPY package*.json ./

# 本番用プロダクション依存関係のみをインストール
# (devDependenciesを除外して不要なコードを混入させない)
RUN npm ci --only=production

# アプリケーションコードのコピー
COPY . .

# ==========================================
# Stage 2: 実行専用ステージ (Production Stage)
# ==========================================
# Googleが提供するDistroless Node.jsイメージを使用
# :nonroot タグを使用することで、root以外のユーザー(UID 65532)で実行される
FROM gcr.io/distroless/nodejs20-debian12:nonroot

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

# ビルドステージから必要な成果物(node_modulesとコード)のみを精選してコピー
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./package.json
COPY --from=builder /usr/src/app/src ./src

# 非rootユーザー権限で実行(Distrolessのデフォルト設定を明示)
USER nonroot

# アプリケーションの起動
# Distroless Node.jsイメージは、暗黙的に `node` バイナリを呼び出すため、引数にスクリプトを指定する
CMD ["src/server.js"]

—

4. インフラレベルでの二重防御(Docker Compose / Kubernetes Security Context)

コンテナイメージを最小化するだけでなく、コンテナ実行時のセキュリティフラグを設定することで「深層防護(Defense in Depth)」を完成させます。

攻撃者がファイルシステムを書き換えてマルウェアを置くのを防ぐため、ルートファイルシステムを読み取り専用(Read-Only)化し、実行ユーザーを明確に固定します。

Docker Compose 設定例 (docker-compose.yml)

version: '3.8'

services:
  web-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    # 【セキュリティ強化1】ルートファイルシステムを完全読み取り専用にする
    read_only: true
    # 【セキュリティ強化2】root権限昇格の防止
    security_opt:
      - no-new-privileges:true
    # 【セキュリティ強化3】一時的な書き込みが必要なディレクトリのみtmpfsでメモリ上にマウント
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=65536k
    # 【セキュリティ強化4】不要なLinuxケーパビリティ(特権)を全て破棄
    cap_drop:
      - ALL
    # 【セキュリティ強化5】実行ユーザーを明示的に指定(Distrolessのnonroot UID)
    user: "65532:65532"
    restart: unless-stopped

Kubernetes Deployment 設定例 (deployment.yaml)

Kubernetes環境では、 Podの securityContext を以下のように厳格に定義します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-node-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: secure-node-app
  template:
    metadata:
      labels:
        app: secure-node-app
    spec:
      # Podレベルのセキュリティ設定
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        fsGroup: 65532
      containers:
      - name: node-app
        image: my-registry.internal/apps/node-app:v1.0.0
        # コンテナレベルのセキュリティ設定
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        resources:
          limits:
            cpu: "500m"
            memory: "512Mi"
          requests:
            cpu: "100m"
            memory: "128Mi"
        ports:
        - containerPort: 3000

—

5. 現場のエンジニアから届く「よくある悲鳴」と処方箋

Distrolessを導入しようとすると、開発チームから必ず以下のような相談(悲鳴)が飛んできます。チーフエンジニアとして、どう回答・対処すべきかを伝授しておきます。

疑問1:「コンテナの中に入ってデバッグ(kubectl exec や docker exec)できないのですが…」

【回答】
「本番環境のコンテナにログインして手動で調査・デバッグする」という運用自体が、セキュリティおよびSREの観点からアンチパターンです。ログはすべて標準出力(stdout / stderr)に吐き出し、DatadogやCloudWatchなどのログ監視基盤に集約するのが原則です。

どうしても本番Podの状態を調査する必要がある場合は、以下の代替案を使用します。

1. Kubernetes Ephemeral Containers(一時コンテナ)の活用
kubectl debug コマンドを使用すると、稼働中のDistroless Podに対して、デバッグ用ツールが入った別コンテナを一時的にアタッチして調査できます。

# 稼働中のPodにbusyboxなどのデバッグコンテナを注入して調査するコマンド
   kubectl debug -it pod/secure-node-app-xxxx --image=busybox --target=node-app

2. Distrolessの :debug イメージの活用(STG環境のみ)
GoogleはDistrolessのバリエーションとして、BusyBoxシェルが同梱された :debug タグも提供しています。ステージング環境(STG)のみ :debug イメージを使用し、本番(PRD)では通常版を使うという運用ルールにするのも有効です。

—

疑問2:「ログファイルやキャッシュファイルをローカルに書き込む処理があるのですが?」

【回答】
readOnlyRootFilesystem: true に設定すると、アプリケーションがファイル出力しようとした瞬間に EACCES や EROFS エラーで落ちます。

対応策は以下の2点です。

  • ログ: ディスクファイルではなく、標準出力(console.log 等)に出力するようコードを修正する。
  • 一時ファイル: どうしてもローカル書き込みが必要な場合(ファイルアップロードの一時処理など)は、上記の Docker Compose や K8s 設定で示したように /tmp ディレクトリ等のみを tmpfs(メモリ領域)として限定マウントします。

—

結論:攻撃者に「何も使わせない」という究極の防御

セキュリティの基本は「境界線で防ぐこと(WAFやFW)」ですが、100%防ぎ切ることは不可能です。インシデントハンドリングの極意は、「侵入された(RCEが実行された)後、攻撃者が何もできない環境を作っておくこと」にあります。

  • パッケージマネージャを消せ(ツールをダウンロードさせない)
  • シェルを消せ(コマンドを実行させない)
  • ルートファイルシステムを固めろ(ファイルを設置させない)

今回紹介した Dockerfile や YAML 設定は、今動いている皆さんのプロジェクトにそのまま導入できるレベルに落とし込んであります。次のスプリントで、ぜひ「Distrolessへの移行」をタスクに組み込んでみてください。

「攻撃されても何も起きないコンテナ」。これこそが、私たちが目指すべき最強のハーデニングです。何か実装でハマったら、いつでも相談に来てくださいね。

コメント

タイトルとURLをコピーしました