以下の内容はhttps://www.m3tech.blog/entry/2026/04/03/180000より取得しました。


distrolessコンテナイメージの中を覗いて「なんか軽くてセキュアらしい」より理解を深める

この記事はセキュリティチームブログリレー3日目 兼 AIチームブログリレー3日目の記事です。

こんにちは、セキュリティチーム 兼 AIチームの横本(@yokomotod)です。

今回は distroless コンテナイメージについて自由研究してみました。

エムスリーでもよく使われている gcr.io/distroless/static などの distroless イメージ、「シェルもパッケージマネージャもない最小限のイメージ」「軽量!なにもないから安全!」といった説明がされますが、今回は実際にその中身を確かめてみようと思います。

ブログリレーの前回記事はこちら

www.m3tech.blog

www.m3tech.blog

コンテナベースイメージのおおまかな選択肢

コンテナのベースイメージには大きく以下のような選択肢があります。

アプローチ シェル パッケージマネージャ 代表例 サイズ帯
完全空 なし なし scratch 0
distroless 系 なし なし distroless/static 2-3MB
軽量 OS あり (busybox) あり (apk) Alpine ~8MB
フル OS あり (bash) あり (apt) Debian, Ubuntu 100MB+

distroless の中身を見始める前に、まず scratch だけだと何が困るのかを(いかにも困りそうですが)確認するところから始めてみます。

scratch だけだと何が足りないか

確認のために Go でデモ用の CLI を書きました。hello, serve, https, timezone の 4 つのサブコマンドを持つプログラムと、hello だけの単独バイナリです。

// main.go — 4つのサブコマンドを持つ CLI
func main() {
        switch os.Args[1] {
        case "hello":
                // 文字列を出力するだけ
                fmt.Println("Hello from Go!")
        case "serve":
                // HTTP サーバーを起動
                http.HandleFunc("/health", ...)
                http.ListenAndServe(":8080", nil)
        case "https":
                // 外部サイトに HTTPS リクエスト
                resp, err := http.Get("https://example.com")
                // ...
        case "timezone":
                // タイムゾーン利用
                loc, err := time.LoadLocation("Asia/Tokyo")
                // ...
        }
}
// hello/main.go — fmt.Println だけ
func main() {
        fmt.Println("Hello from Go on scratch!")
}

scratch で動かしてみる

では動かしてみます。

# Dockerfile
FROM golang:1.26 AS builder
COPY . .
RUN go build -o /hello ./hello/
RUN go build -o /demo .

FROM scratch
COPY --from=builder /hello /hello
COPY --from=builder /demo /demo

これをbuildして出来上がるイメージには本当に自分のバイナリしか入っていません。 *1

まず /hello を動かしてみると…

$ docker run --rm demo-scratch /hello
Hello from Go on scratch!

普通に動きました。では /demo はどうでしょうか。

$ docker run --rm demo-scratch /demo hello
exec /demo: no such file or directory

こちらはなにやらエラーが出て起動しません。ファイルはあるはずなのに no such file or directory と言われてしまいました。

/demo が存在しないと言われてるメッセージに見えますが、実際は /lib64/ld-linux-x86-64.so.2 が無いというエラーです。

$ file hello
ELF 64-bit LSB executable, x86-64, statically linked, ...

$ file demo
ELF 64-bit LSB executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...

fileコマンドで見てみると、 demo が動的リンクバイナリになっているのがわかります。 *2

CGO_ENABLED=0 にしてみる

まだ、ここでscratchを諦める必要はありません。 CGO_ENABLED=0 にすると net パッケージが pure Go 実装にフォールバックし、静的バイナリになります。

FROM golang:1.26 AS builder
COPY . .
RUN CGO_ENABLED=0 go build -o /demo .

FROM scratch
COPY --from=builder /demo /demo
$ docker run -d -p 8080:8080 demo-scratch-static /demo serve
$ curl http://localhost:8080/health
{"status":"ok"}

今度は FROM scratch でHTTP サーバーが動きました。

しかし HTTPS やタイムゾーン変換も試してみると…

$ docker run --rm demo-scratch-static /demo https
Error: ...x509: certificate signed by unknown authority

$ docker run --rm demo-scratch-static /demo timezone
Error: unknown time zone Asia/Tokyo

HTTPS 接続は CA 証明書がなくて失敗、タイムゾーン変換は tzdata がなくて失敗します。これではAPI呼び出し一つできません。静的バイナリであってもこういった外部のファイルを参照する場面で困ります。

そこで登場するのが distroless image というわけですね。

distroless/static にしてみる

ここでいよいよベースイメージを gcr.io/distroless/static-debian13 にしてみると、無事動いてくれるのが確認できます。

# ...
FROM gcr.io/distroless/static-debian13
COPY --from=builder /demo /demo
$ docker run --rm demo-distroless /demo https
Status: 200 OK

$ docker run --rm demo-distroless /demo timezone
UTC:   2026-04-01T09:26:52Z
Tokyo: 2026-04-02T18:26:52+09:00

distroless/static の中身

では具体的に何が入っているのか、中を開けて覗いてみます。

イメージの展開方法

イメージの中を見る便利なOSSもありますが、ここでは原始的に docker export でコンテナのファイルシステムを丸ごと取り出してみます。 *3

docker create --name tmp gcr.io/distroless/static-debian13:latest -- /nonexistent
docker export tmp | tar -xf - -C distroless-static

ファイル構成

展開結果はこんな感じです。言われている通り、シェルなどの実行可能バイナリは一切ありません。

