Container Image Security
Supported Versions: Trivy 0.56+, Kubernetes 1.31, 1.32, 1.33 最終更新: February 22, 2026
Container image security は、Kubernetes security における第一の防御線です。このドキュメントでは、image scanning、signing、verification、および supply chain security を扱います。
Table of Contents
- Image Scanning Overview
- Trivy
- Amazon ECR Image Scanning
- Image Signing with Cosign/Sigstore
- Image Verification in Admission Control
- Supply Chain Security
- Base Image Selection
- Image Registry Best Practices
- CI/CD Pipeline Integration
Image Scanning Overview
Shift-Left Security
Image security は、開発プロセスの早い段階から開始する必要があります。
┌─────────────────────────────────────────────────────────────────────────┐
│ Shift-Left Image Security │
│ │
│ Development Phase Build Phase Runtime Phase │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ IDE │ │ CI/CD │ │ K8s │ │
│ │ Scanning│──────────────▶│ Scanning│──────────────▶│ Scanning│ │
│ │ │ │ │ │ │ │
│ │ • Local │ │ • Build │ │ • Runtime│ │
│ │ scan │ │ gate │ │ monitor│ │
│ │ • Deps │ │ • Regis-│ │ • Policy │ │
│ │ check │ │ try │ │ enforce│ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ◀─────────────────── Cost & Ease of Fix ──────────────────────▶ │
│ Low High │
└─────────────────────────────────────────────────────────────────────────┘Scan Targets
| Target | Description | Tools |
|---|---|---|
| OS Packages | Operating system package vulnerabilities | Trivy, Grype, Clair |
| Language Dependencies | npm, pip, go modules, etc. | Trivy, Snyk |
| Misconfigurations | Dockerfile, K8s manifests | Trivy, Checkov |
| Secrets | Hardcoded sensitive information | Trivy, Trufflehog |
| Licenses | Open source licenses | Trivy, FOSSA |
Trivy
Trivy Overview
Trivy は、containers、filesystems、Git repositories などに対応する包括的な security scanner です。
Trivy Installation
bash
# macOS
brew install trivy
# Linux (Debian/Ubuntu)
sudo apt-get install wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
# Docker
docker pull aquasec/trivy:latestImage Scanning
bash
# Basic image scan
trivy image nginx:latest
# Severity filtering
trivy image --severity HIGH,CRITICAL nginx:latest
# JSON output
trivy image --format json --output results.json nginx:latest
# SARIF format (GitHub Security integration)
trivy image --format sarif --output results.sarif nginx:latest
# Ignore unfixed vulnerabilities
trivy image --ignore-unfixed nginx:latest
# Include secret scanning
trivy image --scanners vuln,secret nginx:latestFilesystem Scanning
bash
# Scan project directory
trivy fs --scanners vuln,secret,config .
# Scan Dockerfile
trivy config Dockerfile
# Scan Kubernetes manifests
trivy config ./k8s/
# Scan Helm charts
trivy config ./charts/my-app/Trivy Configuration File
yaml
# trivy.yaml
severity:
- HIGH
- CRITICAL
scan:
scanners:
- vuln
- secret
- config
vulnerability:
ignore-unfixed: true
type:
- os
- library
secret:
config: /path/to/secret-config.yaml
misconfiguration:
helm:
values:
- values.yaml
ignore:
- CVE-2023-12345 # Ignore specific CVE
- secret-rule-id # Ignore specific secret ruleTrivy Operator (Kubernetes Integration)
bash
# Install Trivy Operator
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set trivy.ignoreUnfixed=trueyaml
# Check VulnerabilityReport
apiVersion: aquasecurity.github.io/v1alpha1
kind: VulnerabilityReport
metadata:
name: deployment-nginx-nginx
namespace: default
spec:
# Auto-generated by Trivy Operator
report:
scanner:
name: Trivy
version: 0.56.0
summary:
criticalCount: 2
highCount: 5
mediumCount: 10
lowCount: 15
vulnerabilities:
- vulnerabilityID: CVE-2024-12345
severity: CRITICAL
title: "Buffer overflow in libssl"
installedVersion: "1.1.1"
fixedVersion: "1.1.2"Amazon ECR Image Scanning
Basic Scanning vs Enhanced Scanning
| Feature | Basic Scanning | Enhanced Scanning (Inspector) |
|---|---|---|
| Scan Engine | Clair | Amazon Inspector |
| Scan Frequency | On push | Continuous scan |
| Vulnerability DB | CVE | CVE + Amazon threat intelligence |
| Cost | Free | Inspector pricing |
| Language Packages | Limited | Broad support |
| AWS Integration | Basic | Security Hub, EventBridge |
Enabling Enhanced Scanning
bash
# Configure registry scanning
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[
{
"repositoryFilters": [
{"filter": "production/*", "filterType": "WILDCARD"}
],
"scanFrequency": "CONTINUOUS_SCAN"
},
{
"repositoryFilters": [
{"filter": "development/*", "filterType": "WILDCARD"}
],
"scanFrequency": "SCAN_ON_PUSH"
}
]'Retrieving Scan Results
bash
# Get scan results
aws ecr describe-image-scan-findings \
--repository-name my-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findings[?severity==`CRITICAL`]'
# Vulnerability summary
aws ecr describe-image-scan-findings \
--repository-name my-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findingSeverityCounts'Notifications via EventBridge
yaml
# CloudFormation/SAM template
Resources:
ECRScanRule:
Type: AWS::Events::Rule
Properties:
Name: ecr-scan-findings
EventPattern:
source:
- aws.ecr
detail-type:
- ECR Image Scan
detail:
scan-status:
- COMPLETE
finding-severity-counts:
CRITICAL:
- exists: true
Targets:
- Id: SNSTarget
Arn: !Ref AlertTopic
InputTransformer:
InputPathsMap:
repo: "$.detail.repository-name"
tag: "$.detail.image-tags[0]"
critical: "$.detail.finding-severity-counts.CRITICAL"
InputTemplate: |
"ECR Scan Alert: Found <critical> CRITICAL vulnerabilities in <repo>:<tag>"Image Signing with Cosign/Sigstore
Cosign Overview
Cosign は Sigstore project の一部であり、container images の signing と verification を行う tool です。
┌─────────────────────────────────────────────────────────────────────────┐
│ Cosign Image Signing Workflow │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Build │────▶│ Sign │────▶│ Push to Registry │ │
│ │ Image │ │ (Cosign) │ │ (Image + Signature) │ │
│ └─────────────┘ └─────────────┘ └───────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Container Registry │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Image │ │ Signature │ │ │
│ │ │ sha256:abc... │ │ sha256:xyz... │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Kubernetes Cluster │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Admission │────▶│ Verify │────▶│ Deploy │ │ │
│ │ │ Controller │ │ Signature │ │ Pod │ │ │
│ │ │ (Kyverno) │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘Cosign Installation
bash
# macOS
brew install cosign
# Linux
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Version check
cosign versionKey-Based Signing
bash
# Generate key pair
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
# Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0Keyless Signing (OIDC-based)
bash
# Keyless signing in GitHub Actions
# GITHUB_TOKEN automatically provides OIDC token
cosign sign myregistry.io/myapp:v1.0.0
# Verify keyless signature
cosign verify \
--certificate-identity=https://github.com/myorg/myrepo/.github/workflows/build.yaml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
myregistry.io/myapp:v1.0.0GitHub Actions Integration
yaml
name: Build, Sign and Push
on:
push:
branches: [main]
jobs:
build-sign-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
steps:
- uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
id: build
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign Image (Keyless)
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}
- name: Verify Signature
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/${{ github.repository }}:${{ github.sha }}Image Verification in Admission Control
Kyverno imageVerify
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/*/.github/workflows/*@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
---
# Key-based verification
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-key
spec:
validationFailureAction: Enforce
rules:
- name: verify-key-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.io/*"
attestors:
- count: 1
entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----Connaisseur
yaml
# Connaisseur installation
helm repo add connaisseur https://sse-secure-systems.github.io/connaisseur/charts
helm install connaisseur connaisseur/connaisseur \
-n connaisseur \
--create-namespaceyaml
# Connaisseur configuration
# values.yaml
validators:
- name: cosign
type: cosign
trustRoots:
- name: production
key: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
policy:
- pattern: "ghcr.io/myorg/*"
validator: cosign
with:
trustRoot: production
- pattern: "*"
validator: deny # Deny other imagesSupply Chain Security
SBOM (Software Bill of Materials) Generation
bash
# Generate SBOM with Syft
syft myregistry.io/myapp:latest -o spdx-json > sbom.spdx.json
syft myregistry.io/myapp:latest -o cyclonedx-json > sbom.cyclonedx.json
# Generate SBOM with Trivy
trivy image --format spdx-json --output sbom.json myregistry.io/myapp:latest
# Attach SBOM to image (Cosign)
cosign attach sbom --sbom sbom.spdx.json myregistry.io/myapp:latestSBOM-based Vulnerability Scanning
bash
# Scan SBOM for vulnerabilities
trivy sbom sbom.spdx.json
# Scan SBOM with Grype
grype sbom:sbom.spdx.jsonSLSA (Supply chain Levels for Software Artifacts)
yaml
# Generate SLSA Provenance in GitHub Actions
name: SLSA Build
on:
push:
branches: [main]
jobs:
build:
outputs:
digest: ${{ steps.build.outputs.digest }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
provenance:
needs: build
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.build.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}Base Image Selection
Image Type Comparison
| Image Type | Size | Vulnerabilities | Debugging | Use Case |
|---|---|---|---|---|
| distroless | Very small | Very few | Difficult | Production |
| Alpine | Small (~5MB) | Few | Possible | Lightweight apps |
| Chainguard | Small | Very few | Limited | Security-focused |
| Ubuntu/Debian | Large | Many | Easy | Dev/Legacy |
| Scratch | Minimal | None | Impossible | Static binaries |
Using Distroless Images
dockerfile
# Multi-stage build with distroless
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server
# Distroless runtime image
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]Using Chainguard Images
dockerfile
# Chainguard Python image
FROM cgr.dev/chainguard/python:latest
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER nonroot
CMD ["python", "app.py"]Alpine Security Hardening
dockerfile
FROM alpine:3.19
# Install minimal packages only
RUN apk add --no-cache \
python3 \
py3-pip \
&& rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1000 app && \
adduser -u 1000 -G app -s /bin/sh -D app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["python3", "app.py"]Image Registry Best Practices
Using Private Registries
yaml
# ImagePullSecrets configuration
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
namespace: production
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-docker-config>
---
# Set default ImagePullSecret on ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
imagePullSecrets:
- name: registry-credentialsImage Pull Policies
yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: myregistry.io/myapp@sha256:abc123... # Use digest
imagePullPolicy: Always # Always verify latestImmutable Tag Policy (Kyverno)
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
rules:
- name: require-digest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must use digest instead of tags"
pattern:
spec:
containers:
- image: "*@sha256:*"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
validationFailureAction: Enforce
rules:
- name: disallow-latest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Using 'latest' tag is not allowed"
pattern:
spec:
containers:
- image: "!*:latest"CI/CD Pipeline Integration
Complete Image Security Pipeline
yaml
# .github/workflows/secure-build.yaml
name: Secure Image Build
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 1. Code scanning
code-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner (fs mode)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1'
# 2. Build and scan
build-scan:
needs: code-scan
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image for scanning
uses: docker/build-push-action@v5
with:
context: .
load: true
tags: ${{ env.IMAGE_NAME }}:scan
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.IMAGE_NAME }}:scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Check for critical vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.IMAGE_NAME }}:scan'
exit-code: '1'
severity: 'CRITICAL'
# 3. Push and sign
push-sign:
needs: build-scan
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Sign image with Cosign
run: |
cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Attach SBOM to image
run: |
cosign attach sbom --sbom sbom.spdx.json ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}
# 4. Generate SLSA Provenance
provenance:
needs: push-sign
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.push-sign.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}Summary
Container image security の主な側面:
- Scanning: Trivy、ECR enhanced scanning で vulnerabilities を検出します
- Signing: Cosign/Sigstore で image integrity を確保します
- Verification: Kyverno で signed images のみを許可します
- Supply Chain: SBOM、SLSA で透明性を実現します
- Base Images: distroless、Chainguard を使用します
- CI/CD Integration: automated security pipelines を構築します
Recommendations
- すべての images に vulnerability scanning を適用してください
- production images には必ず signing を行ってください
latesttag ではなく digests を使用してください- distroless または minimal base images を使用してください
- SBOM generation と management を自動化してください