Docker 上級 #4 SBOM と署名 — サプライチェーンセキュリティの入口

#3 のセキュリティチェックリストが一コンテナの中の話だったとすれば、この記事は一段上 — サプライチェーン (supply chain) の話です。このイメージに何が入っているか、そして誰が作ったものか。

Docker 上級 シリーズでこの記事の位置:

なぜサプライチェーンセキュリティが急に大事になったか #

近年大きな事故が連続して起きました。

  • 2020 SolarWinds — 正常に署名されたビルド成果物にバックドアが入っていた
  • 2021 codecov bash uploader — 正常な道具のダウンロードスクリプトが破壊されて秘密漏洩
  • 2024 xz utils バックドア — 広く使われる圧縮道具に信頼されているメンテナがバックドアを挿入

共通点は — 脆弱性がコードの中ではなく、コードを作って配布する過程に挟まったこと です。CVE スキャンだけでは捉えられない脅威です。これを扱う道具が SBOM署名 の二軸です。

質問道具
このイメージの中に正確に何が入っているか?SBOM (Software Bill of Materials)
このイメージを本当にその人 / 組織が作ったか?署名 (cosign / sigstore)
このイメージはどんな手順を経て作られたか?attestation / SLSA

SBOM とは #

SBOM は一つのソフトウェアが 何で構成されているか を機械が読める形式で書いた仕様書です。例えると食品の栄養成分表のようなものです。

標準形式が二つあります。

形式生成元特徴
SPDXLinux Foundation最も古い標準、ライセンス情報に強い
CycloneDXOWASPセキュリティ視点、依存グラフ表現が豊富

ツールごとに両形式とも出力可能です。最初に使うときは片方を選んで一貫して使えばよいでしょう。

Syft で SBOM 生成 #

syft (Anchore) が標準に近いツールです。

インストール
brew install syft
# または
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
イメージの SBOM を作る
syft myapp:1.0 -o cyclonedx-json > sbom.cdx.json
syft myapp:1.0 -o spdx-json > sbom.spdx.json

出力例 (CycloneDX の一部):

sbom.cdx.json (要約)
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "components": [
    {
      "name": "openssl",
      "version": "3.0.13-1ubuntu0.1",
      "type": "library",
      "purl": "pkg:deb/ubuntu/openssl@3.0.13-1ubuntu0.1?arch=amd64"
    },
    {
      "name": "flask",
      "version": "3.0.3",
      "type": "library",
      "purl": "pkg:pypi/flask@3.0.3"
    }
  ]
}

各コンポーネントの purl (Package URL) が正確な識別子です。pkg:pypi/flask@3.0.3 は PyPI の flask 3.0.3 を指す標準 URL。

docker buildx が直接作る #

ビルド時に SBOM を attestation として添付できます。

ビルド時 SBOM 添付
docker buildx build \
  --sbom=true \
  --provenance=mode=max \
  -t ghcr.io/me/myapp:1.0 \
  --push .
  • --sbom=true — SBOM を作ってイメージに attestation として付ける
  • --provenance=mode=max — ビルド出所情報 (どんなコマンドでどんな入力からビルドされたか) まで含む

こうして push したイメージは manifest list に attestation manifest が一緒に入りますimagetools inspect で確認できます。

attestation を覗く
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
  --format '{{json .SBOM}}'

これが最も綺麗な道です — ビルド一回で SBOM まで一緒に作って、同じマニフェストに束ねて保管。

SBOM をどこで使うか #

SBOM 自体はただのデータです。価値は それを持ってクエリする道具 から出てきます。

脆弱性スキャン — Grype #

SBOM でスキャン
grype sbom:./sbom.cdx.json

イメージダウンロードなしに、SBOM 一ファイルだけあれば脆弱性スキャンが動きます。CI のキャッシュ / 運用環境の事後追跡にとても速い流れです。

ライセンスコンプライアンス #

ライセンス抽出
syft myapp:1.0 -o spdx-tag-value | grep PackageLicenseDeclared | sort -u

イメージにどんなオープンソースライセンスが入っているか — コンプライアンスレポートでよく使う情報です。

インシデント対応 #

新しい CVE が公開されたとき (例: log4shell、xz)、既に運用中のイメージのどこにそのライブラリが入っているか を SBOM でクエリします。CVE が落ちて一時間以内に影響範囲をレポートするのは SBOM なしには難しいです。

署名 — Cosign #

イメージが本当にその人 / 組織が作ったものかを検証する道具。Sigstore プロジェクトの Cosign が標準になりました。

インストール
brew install cosign

キーベース署名 (旧方式) #

キー生成
cosign generate-key-pair
# cosign.key, cosign.pub
署名
cosign sign --key cosign.key ghcr.io/me/myapp:1.0

イメージに署名が付いてレジストリと一緒に保存されます。

検証
cosign verify --key cosign.pub ghcr.io/me/myapp:1.0

キーを 管理しなければならない という負担があります。キーが漏洩すれば信頼が崩れます。これを解いてくれるのが次の節のキーレス (keyless) 署名。

キーレス署名 — OIDC + Fulcio + Rekor #

Sigstore の大きなアイデア。長期キーなしに OIDC 身元で一度署名し、その事実を公開透明性ログ (Rekor) に記録します。