etc/
  debian_version, group, host.conf, hostname, hosts, nsswitch.conf, passwd, ...
  mime.types                          ← 78KB
  ssl/certs/ca-certificates.crt       ← 224KB, HTTPS 用 CA 証明書バンドル
  dpkg/origins/debian

usr/
  lib/os-release
  share/
    common-licenses/                  ← GPL-3, Apache-2.0 等 (264KB)
    doc/                              ← copyright, changelog
    zoneinfo/                         ← タイムゾーンデータ (3.8MB)

var/lib/dpkg/status.d/                ← dpkg パッケージメタデータ

Alpine や素の Debian と並べてみました。

イメージ ファイル数 パッケージ数 シェル パッケージマネージャ
distroless/static 967 5 なし なし
alpine:latest 522 6 あり (busybox ash) あり (apk)
debian:trixie 5,438 78 あり (bash) あり (apt)

Alpine はファイル数こそ少ないですが、busybox(シェル + 各種ユーティリティ)と apk(パッケージマネージャ)が含まれているのに対して、distroless は「実行可能バイナリが一切ない」というのが明確な違いです。

イメージの階層構造

distroless/static を見ましたが、本当に最小限の distroless/static で動くのは静的バイナリくらいで、他にもさらに必要なパッケージのためにバリエーションが用意されています。

distroless のソースコード *4 を読むと、イメージ間の依存関係はこうなっていました。

static
├── base-nossl  (+ libc6)
│   └── java-base → java17, java21, java25
└── base        (+ libc6, libssl3)
    └── cc      (+ libstdc++6)
        ├── python3
        └── nodejs20, nodejs22, nodejs24

各層で追加されるものとサイズの推移です。

イメージ static からの追加 サイズ (compressed)
static 2.21MB
base glibc, OpenSSL 24.2MB
cc libstdc++, libgcc 27.5MB
nodejs24 Node.js バイナリ 150MB

Go や Rust のように静的バイナリを生成できる言語なら static で十分(なことが多い)な一方、実行エンジンが C/C++ 製な言語ではどうしても glibc や libstdc++ が必要になる、というわけです。

セキュリティ上の意味

攻撃面の削減

ここまで見てきたように、distroless/static にはシェルも実行可能バイナリも一切ありません。

そのため仮にアプリケーションの脆弱性を突かれてコンテナ内でコード実行される状況に陥ったとしても、攻撃者が使えるツールがほぼありません。通常のコンテナなら sh でシェルを起動し、curlwget でマルウェアをダウンロードし、chmod で実行権限を付ける…といった操作ができますが、distroless ではそもそもこれらのバイナリが存在しないことで、攻撃の難易度を大きく高めます。

脆弱性トリアージコストの削減

コンテナイメージは脆弱性スキャンを行い日々アップデートが行われていると思いますが、含まれるパッケージが少ないことでそのまま検出件数が減少し、そもそもトリアージすべき脆弱性が存在しないことで運用コストの削減になります。

実際に Trivy で各イメージをスキャンしてみると、2026年4月3日現在で static は 検知ゼロ、glibc の入った base では 12 件でした。

イメージ CVE 数 内訳
static 0
base (+ glibc, OpenSSL) 12 HIGH 1, MEDIUM 3, LOW 8
cc (+ libstdc++) 12 base と同数

Go (CGO_ENABLED=0) や Rust のように static で済む言語なら、この恩恵をそのまま受けられるのが魅力です。

Google Distroless 以外の選択肢

この記事では Google Distroless を中心に見てきましたが、 「最小限のコンテナイメージ」を目指すプロジェクトは他にも存在するので一部簡単に紹介します。

Docker Hardened Images (DHI)

Docker Hardened Images は、Docker Hub の公式イメージ (nginx, python, node 等) をシェル除去・non-root 化したハードニング版です。2025 年 12 月に Apache 2.0 ライセンスで無料化されました。

Google Distroless が「最小限から積み上げる」アプローチなのに対し、DHI は「既存イメージから削ぎ落とす」アプローチという違いがあります。このあたりも中身の違いを見てみると面白そうです。

Chainguard Images

Chainguard は、Google Distroless プロジェクトの元メンテナーが設立した企業です。独自の Linux ディストリビューション上にイメージを構築し、パッチの即時適用を実現しています。 *5

まとめ

今回は distroless コンテナイメージの中を覗いてみました。bazelでビルドされているためビルドプロセスを理解するにはbazelを理解する必要がありますが、実現していること、イメージの中身はシンプルなことがわかります。

必要な依存が足りないなどそのままでは導入できない場面もありますが、中身が見えてくると挑戦のハードルも下がるのではないでしょうか。興味が湧いた方は docker export:debug タグ版で中を覗いてみてください。

github.com

We are hiring!

エムスリーではセキュリティやDevOpsに興味のあるエンジニアを大募集しています!興味のある方はぜひカジュアル面談にお越しください!

エンジニア採用ページはこちら

jobs.m3.com

カジュアル面談もお気軽にどうぞ

jobs.m3.com

インターンも常時募集しています

open.talentio.com

*1:実際にはコンテナとして起動時には /etc/hostname, /etc/hosts, /etc/resolv.conf 等が注入されます

*2:net パッケージはデフォルトで DNS 解決に libc の getaddrinfo を使うため

*3:distroless/static には ENTRYPOINT も CMD もないため、docker create にダミーコマンドを指定する必要がある

*4:Bazel の各 .bzl ファイルの oci_image() の base フィールドで確認できます

*5:https://edu.chainguard.dev/chainguard/chainguard-images/overview/




以上の内容はhttps://www.m3tech.blog/entry/2026/04/03/180000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14