Container security requires a multi-layered approach covering build time, image security, runtime protection, and supply chain security. This guide covers all aspects of securing containers.
┌─────────────────────────────────────────┐
│ Container Security Layers │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Supply Chain Security │ │
│ │ (SBOM, Signing, Provenance) │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Runtime Security │ │
│ │ (Falco, Monitoring, Policies) │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Image Security │ │
│ │ (Scanning, Signing, Minimal) │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Build Security │ │
│ │ (Dockerfile, Non-root, Minimal)│ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
# Use specific tags, not latest
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Set working directory
WORKDIR /app
# Copy package files first (layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy application code
COPY . .
# Remove unnecessary packages
RUN apk del .build-deps
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD node healthcheck.js
# Use exec form for CMD
CMD ["node", "server.js"]# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
RUN npm ci --only=production
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]# Remove unnecessary tools
RUN apk del curl wget
# Set read-only root filesystem (in Kubernetes)
# securityContext:
# readOnlyRootFilesystem: true
# Drop capabilities
# securityContext:
# capabilities:
# drop:
# - ALL
# add:
# - NET_BIND_SERVICEComprehensive vulnerability scanner
# Install Trivy
brew install trivy
# or
docker pull aquasec/trivy
# Scan image
trivy image nginx:latest
# Scan with severity filter
trivy image --severity HIGH,CRITICAL nginx:latest
# Scan and output JSON
trivy image -f json -o results.json nginx:latest
# Scan filesystem
trivy fs .
# Scan Kubernetes cluster
trivy k8s cluster --report summaryCI/CD Integration:
# GitHub Actions
name: Trivy Scan
on:
push:
branches: [ main ]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:latest .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'Container vulnerability scanning
# Install Snyk
npm install -g snyk
# Authenticate
snyk auth
# Test container image
snyk container test nginx:latest
# Monitor image
snyk container monitor nginx:latest
# Test with policy
snyk container test nginx:latest --policy-path=.snykStatic analysis of container images
# Run Clair
docker run -d --name clair \
-p 6060-6061:6060-6061 \
quay.io/projectclair/clair:latest
# Scan image
clair-scanner --clair=http://localhost:6060 \
--ip=host.docker.internal nginx:latestImage Size Comparison:
- Alpine: ~5MB
- Distroless: ~10-20MB
- Ubuntu: ~70MB
- Debian: ~120MB
Use Alpine:
FROM alpine:latest
RUN apk add --no-cache nodejs npmUse Distroless:
FROM gcr.io/distroless/nodejs:20
COPY . .
CMD ["server.js"]Sign images for integrity
# Install Cosign
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Generate key pair
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key user/image:tag
# Verify signature
cosign verify --key cosign.pub user/image:tag
# Keyless signing (CI/CD)
cosign sign user/image:tag
cosign verify user/image:tagRuntime threat detection
# Install Falco
curl -s https://falco.org/repo/falcosecurity-3672BA8F.asc | sudo apt-key add -
echo "deb https://download.falco.org/packages/deb stable main" | sudo tee -a /etc/apt/sources.list.d/falcosecurity.list
sudo apt-get update
sudo apt-get install -y falco
# Start Falco
sudo systemctl start falco
sudo systemctl enable falco# Install Falco in Kubernetes
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco# falco_rules.yaml
- rule: Write below binary dir
desc: Detect writes to binary directories
condition: >
bin_dir and evt.dir = < and open_write
output: >
File below a known binary directory opened for writing
(user=%user.name command=%proc.cmdline file=%fd.name)
priority: ERROR
tags: [filesystem, mitre_persistence]
- rule: Unexpected network activity
desc: Detect unexpected network connections
condition: >
evt.type = connect and
container.id != host and
not fd.sip in (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
output: >
Unexpected network connection
(user=%user.name command=%proc.cmdline connection=%fd.name)
priority: WARNING
tags: [network]apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
runAsNonRoot: true
runAsUser: 1000# Enforce Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-network-policy
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: []
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 80Software Bill of Materials
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM
syft packages docker:nginx:latest -o spdx-json > sbom.json
# Generate for directory
syft packages dir:. -o cyclonedx-json > sbom.jsonSLSA Provenance
# Generate provenance with SLSA
slsa-verifier verify-image \
--source-uri github.com/user/repo \
--source-tag v1.0.0 \
user/image:tag- Vulnerability Count: By severity
- Compliance Score: Policy compliance
- Runtime Alerts: Falco alerts
- Image Age: Outdated images
- Scan Coverage: Percentage scanned
# Grafana Dashboard
- Vulnerability trends
- Runtime alerts
- Compliance status
- Image security scores
- Remediation progress# Scan in CI/CD
# Scan before deployment
# Scan in registries
# Regular scanning# Alpine Linux
# Distroless images
# Remove unnecessary packages
# Multi-stage builds# Create non-root users
# Set security contexts
# Drop capabilities
# Read-only filesystems# Falco rules
# Network policies
# Pod security standards
# Monitoring# Update base images
# Patch vulnerabilities
# Update dependencies
# Security patches# Sign all images
# Verify signatures
# Keyless signing in CI/CD
# Signature policies- Secure Dockerfiles
- Scan container images
- Use minimal base images
- Sign container images
- Implement runtime protection
- Set up Falco
- Configure network policies
- Generate SBOMs
- Monitor security
- Regular updates
Next Steps:
- Learn Kubernetes Runtime Security
- Explore Supply Chain Security
- Master Image Signing
Remember: Container security requires defense in depth. Secure builds, scan images, protect runtime, and monitor continuously. Start with basics, implement best practices, and continuously improve your security posture.