Distroless Dockerイメージ:不要なシェルとOSを排除してコンテナセキュリティを強化する

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

通常のDockerイメージがセキュリティリスクになる理由

Dockerを学び始めた頃、多くの人がubuntu:22.04debian:bookwormをベースイメージとして使い慣れているでしょう。便利で必要なツールが揃っており、apt installも自由に使えます。しかし、しばらくproductionで運用していると、この習慣を見直す必要があると感じ始めました。

以下のコマンドを実行して、「普通の」Ubuntuイメージに実際に何が含まれているか確認してみましょう:

docker run --rm ubuntu:22.04 ls /bin

結果:数百ものバイナリ — bashshcurlwgetncpython3… これらはいずれもあなたのアプリケーションとは無関係です。しかし、コンテナが侵害された場合、攻撃者はラテラルムーブメント、データ漏洩、リバースシェルの設置に必要なツールを全て手にすることになります。

Googleはまさにこの問題を解決するためにDistroless Imagesを開発しました。

Distroless Imagesとは?

Distrolessは、Googleがメンテナンスするベースイメージのセットで、ランタイムに必要なものだけを含みます — それ以上でも以下でもありません。シェル(/bin/sh)なし、パッケージマネージャーなし、coreutilsなし。含まれるのは:

  • 標準Cライブラリ(glibcまたはmusl
  • CA証明書(HTTPS用)
  • 対応ランタイム(JRE、Pythonインタープリター、Node.js…)
  • タイムゾーンデータ

セキュリティの観点から言えば、RCEを受けてもコンテナに侵入した攻撃者はシェルがないため何のコマンドも実行できません。パッケージマネージャーもないため、追加ツールのダウンロードも不可能です。アタックサーフェスは通常のUbuntuイメージと比較して大幅に縮小されます。

30以上のコンテナを稼働させているproductionクラスターにこの手法を適用したところ、リソース使用量を40%削減できました — その大部分は、イメージの小型化、レイヤーキャッシュの効率化、スケールアウト時のイメージpull時間の明確な短縮によるものです。

環境のセットアップと準備

追加インストールは不要です。DistrolessイメージはGoogleコンテナレジストリ(Google Container Registry)とGitHub Container Registryで直接ホストされており、Dockerがあればすぐに使えます。

主要なイメージ一覧:

# 主要なDistrolessイメージ
gcr.io/distroless/static-debian12        # CA証明書+タイムゾーンのみ(Go静的バイナリ向け)
gcr.io/distroless/base-debian12          # glibc + OpenSSL(C/C++、Rust向け)
gcr.io/distroless/java21-debian12        # JRE 21
gcr.io/distroless/python3-debian12       # Python 3
gcr.io/distroless/nodejs20-debian12      # Node.js 20

各イメージには:debugタグもあり、必要に応じてデバッグできるようbusyboxシェルが追加されています。ただし、productionでは絶対に:debugを使わないでください

詳細設定:Distrolessを使ったマルチステージビルド

Distrolessを使いこなす鍵はマルチステージビルドとの組み合わせです。最初のステージではフル機能のイメージでビルドを行い、最終ステージでアーティファクトをDistrolessにコピーします — ソースコードを変更せずにすっきりと実現できます。

例1:Goアプリケーション(静的バイナリ)

# Stage 1: ビルド
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp ./cmd/server

# Stage 2: ランタイム — Distroless staticを使用
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /myapp
EXPOSE 8080
ENTRYPOINT ["/myapp"]

最終イメージはわずか約3〜5MB — golang:1.22(約800MB)と比べて160分の1以下で、シェルを含まないためalpine(約10MB)よりも小さくなります。

例2:Pythonアプリケーション(FastAPI/Flask)

# Stage 1: 依存関係のインストール
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

# Stage 2: ランタイム
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY --from=builder /app /app
WORKDIR /app
COPY . .
EXPOSE 8000
ENTRYPOINT ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

例3:Java Spring Boot

# Stage 1: JARのビルド
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# Stage 2: 最小限のJREを使ったランタイム
FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/target/myapp.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

設定時の重要な注意事項

  • ENTRYPOINTは必ずexec形式["binary", "arg"]を使用し、シェル形式binary argは使わない — シェルがなくパースできないため。
  • 文字列形式のCMDは使用不可 — 同じ理由です。
  • ユーザー管理機能なし — Distrolessにはnonrootユーザー(UID 65532)が組み込まれています。rootで実行しないよう、ENTRYPOINTの前にUSER nonrootを追加してください。
  • デフォルトでは/tmpへの書き込み不可 — アプリが一時ファイルを必要とする場合は、tmpfsをマウントするかKubernetesのemptyDirを使用してください。
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /myapp
# rootではなくnonrootユーザーで実行
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/myapp"]

検証とモニタリング

イメージサイズの比較

# ビルドして比較
docker build -t myapp:distroless -f Dockerfile.distroless .
docker build -t myapp:ubuntu -f Dockerfile.ubuntu .

docker images | grep myapp
# myapp   distroless   a1b2c3d4   2 minutes ago   8.2MB
# myapp   ubuntu       e5f6g7h8   3 minutes ago   182MB

シェルが存在しないことの確認

# コンテナへのexecを試みる — 失敗することを確認
docker run -d --name test myapp:distroless
docker exec -it test /bin/sh
# Error: OCI runtime exec failed: exec failed: unable to start container process:
# exec: "/bin/sh": stat /bin/sh: no such file or directory

# トラブルシューティングが必要な場合はdebugイメージを使用
docker run -it gcr.io/distroless/static-debian12:debug /busybox/sh

Trivyによる脆弱性スキャン

CVEの数を比較するのが、両者の違いを最も素早く確認できる方法です:

# Trivyのインストール
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Ubuntuベースイメージのスキャン
trivy image myapp:ubuntu
# 結果:約180件の脆弱性(HIGH: 12、CRITICAL: 3)

# Distrolessイメージのスキャン
trivy image myapp:distroless
# 結果:約8件の脆弱性(HIGH: 1、CRITICAL: 0)

この差は非常に印象的です — CRITICALが3件からゼロになり、CVEの総数は20分の1以上に減少しています。これがproductionでDistrolessへの移行を検討する最も現実的な理由です。

ヘルスチェックの統合

イメージにはcurlwgetもないため、ヘルスチェックには2つのアプローチがあります。アプリ内に自前のバイナリを用意するか、Kubernetesのprobeに完全に移行するかです。前者の方法:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD ["/healthcheck"]

Kubernetesではさらにシンプルです — httpGet probeはコンテナの外で完全に動作するため、内部にバイナリは一切不要です:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 20

Distrolessを使うべき場面・避けるべき場面

推奨される場面:アプリが安定しており長期productionで稼働している場合、チームがマルチステージビルドに慣れている場合、Kubernetesを使用している場合(ヘルスチェックがはるかに容易)。

慎重に検討すべき場面:エントリーポイントでシェルスクリプトの実行が必要なアプリ、外部システムコマンドを呼び出すアプリ(subprocessexec.Command)、またはチームがまだ開発段階にあり频繁なデバッグが必要な場合。

最小限のシェルが必要なケースでは、alpineが依然として良いバランスの選択肢です — 約5MB、/bin/shあり、UbuntuよりはるかにCVEが少ない。

Distrolessは万能薬ではありません。しかし、Go、Java、純粋なPythonで書かれたステートレスなマイクロサービスにとっては、運用コストを増やすことなくセキュリティポスチャを改善する最もシンプルな方法です。

Share: