Container Security Hardening
Secure containerized applications from image building to runtime. Covers image scanning, minimal base images, rootless containers, runtime security, Kubernetes security contexts, network policies, and the patterns that protect containerized workloads.
Containers share the host kernel, which means a container escape can compromise every workload on the node. Container security requires defense in depth — securing the image, the build pipeline, the runtime, and the orchestrator. A single misconfigured container can be your weakest link.
Image Security
# BAD: Fat image, runs as root, no scanning
FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3 python3-pip
COPY . /app
RUN pip3 install -r /app/requirements.txt
CMD ["python3", "/app/main.py"]
# GOOD: Minimal image, non-root, pinned versions
FROM python:3.12-slim@sha256:abc123... AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim@sha256:abc123...
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser . .
USER appuser
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8080/health || exit 1
CMD ["python", "main.py"]
# Image size comparison:
# ubuntu:latest + python = ~450MB, 200+ CVEs
# python:3.12-slim = ~120MB, 15-30 CVEs
# python:3.12-alpine = ~50MB, 5-10 CVEs
# distroless/python3 = ~30MB, 0-5 CVEs
Image Scanning
# CI/CD Pipeline: Scan before pushing
name: Container Security
on: [push]
jobs:
scan:
steps:
- 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 }}
severity: "CRITICAL,HIGH"
exit-code: "1" # Fail pipeline on critical/high CVEs
- name: Dockle best practices
run: dockle myapp:${{ github.sha }}
- name: Cosign sign image
run: cosign sign --key cosign.key myapp:${{ github.sha }}
Kubernetes Security Context
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# Read-only root needs writable paths
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Network Policies
# Default deny all ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: production
spec:
podSelector: {} # Apply to all pods
policyTypes: ["Ingress", "Egress"]
# Allow only specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
spec:
podSelector:
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels:
app: api-server
ports:
- port: 5432
protocol: TCP
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Running as root | Container escape = root on host | USER directive in Dockerfile |
| latest tag | Unpredictable, unauditable | Pin image digest, scan on pull |
| No network policies | Any pod can reach any pod | Default deny + explicit allow |
| Writable root filesystem | Malware can modify container | readOnlyRootFilesystem: true |
| No runtime monitoring | Post-exploitation undetected | Falco, Aqua, Sysdig for runtime detection |
Container security is not a single tool — it is a lifecycle practice from image building through runtime. Every layer you secure reduces the blast radius of a breach.