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.