Container Security Best Practices


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.