Containers introduced new attack surfaces: base image vulnerabilities, runtime exploits, secrets in environment variables, and privilege escalation. This guide provides a comprehensive security framework covering the entire container lifecycle—from build to runtime—based on NIST, CIS Benchmarks, and production incident learnings.
Container Security Layers
flowchart TB
subgraph Build ["Build Time"]
BaseImage["Base Image Scanning"]
SBOM["SBOM Generation"]
Signing["Image Signing"]
end
subgraph Registry ["Registry"]
Scan["Vulnerability Scanning"]
Policy["Admission Policies"]
end
subgraph Runtime ["Runtime"]
Network["Network Policies"]
Seccomp["Seccomp/AppArmor"]
Readonly["Read-Only FS"]
end
Build --> Registry --> Runtime
style BaseImage fill:#FFCDD2,stroke:#C62828
style Scan fill:#FFF3E0,stroke:#E65100
style Network fill:#C8E6C9,stroke:#2E7D32
Build-Time Security
Minimal Base Images
# ❌ Bad: Full OS with package manager
FROM ubuntu:22.04
# ✅ Good: Minimal distroless image
FROM gcr.io/distroless/dotnet:6.0
# ✅ Better: Scratch for statically compiled
FROM scratch
COPY --from=build /app/binary /binary
ENTRYPOINT ["/binary"]
Distroless images have 80% fewer CVEs than traditional base images because they exclude shells, package managers, and utilities.
Multi-Stage Builds
# Build stage: Full SDK with tools
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
# Runtime stage: Minimal runtime only
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app .
# Run as non-root user
USER 1000:1000
ENTRYPOINT ["dotnet", "MyApp.dll"]
SBOM Generation
# Generate SBOM with Syft
syft packages myimage:latest -o spdx-json > sbom.json
# Include in image labels
docker build --label "sbom=$(cat sbom.json | base64)" .
Registry Security
Vulnerability Scanning
# GitHub Actions: Scan on push
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myregistry/myimage:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail build on vulnerabilities
Image Signing (Sigstore/Cosign)
# Sign image
cosign sign --key cosign.key myregistry/myimage:v1.0.0
# Verify in admission controller
cosign verify --key cosign.pub myregistry/myimage:v1.0.0
Runtime Security
Pod Security Standards
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:v1
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "256Mi"
cpu: "500m"
Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-isolation
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 5432
Secrets Management
# ❌ Never: Environment variables
env:
- name: DB_PASSWORD
value: "mysecret" # Visible in pod spec!
# ✅ Better: External secrets (AWS/Azure)
containers:
- name: app
volumeMounts:
- name: secrets
mountPath: "/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
volumeAttributes:
secretProviderClass: "azure-keyvault"
Key Takeaways
- Use minimal/distroless base images
- Multi-stage builds separate build tools from runtime
- Scan images in CI/CD before pushing
- Sign images with Sigstore/Cosign
- Enable Pod Security Standards (Restricted)
- Network Policies implement microsegmentation
- Never store secrets in environment variables
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.