Goコンテナセキュリティスキャン:Trivy SBOMサプライチェーンセキュリティの6つの重要プラクティス
サプライチェーン攻撃がイメージ脆弱性に出会う時:コンテナセキュリティの至暗の刻
深夜3時、アラートが鳴り止まない。本番コンテナにマイニングマルウェアが植入され、CPU使用率が99%に急上昇。調査の結果:ベースイメージgolang:1.22-alpineに未修正のCVE-2024-1234が存在し、攻撃者が依存関係の連鎖を通じて悪意あるコードを注入。CI/CDにはセキュリティゲートが一切ない。さらに悪いことに、イメージに何が含まれているのか全く分からない——SBOMもなく、署名検証もなく、コンプライアンス監査に何も答えられない。
これは仮定の話ではない。2024年のXZ Utilsバックドア事件、Log4Shell脆弱性、SolarWindsサプライチェーン攻撃はすべて、コンテナセキュリティはオプションではなく生存のベースラインであることを証明している。脆弱性スキャンでリスクを発見し、SBOMで依存関係を透明化し、イメージ署名で信頼性を担保し、CI/CDゲートで脅威を遮断する必要がある。本記事では6つの重要プラクティスを通じて、Goコンテナサプライチェーンセキュリティの防衛線を構築する。
コア概念クイックリファレンス
| 概念 | コア思想 | 主要ツール | 典型的なシナリオ |
|---|---|---|---|
| コンテナセキュリティ | ビルドからランタイムまでのライフサイクル全体の防護 | Trivy, Clair | イメージ脆弱性スキャン、ランタイム防護 |
| SBOM(ソフトウェア部品表) | ソフトウェアの全コンポーネントと依存関係を列挙 | syft, trivy sbom |
依存関係の透明化、コンプライアンス監査 |
| CVE(共通脆弱性識別子) | 標準化された脆弱性識別と評価 | NVD, OSV | 脆弱性追跡と修正優先度 |
| Trivy | オールインワンコンテナセキュリティスキャナー | trivy |
イメージ/ファイルシステム/Gitリポジトリスキャン |
| Cosign | コンテナイメージの署名と検証 | cosign |
イメージの来歴信頼性検証 |
| SLSA(サプライチェーンレベルフレームワーク) | ソフトウェアサプライチェーンの完全性保障フレームワーク | SLSA仕様 | ビルドプロセスの信頼性保障 |
| Sigstore | 無料オープンソースのコード署名プラットフォーム | cosign, rekor |
イメージ/アーティファクトの署名と監査 |
| イメージ署名 | 鍵ペアでイメージダイジェストに署名 | cosign sign |
イメージ改ざん防止 |
| 脆弱性重大度 | CRITICAL > HIGH > MEDIUM > LOW | CVSSスコア | 修正優先度の決定 |
| CISベンチマーク | コンテナ/オーケストレーションのセキュリティ設定標準 | trivy misconfig |
Docker/K8sセキュリティ強化 |
目次
- 問題分析:5つのコンテナセキュリティ課題
- プラクティス1:Trivyイメージ脆弱性スキャン統合
- プラクティス2:SBOM生成と検証
- プラクティス3:Cosignイメージ署名と検証
- プラクティス4:CI/CDセキュリティゲート設定
- プラクティス5:Goモジュール依存監査
- プラクティス6:ランタイムセキュリティ監視
- 5つのよくある落とし穴
- 10のエラートラブルシューティング
- 高度な最適化のヒント
- スキャンツール比較
- オンラインツール推奨
- まとめと展望
問題分析:5つのコンテナセキュリティ課題
課題1:イメージ脆弱性修正の遅れ — Goベースイメージgolang:1.22には数百のシステムパッケージが含まれ、CVEの公開から修正まで平均97日かかる。イメージが既知の脆弱性を抱えたまま数ヶ月稼働する可能性がある。
課題2:推移的依存関係のリスク — go mod tidyが取得する間接依存に悪意あるコードが含まれる可能性がある。XZ Utils事件は、広く使用されているライブラリでさえバックドアを仕込まれる可能性を証明した。go.sumは完全性は検証するが、安全性は検証しない。
課題3:イメージ署名検証の欠如 — 署名検証がなければ、誰でも改ざんされたイメージをレジストリにプッシュできる。K8sはデフォルトでイメージの来歴を検証せず、取得したものをそのまま実行する。
課題4:SBOMの生成と管理 — SBOMがなければ「このイメージに何が入っているか」に答えられない。コンプライアンス監査でSBOMが要求されるが、ほとんどのチームは一度も生成したことがない。
課題5:コンプライアンス監査の自動化 — SOC2や同等のフレームワークは継続的なセキュリティスキャン記録を要求する。手動監査は非効率で見落としが発生しやすい。
プラクティス1:Trivyイメージ脆弱性スキャン統合
インストールと基本スキャン
# Trivyのインストール
brew install trivy
# またはDockerを使用
docker run aquasec/trivy:latest version
Goコンテナイメージのスキャン
# ローカルイメージのスキャン
trivy image myapp:latest
# CRITICALとHIGH重大度のみ表示
trivy image --severity CRITICAL,HIGH myapp:latest
# 統合用JSON形式で出力
trivy image --format json --output result.json myapp:latest
# 特定アーキテクチャのスキャン
trivy image --platform linux/amd64 myapp:latest
Goプロジェクト統合スキャン
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
)
type TrivyReport struct {
Results []TrivyResult `json:"Results"`
}
type TrivyResult struct {
Target string `json:"Target"`
Type string `json:"Type"`
Vulnerabilities []Vuln `json:"Vulnerabilities"`
}
type Vuln struct {
VulnerabilityID string `json:"VulnerabilityID"`
Severity string `json:"Severity"`
PkgName string `json:"PkgName"`
InstalledVersion string `json:"InstalledVersion"`
FixedVersion string `json:"FixedVersion"`
Title string `json:"Title"`
}
func scanImage(imageRef string) (*TrivyReport, error) {
cmd := exec.Command("trivy", "image",
"--format", "json",
"--severity", "CRITICAL,HIGH",
imageRef,
)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("trivy scan failed: %w", err)
}
var report TrivyReport
if err := json.Unmarshal(output, &report); err != nil {
return nil, fmt.Errorf("parse report failed: %w", err)
}
return &report, nil
}
func main() {
imageRef := os.Getenv("SCAN_IMAGE")
if imageRef == "" {
imageRef = "myapp:latest"
}
report, err := scanImage(imageRef)
if err != nil {
fmt.Fprintf(os.Stderr, "scan error: %v\n", err)
os.Exit(1)
}
criticalCount := 0
for _, result := range report.Results {
for _, v := range result.Vulnerabilities {
if v.Severity == "CRITICAL" {
criticalCount++
fmt.Printf("[CRITICAL] %s: %s (%s -> %s)\n",
v.VulnerabilityID, v.Title,
v.InstalledVersion, v.FixedVersion)
}
}
}
if criticalCount > 0 {
fmt.Printf("\nFound %d CRITICAL vulnerabilities. Blocking deploy.\n", criticalCount)
os.Exit(1)
}
fmt.Println("No CRITICAL vulnerabilities found. Safe to deploy.")
}
Trivy設定ファイル
# trivy.yaml
severity:
- CRITICAL
- HIGH
skipDirs:
- /tmp
- /var/cache
ignorefile: .trivyignore
db:
skipUpdate: false
vulnerability:
type:
- os
- library
misconfiguration:
type:
- dockerfile
- kubernetes
プラクティス2:SBOM生成と検証
TrivyでSBOMを生成
# SPDX形式のSBOMを生成
trivy image --format spdx-json --output sbom.spdx.json myapp:latest
# CycloneDX形式のSBOMを生成
trivy image --format cyclonedx --output sbom.cyclonedx.json myapp:latest
SyftでSBOMを生成
# Syftのインストール
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# SBOMの生成
syft myapp:latest -o spdx-json > sbom.spdx.json
syft myapp:latest -o cyclonedx-json > sbom.cyclonedx.json
GoでのプログラマティックSBOM管理
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
)
type SBOMMetadata struct {
ImageRef string `json:"imageRef"`
GeneratedAt time.Time `json:"generatedAt"`
Tool string `json:"tool"`
Format string `json:"format"`
ComponentCount int `json:"componentCount"`
}
func generateSBOM(imageRef, format, outputPath string) error {
args := []string{"image", "--format", format, "--output", outputPath, imageRef}
cmd := exec.Command("trivy", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func validateSBOM(sbomPath string) error {
data, err := os.ReadFile(sbomPath)
if err != nil {
return fmt.Errorf("read sbom failed: %w", err)
}
if !json.Valid(data) {
return fmt.Errorf("invalid SBOM JSON format")
}
var sbom map[string]interface{}
if err := json.Unmarshal(data, &sbom); err != nil {
return fmt.Errorf("parse SBOM failed: %w", err)
}
if _, ok := sbom["components"]; !ok {
if _, ok2 := sbom["packages"]; !ok2 {
return fmt.Errorf("SBOM missing components/packages field")
}
}
fmt.Printf("SBOM validation passed: %s\n", sbomPath)
return nil
}
func main() {
imageRef := "myapp:latest"
sbomPath := fmt.Sprintf("sbom-%s.json", time.Now().Format("20060102"))
fmt.Printf("Generating SBOM for %s...\n", imageRef)
if err := generateSBOM(imageRef, "cyclonedx", sbomPath); err != nil {
fmt.Fprintf(os.Stderr, "generate SBOM failed: %v\n", err)
os.Exit(1)
}
if err := validateSBOM(sbomPath); err != nil {
fmt.Fprintf(os.Stderr, "validate SBOM failed: %v\n", err)
os.Exit(1)
}
meta := SBOMMetadata{
ImageRef: imageRef,
GeneratedAt: time.Now(),
Tool: "trivy",
Format: "cyclonedx",
ComponentCount: 0,
}
metaData, _ := json.MarshalIndent(meta, "", " ")
fmt.Println(string(metaData))
}
プラクティス3:Cosignイメージ署名と検証
鍵ペアの生成と署名
# 鍵ペアの生成
cosign generate-key-pair
# イメージの署名(秘密鍵を使用)
cosign sign --key cosign.key myregistry/myapp:latest
# 署名の検証(公開鍵を使用)
cosign verify --key cosign.pub myregistry/myapp:latest
# Keyless署名(Sigstore)
cosign sign myregistry/myapp:latest
# Keyless検証
cosign verify myregistry/myapp:latest
Goイメージ検証統合
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
func verifyImageSignature(imageRef, publicKey string) error {
args := []string{"verify", "--key", publicKey, imageRef}
cmd := exec.Command("cosign", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("signature verification failed: %s", string(output))
}
if !strings.Contains(string(output), "Verified OK") {
return fmt.Errorf("signature not verified for %s", imageRef)
}
fmt.Printf("Image verified: %s\n", imageRef)
return nil
}
func signImage(imageRef, keyPath string) error {
args := []string{"sign", "--key", keyPath, imageRef}
cmd := exec.Command("cosign", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func main() {
action := os.Args[1]
imageRef := os.Args[2]
switch action {
case "sign":
if err := signImage(imageRef, "cosign.key"); err != nil {
fmt.Fprintf(os.Stderr, "sign failed: %v\n", err)
os.Exit(1)
}
case "verify":
if err := verifyImageSignature(imageRef, "cosign.pub"); err != nil {
fmt.Fprintf(os.Stderr, "verify failed: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "unknown action: %s\n", action)
os.Exit(1)
}
}
K8s Admission Controller検証ポリシー
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: verify-image-signatures
spec:
module: registry://ghcr.io/kubewarden/policies/verify-image-signatures:v0.2.5
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
settings:
signatures:
- image: "myregistry.io/*"
pubKeys:
- |
-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----
プラクティス4:CI/CDセキュリティゲート設定
GitHub Actionsセキュリティパイプライン
name: Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: "myapp:${{ github.sha }}"
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: "1"
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: "myapp:${{ github.sha }}"
format: cyclonedx-json
output-file: sbom.json
- name: Sign Image
run: |
cosign sign --key env://COSIGN_PRIVATE_KEY myapp:${{ github.sha }}
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
- name: Go Module Audit
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Upload Trivy Results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
GitLab CIセキュリティゲート
stages:
- build
- scan
- sign
- deploy
build-image:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
trivy-scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- trivy image --format cyclonedx --output sbom.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
paths:
- sbom.json
cosign-verify:
stage: sign
script:
- cosign sign --key cosign.key $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
deploy:
stage: deploy
script:
- cosign verify --key cosign.pub $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
プラクティス5:Goモジュール依存監査
govulncheckディープスキャン
# govulncheckのインストール
go install golang.org/x/vuln/cmd/govulncheck@latest
# 現在のプロジェクトをスキャン
govulncheck ./...
# JSON形式で出力
govulncheck -json ./...
# 呼び出された脆弱性のみ表示(より正確)
govulncheck -mode binary ./...
Go依存監査プログラム
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
type VulnEntry struct {
OSV string `json:"osv"`
Module string `json:"module"`
Version string `json:"version"`
Package string `json:"package"`
Call string `json:"call"`
Severity string `json:"severity"`
}
func auditGoModules(projectPath string) ([]VulnEntry, error) {
cmd := exec.Command("govulncheck", "-json", "./...")
cmd.Dir = projectPath
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("govulncheck failed: %w", err)
}
var vulns []VulnEntry
decoder := json.NewDecoder(strings.NewReader(string(output)))
for decoder.More() {
var entry map[string]interface{}
if err := decoder.Decode(&entry); err != nil {
continue
}
if vulnData, ok := entry["vulnerability"]; ok {
v := VulnEntry{}
if m, ok := vulnData.(map[string]interface{}); ok {
if id, ok := m["id"].(string); ok {
v.OSV = id
}
if mod, ok := m["module"].(string); ok {
v.Module = mod
}
}
vulns = append(vulns, v)
}
}
return vulns, nil
}
func checkGoModTidy(projectPath string) error {
cmd := exec.Command("go", "mod", "tidy", "-diff")
cmd.Dir = projectPath
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("go.mod needs tidy: %s", string(output))
}
return nil
}
func main() {
projectPath := "."
if len(os.Args) > 1 {
projectPath = os.Args[1]
}
fmt.Println("Checking go.mod tidiness...")
if err := checkGoModTidy(projectPath); err != nil {
fmt.Fprintf(os.Stderr, "[WARN] %v\n", err)
}
fmt.Println("Running govulncheck...")
vulns, err := auditGoModules(projectPath)
if err != nil {
fmt.Fprintf(os.Stderr, "audit failed: %v\n", err)
os.Exit(1)
}
if len(vulns) > 0 {
fmt.Printf("Found %d vulnerabilities:\n", len(vulns))
for _, v := range vulns {
fmt.Printf(" - %s: %s@%s\n", v.OSV, v.Module, v.Version)
}
os.Exit(1)
}
fmt.Println("No known vulnerabilities found.")
}
go.modセキュリティ設定
// go.mod
module github.com/myorg/myapp
go 1.22.0
require (
github.com/gin-gonic/gin v1.9.1
github.com/redis/go-redis/v9 v9.5.1
)
// 既知の脆弱バージョンを除外
exclude (
github.com/example/vulnerable v1.0.0
)
// 安全なバージョンへの置き換えを指定
replace github.com/example/vulnerable => github.com/example/vulnerable v1.0.1
プラクティス6:ランタイムセキュリティ監視
Falcoランタイム検出ルール
# falco-rules/go-container.yaml
- rule: Unexpected Process in Go Container
desc: Detect unexpected processes running in Go containers
condition: >
container and container.image contains "myapp" and
proc.name not in (myapp, sh, cat, ls)
output: >
Unexpected process in Go container
(user=%user.name container=%container.name
image=%container.image.proc=%proc.name)
priority: WARNING
tags: [container, go, runtime]
- rule: Crypto Mining Detected
desc: Detect crypto mining activity in containers
condition: >
container and
(proc.name in (xmrig, minerd, cpuminer) or
proc.cmdline contains "stratum+tcp")
output: >
Crypto mining detected
(user=%user.name container=%container.name
image=%container.image.proc=%proc.name)
priority: CRITICAL
tags: [crypto, container, runtime]
- rule: Container Drift Detected
desc: Detect new files created in running container
condition: >
container and evt.type = openat and
evt.arg.flags contains O_CREAT and
not fd.name startswith /tmp and
not fd.name startswith /var/log
output: >
Container drift detected
(user=%user.name container=%container.name
file=%fd.name)
priority: WARNING
tags: [drift, container, runtime]
Goランタイムセキュリティチェック
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
type SecurityCheck struct {
Name string
Status string
Detail string
}
func checkRunningAsRoot() SecurityCheck {
if os.Getuid() == 0 {
return SecurityCheck{
Name: "Root User Check",
Status: "FAIL",
Detail: "Container running as root",
}
}
return SecurityCheck{
Name: "Root User Check",
Status: "PASS",
Detail: fmt.Sprintf("Running as UID %d", os.Getuid()),
}
}
func checkReadOnlyFilesystem() SecurityCheck {
if err := os.WriteFile("/tmp/.security-test", []byte("test"), 0644); err != nil {
return SecurityCheck{
Name: "Read-Only FS Check",
Status: "PASS",
Detail: "Filesystem is read-only",
}
}
os.Remove("/tmp/.security-test")
return SecurityCheck{
Name: "Read-Only FS Check",
Status: "WARN",
Detail: "Filesystem is writable",
}
}
func checkDistroless() SecurityCheck {
_, err := exec.LookPath("sh")
if err != nil {
return SecurityCheck{
Name: "Distroless Check",
Status: "PASS",
Detail: "No shell available (distroless)",
}
}
return SecurityCheck{
Name: "Distroless Check",
Status: "WARN",
Detail: "Shell available in image",
}
}
func runSecurityAudit() []SecurityCheck {
return []SecurityCheck{
checkRunningAsRoot(),
checkReadOnlyFilesystem(),
checkDistroless(),
}
}
func main() {
fmt.Printf("Runtime Security Audit - %s\n", time.Now().Format(time.RFC3339))
fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Println(strings.Repeat("-", 60))
checks := runSecurityAudit()
failCount := 0
for _, c := range checks {
status := c.Status
if status == "FAIL" {
failCount++
}
fmt.Printf("[%s] %s: %s\n", status, c.Name, c.Detail)
}
fmt.Println(strings.Repeat("-", 60))
if failCount > 0 {
fmt.Printf("Audit FAILED: %d checks failed\n", failCount)
os.Exit(1)
}
fmt.Println("Audit PASSED")
}
セキュリティ強化Dockerfile
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 -ldflags="-s -w" -o /myapp .
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /myapp /myapp
USER nonroot:nonroot
HEALTHCHECK --interval=30s --timeout=3s \
CMD ["/myapp", "healthcheck"]
ENTRYPOINT ["/myapp"]
5つのよくある落とし穴
| # | 落とし穴 | 誤ったアプローチ | 正しいアプローチ |
|---|---|---|---|
| 1 | ❌ スキャンしてもブロックしない | スキャン結果をアーカイブするだけでCI/CDがデプロイを継続 | ✅ --exit-code 1でCRITICAL脆弱性のあるデプロイをブロック |
| 2 | ❌ 間接依存を無視 | go.modの直接依存のみチェック |
✅ govulncheck ./...で完全な依存ツリーをスキャン |
| 3 | ❌ latestタグを使用 | FROM golang:latestは再現不可能 |
✅ バージョンを固定:FROM golang:1.22.3-alpine3.19 |
| 4 | ❌ コンテナをrootで実行 | Dockerfileのデフォルトがrootユーザー | ✅ USER nonroot:nonroot + distrolessベースイメージ |
| 5 | ❌ 署名とデプロイを分離 | 署名後に検証せずデプロイ | ✅ K8s Admission Controllerで署名検証を強制 |
10のエラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決策 |
|---|---|---|---|
| 1 | FATAL: error in DB download |
Trivy脆弱性DBのダウンロード失敗 | trivy image --download-db-only後にリトライ;HTTP_PROXYを設定 |
| 2 | unsupported format: cyclonedx |
Trivyバージョンが古い | v0.40+にアップグレード:brew upgrade trivy |
| 3 | cosign: signing failed: KEY_REF |
鍵の環境変数が未設定 | export COSIGN_PRIVATE_KEY=...または--key cosign.keyを使用 |
| 4 | verification failed: no signatures |
イメージが未署名 | 先にcosign signで署名してから検証 |
| 5 | govulncheck: module not found |
Goモジュールキャッシュの破損 | go clean -modcache && go mod download |
| 6 | trivy: permission denied |
Dockerソケットの権限不足 | sudo usermod -aG docker $USERまたはrootlessモードを使用 |
| 7 | SBOM: empty components |
マルチステージビルドでbuilderステージをスキャン | 最終イメージをスキャン(builderステージではない) |
| 8 | cosign verify: REKOR error |
Rekorトランスペアレンシーログに到達不可 | ネットワークを確認;または--insecure-ignore-tlogを使用(テストのみ) |
| 9 | trivy: image not found locally |
ローカルにイメージが存在しない | 先にdocker pullまたはリモートレジストリを指定 |
| 10 | Falco: rule syntax error |
YAMLのインデントまたはフィールド名のエラー | falco -Vでルールファイルの構文を検証 |
高度な最適化のヒント
1. Trivy OperatorでK8sクラスタ全体の自動スキャン — Trivy OperatorをK8sクラスタにデプロイし、全Podイメージを自動スキャンしてVulnerabilityReportを生成。Prometheusアラートと組み合わせて継続的監視を実現。
2. SBOM Attestationでイメージに付加 — cosign attestを使用してSBOMをイメージマニフェストのアタッチメントとして付加。SBOMとイメージをバインドして配布し、SBOMの紛失を防止。
3. VEXドキュメントで誤検知を抑制 — VEX(Vulnerability Exploitability eXchange)ドキュメントを生成し、影響なしと評価された脆弱性をマーク。スキャンノイズを減らし、真のリスクに集中。
4. マルチアーキテクチャイメージの統一署名 — cosign signで個別マニフェストではなくマニフェストリストに署名し、linux/amd64とlinux/arm64の両アーキテクチャで統一的に検証。
5. SLSA Level 3のビルド保証 — CI/CDでSigstore署名 + Rekorトランスペアレンシーログ + SLSA Provenanceを使用し、SLSA Level 3を達成。監査可能で検証可能なビルドプロセスを確保。
スキャンツール比較
| 機能 | Trivy | Grype | Clair | Snyk |
|---|---|---|---|---|
| スキャン速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 脆弱性DBカバレッジ | 包括的(OS+言語) | 良好 | OS層中心 | 包括的 |
| SBOM生成 | ✅ SPDX/CycloneDX | ❌ Syftが必要 | ❌ | ✅ |
| イメージ署名 | ❌ Cosignと組み合わせ | ❌ | ❌ | ❌ |
| IaCスキャン | ✅ | ❌ | ❌ | ✅ |
| シークレット検出 | ✅ | ❌ | ❌ | ✅ |
| CI/CD統合 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| オープンソース無料 | ✅ Apache 2.0 | ✅ Apache 2.0 | ✅ Apache 2.0 | ❌ 商用 |
| K8s Operator | ✅ | ❌ | ✅ | ✅ |
| Goモジュール対応 | ✅ | ✅ | ❌ | ✅ |
| 推奨用途 | オールラウンド首选 | 軽量スキャン | K8sクラスタレベル | エンタープライズコンプライアンス |
選定推奨:中小チームはTrivy(オープンソース無料、機能包括)を首选。大規模K8sクラスタはTrivy Operator + Clairの組み合わせを検討。エンタープライズコンプライアンス要件ではSnykを評価。
オンラインツール推奨
| ツール | 用途 | リンク |
|---|---|---|
| JSONフォーマッター | SBOM/スキャン結果のフォーマット表示 | /ja/json/format |
| ハッシュ計算 | イメージダイジェスト/ファイル整合性検証 | /ja/encode/hash |
| Curl→コード変換 | セキュリティスキャンAPI呼び出し生成 | /ja/dev/curl-to-code |
| Base64エンコード/デコード | 鍵/証明書のエンコード/デコード | /ja/encode/base64 |
| 正規表現テスター | Falcoルール/ログマッチングのデバッグ | /ja/dev/regex |
まとめと展望
コンテナセキュリティスキャンは一度きりのアクションではなく、ビルドからランタイムまでのライフサイクル全体のプラクティスである。6つの重要プラクティスの中核ロジック:Trivyスキャンで脆弱性を発見 → SBOMで依存関係を透明化 → Cosignでイメージの信頼性を担保 → CI/CDゲートで自動ブロック → govulncheckでGo依存を監査 → Falcoでランタイム異常を監視。
2026年のトレンド:SLSAフレームワークがサプライチェーンセキュリティのデファクトスタンダードになりつつあり、Sigstore Keyless署名が署名のハードルを下げ、SBOM Attestationが部品表とイメージをバインドし、AIによる脆弱性修正優先度付けが実用化している。コンテナセキュリティは「事後対応」から「シフトレフト組み込み」へと移行しており、セキュリティプラクティスを早く統合するほど修正コストは低くなる。
覚えておこう:セキュリティはデリバリーを阻む壁ではなく、デリバリーの品質を保証する門である。Trivyスキャンから始めて、段階的にコンテナセキュリティの防衛線を構築しよう。
ブラウザローカルツールを無料で試す →