ランタイムイメージの余分な1バイトは、攻撃面の1バイトです。理想的な本番イメージはアプリケーションのバイナリと数個の共有ライブラリのみを含み、シェルもパッケージマネージャーも持たず——そしてクラスタが信頼できるよう署名されているべきです。
マルチステージビルドとdistrolessベースイメージ
Goアプリケーション向けの参考パターン:
FROM golang:1.23 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /out/app ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /out/app /app USER nonroot:nonroot ENTRYPOINT ["/app"]
メリット:
- ランタイムイメージが20MB未満で、
sh、apt、curlを含まない。 - 攻撃者がRCEに到達しても、権限昇格やペイロードのダウンロードに使えるツールがない。
- OSレベルのCVE件数がほぼゼロになり、スキャンノイズが減る。
Node.js/Pythonの場合:Chainguard Images、distroless/nodejs、distroless/python3を使用。:latestは避け、必ずダイジェスト@sha256:...でピン留めしましょう。
非root、読み取り専用ファイルシステム、capabilityのドロップ
Kubernetesでは:
securityContext:
runAsNonRoot: true
runAsUser: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
これはPod Security Standards: restrictedプロファイルのベースラインでもあります。アプリがファイル書き込みを必要とする場合(キャッシュ、tmp)、ルートFSを開放するのではなく、その特定パスにemptyDirをマウントしましょう。
Trivy/Grypeによるイメージスキャン
- name: Build
run: docker build -t ghcr.io/org/app:${{ github.sha }} .
name: Scan image uses: aquasecurity/trivy-action@master with: image-ref: ghcr.io/org/app:${{ github.sha }} severity: CRITICAL,HIGH exit-code: 1 ignore-unfixed: true
ignore-unfixed: trueは、修正がないCVEでビルドが失敗するのを防ぎますが、追跡のためレポートには記録されます。
Cosignキーレス:秘密鍵を管理せずに署名する
Cosignキーレスは、OIDCアイデンティティ(GitHub Actions、Googleなど)とFulcioが発行する短命証明書に依存します。ワークフロー:
permissions:
id-token: write # OIDC用
contents: read
packages: write
-
uses: sigstore/cosign-installer@v3
-
name: Sign image env: COSIGN_EXPERIMENTAL: "true" run: cosign sign --yes ghcr.io/org/app@${{ steps.push.outputs.digest }}
name: Attach SBOM as attestation run: | syft ghcr.io/org/app@${{ steps.push.outputs.digest }} -o cyclonedx-json > sbom.json cosign attest --yes --predicate sbom.json --type cyclonedx
ghcr.io/org/app@${{ steps.push.outputs.digest }}
検証ログはRekor透明性ログに永続的に保存されます。保管・ローテーションが必要な秘密鍵はありません。
Kyvernoによるデプロイ前検証
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
rules:
- name: check-cosign-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["ghcr.io/org/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
Podは、正しいリポジトリ + ワークフロー + ブランチからの署名を持つイメージである場合のみ作成されます。これはサプライチェーンセキュリティ(SLSA L2-L3)の重要なピースです。
本番イメージのチェックリスト
- マルチステージ、distroless/Chainguardベースイメージ、ダイジェストピン留め。
- USERは非root、ALL capabilityをドロップ、readOnlyRootFilesystem。
- ランタイムステージに
curl、wget、bashをインストールしない。 - Trivy/Grypeでスキャンし、修正があるCRITICAL/HIGHではビルドを失敗させる。
- Cosignキーレスで署名し、CycloneDX SBOMを添付。
- クラスタにKyverno verifyImagesを設定し、署名なしイメージをブロック。
- HEALTHCHECKとEXPOSEを明示し、未使用ポートを公開しない。
結論
イメージのハードニングは、DevSecOpsで最もROIが高い投資の1つです:労力は少なく、攻撃面とアラート疲労の両方を削減します。distroless + 非root + Cosignキーレスのパターンに慣れれば、新サービスがほぼ無償で良いベースラインを得られます——そして、コンプライアンス違反のPodをクラスタ全体でブロックするアドミッションポリシーを自信を持って有効化できます。