キーレス署名 (CI で)
cosign sign ghcr.io/me/myapp:1.0
# ブラウザが開いて OIDC ログイン (または CI 環境で自動)
# Fulcio が短い寿命の証明書を発行
# それで署名
# Rekor (公開透明性ログ) に記録
キーレス検証
cosign verify \
  --certificate-identity-regexp 'https://github.com/me/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/me/myapp:1.0
  • certificate-identity-regexp — 誰が署名できるか (URL パターン)
  • certificate-oidc-issuer — どの OIDC 発行者が発行した証明書を信頼するか

GitHub Actions の上で動くビルドは OIDC トークンを自動で 受け取れるので、環境変数 / 秘密管理なしに署名ができます。運用ではほぼ常にキーレスに行く方が安全です。

GitHub Actions で — 一箇所まとめ #

.github/workflows/build-sign.yml
name: build and sign
on: { push: { branches: [main] } }

permissions:
  contents: read
  packages: write
  id-token: write    # キーレス署名に必要

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/setup-buildx-action@v3

      - id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          sbom: true               # SBOM attestation
          provenance: mode=max     # provenance attestation

      - uses: sigstore/cosign-installer@v3

      - name: Sign image
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}

このワークフロー一つで:

  1. イメージビルド + マルチアーキ (#2)
  2. SBOM + provenance attestation 添付
  3. キーレス署名 (Rekor 記録)

ほぼ全てのサプライチェーンセキュリティの流れが一ワークフローで終わります。

Attestation — SBOM と署名を束ねる #

ここまで出てきた断片を一つの絵で:

サプライチェーンメタデータ
   image
     ├── manifest (linux/amd64)
     ├── manifest (linux/arm64)
     ├── attestation: SBOM (CycloneDX)
     ├── attestation: provenance (SLSA build info)
     └── signature (cosign)

これら全てが 同じ OCI レジストリ に同じイメージのメタデータとして保存されます。imagetools inspect で一度に覗けます。

attestations を見る
docker buildx imagetools inspect ghcr.io/me/myapp:1.0 \
  --format '{{json .}}' | jq '.Manifest'

検証ポリシーは道具 / 環境ごとに違いますが、運用環境でよく見るパターン:

  1. このイメージに SBOM が添付されているか — なければ拒否
  2. このイメージが私たちの組織の OIDC 身元で署名されたか — そうでなければ拒否
  3. このイメージの SBOM に critical CVE がないか — あれば拒否

Admission control — クラスタ単で強制 #

Kubernetes のような環境では admission controller が上のポリシーを強制します。

道具役割
KyvernoYAML ポリシーでイメージ検証 — verifyImages キー
OPA GatekeeperRego 言語ベースのポリシー
Sigstore Policy Controllercosign 検証専用

Docker だけ使う単一ホスト環境ではここまで行くことは少ないですが、運用が K8s に行けば自然に出会うテーマです。

SLSA — サプライチェーン成熟度モデル #

SLSA (Supply-chain Levels for Software Artifacts) はサプライチェーンセキュリティの段階を定義したフレームワークです。

Level何が必要か
L1ビルド手順が文書化されている
L2ビルドが hosted サービスで — provenance が自動生成
L3ビルドが隔離された環境、provenance 改ざん防止
L4 (旧標準)再現可能なビルド、two-party 検証

GitHub Actions のホスト型ランナー + --provenance=mode=max + cosign キーレス署名くらいでも L3 の大部分 を満たします。深く入らなくても段階的に上の段階に上がれます。

よく出会う落とし穴 / ミス #

  • 運用イメージに SBOM だけ作って検証しない — 作った意味がありません。CI / admission controller 側で検証をゲートに組み込みます
  • キーベース署名でキー紛失 / 漏洩 — キーレスにマイグレーション
  • 複数のビルドが同じタグを上書きするlatest のようなタグは attestation の追跡を壊す。digest (@sha256:...) で検証
  • Rekor がダウンしたタイミングでビルド — キーレス署名ができません。署名を retry ロジックの中に
  • SBOM のコンポーネント出所が正確でない — 一部の静的バイナリは syft が掴めません。ビルド時 SBOM (buildx --sbom) の方が正確

まとめ #

この記事で掴んだ絵:

  • サプライチェーン脅威は コードの中ではなくコードを作る過程 から入る — CVE スキャンでは不足
  • SBOM = イメージに何が入っているかの機械可読仕様 (CycloneDX / SPDX)
  • Syft で SBOM 生成、buildx --sbom=true でビルド時に attestation 添付
  • Cosign キーレス署名 = OIDC 身元 + 短寿命証明書 (Fulcio) + 透明性ログ (Rekor) — 長期キー管理なし
  • GHA 一ワークフローでビルド → マルチアーキ → SBOM → provenance → キーレス署名まで終わり
  • 運用環境では 検証をゲートに — デジタル署名 / SBOM が単純添付に留まると効果なし
  • SLSA Level は段階的 — 一段ずつ上げる道

次の記事 (#5 リソース制限と cgroups) ではコンテナの リソース制限 に視点を変えます。cgroups v2 の基本、mem_limit / cpus の動作、OOMKilled のデバッグ、そして ulimit / pids のような別の隔離手段です。

X