The Container Attack Surface
Containers share the host kernel, which introduces unique security considerations. While containers provide process isolation through namespaces and cgroups, a misconfigured container can expose the host system to significant risk. Container security spans the entire lifecycle: build, ship, and run.
Build Phase Security
Use Minimal Base Images
Smaller base images reduce the attack surface by eliminating unnecessary tools and libraries.
# UNSAFE: Large attack surface
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
# BETTER: Minimal distribution
FROM python:3.12-slim
# BEST: Distroless (no shell, no package manager)
FROM gcr.io/distroless/python3
| Image | Size | Packages | Attack Surface |
|-------|------|----------|----------------|
| ubuntu:22.04 | 77 MB | 600+ | Large |
| python:3.12-slim | 120 MB | 100+ | Moderate |
| gcr.io/distroless/python3 | 60 MB | ~10 | Minimal |
| alpine:3.19 | 7 MB | ~5 | Small (uses musl libc) |
Scan for Vulnerabilities
Integrate image scanning into your CI/CD pipeline:
# .github/workflows/security.yml
name: Container Security Scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:latest .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'app:latest'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Multi-Stage Builds
Use multi-stage builds to keep the final image clean:
# Build stage
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server
# Runtime stage
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
Ship Phase Security
Sign and Verify Images
Use Docker Content Trust or cosign to sign images and verify integrity:
# Generate a signing key pair
cosign generate-key-pair
# Sign the image
cosign sign --key cosign.key ghcr.io/myorg/myapp:latest
# Verify before deployment
cosign verify --key cosign.pub ghcr.io/myorg/myapp:latest
Use a Private Registry
Push images to a private registry with access controls and vulnerability scanning:
# Push to private ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.us-east-1.amazonaws.com
docker tag app:latest $ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/app:latest
docker push $ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/app:latest
Run Phase Security
Run as Non-Root
Never run containers as root. Create and use a non-root user:
FROM node:22-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . /app
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
Read-Only Root Filesystem
Mount the root filesystem as read-only to prevent file-system-based attacks:
# Kubernetes pod security context
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containers:
- name: app
image: myapp:latest
securityContext:
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
allowPrivilegeEscalation: false
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Resource Limits
Prevent DoS attacks through resource exhaustion:
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
Seccomp and AppArmor
Restrict system calls available to containers:
securityContext:
seccompProfile:
type: RuntimeDefault # Uses the container runtime's default seccomp profile
Kubernetes Pod Security Standards
Kubernetes defines three Pod Security Standards:
| Standard | Description | When to Use |
|----------|-------------|-------------|
| Privileged | Unrestricted policy | System-level pods |
| Baseline | Minimally restrictive, prevents known escalations | Most workloads |
| Restricted | Heavily restricted, follows pod hardening best practices | Security-critical workloads |
# Apply Restricted standard via namespace labels
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
Runtime Monitoring
Use tools like Falco for runtime threat detection:
# Install Falco
helm install falco falcosecurity/falco \
--set falco.driver.kind=modern-bpf
# Falco detects:
# - Shell spawned in container
# - Sensitive file reads
# - Unexpected network connections
# - Privilege escalation attempts
Summary
Container security requires attention at every stage of the lifecycle. Build minimal images based on distroless or scratch, scan for vulnerabilities before deployment, sign images to verify integrity, run containers as non-root with read-only filesystems, drop all unnecessary capabilities, set resource limits, and enforce Pod Security Standards in Kubernetes. Combine these preventive measures with runtime monitoring for defense in depth.