“Containers are isolated, so they’re secure.” That’s what we thought — until a penetration tester escalated from a container to root on the host in half an hour. Containers are not VMs, and the security model is fundamentally different.
A Container Is Not a Sandbox¶
A Docker container shares the kernel with the host. Namespaces and cgroups provide process, network stack, and filesystem isolation — but it’s not hardware virtualization. A kernel exploit inside a container = a kernel exploit on the host. This is the fundamental difference from VMs, and it must be accounted for.
That doesn’t mean containers are insecure. It means you need to address security at every layer — from the base image through the build pipeline to runtime configuration.
Base Image — Minimize the Attack Surface¶
Every package in an image is a potential vulnerability. An Ubuntu base image has hundreds of packages your application doesn’t need — and each one may have a CVE. Rule number one: use a minimal base image.
# ❌ Bad — full Ubuntu, 188 MB, hundreds of packages
FROM ubuntu:16.04
# ✅ Better — Alpine, 5 MB, minimal packages
FROM alpine:3.6
# ✅ Best — distroless, runtime only
FROM gcr.io/distroless/java:latest
Alpine Linux is a good compromise — 5 MB, musl libc, apk package manager. For maximum security, consider distroless images from Google: no shell, no package manager, no utilities. An attacker who gets into the container doesn’t even have ls.
Multi-Stage Builds — Separate Build and Runtime¶
Build tools (gcc, npm, maven) have no business in a production image. Multi-stage builds separate compilation from the final image.
FROM golang:1.9 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:3.6
RUN adduser -D -u 1001 appuser
COPY --from=builder /app/server /server
USER appuser
EXPOSE 8080
CMD ["/server"]
The resulting image contains just the binary and Alpine. No Go toolchain, no source code, no build dependencies. Smaller image = smaller attack surface = faster deploy.
Don’t Run as Root¶
A surprising number of Docker images run as root. If the application doesn’t need a privileged port or access to system resources, always add a USER directive. Root inside a container can, under certain circumstances, escalate to root on the host.
In Kubernetes, use securityContext at the pod level:
securityContext:
runAsNonRoot: true
runAsUser: 1001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem prevents writes to the container’s filesystem — an attacker cannot persist malware. drop ALL capabilities removes Linux capabilities the application doesn’t need.
Image Scanning — Find CVEs Before the Attacker Does¶
Every image should pass through a vulnerability scanner before being deployed to production. Tools like Clair, Trivy, or Anchore analyze image layers and compare packages against CVE databases.
We scan at two points: in the CI pipeline (build time) and in the registry (continuous scan). Build-time scanning blocks deploys with critical CVEs. Continuous scanning catches newly discovered vulnerabilities in images already running in production.
The key is setting a policy: what’s acceptable and what isn’t. Zero tolerance for critical CVEs in the base image. Medium severity with a remediation plan within 30 days. Low severity tracked but not blocking deploy.
Supply Chain — Trust, but Verify¶
docker pull node:latest — do you know exactly what you’re downloading? Who created that image? Was it modified? Docker Content Trust (Notary) enables image signing. Enable it and accept only signed images.
Use specific tags, not :latest. Better yet: pin to a digest. A tag can be overwritten; a digest is an immutable hash. And run your own registry (Harbor, GitLab Registry) instead of pulling directly from Docker Hub.
Runtime Protection¶
Build-time security isn’t enough. At runtime, you need anomaly detection. Seccomp profiles restrict system calls — a container that normally makes HTTP requests has no reason to call ptrace or mount. AppArmor or SELinux profiles add another layer of mandatory access control.
Network policies in Kubernetes act as a firewall between pods. Default deny — no pod communicates with another unless you explicitly allow it. A payment microservice has no reason to talk to the CMS.
Secrets Management¶
Never bake secrets into an image. Not as an environment variable in the Dockerfile, not as a file in the build context. Secrets belong in Kubernetes Secrets (ideally backed by an external store like HashiCorp Vault), mounted as volumes, rotated automatically.
Scan image layer history — docker history shows all layers including deleted files. A secret added and then deleted in the next RUN command is still in the previous layer.
Security as a Continuous Process¶
Container security isn’t a one-time audit. It’s a continuous process integrated into the entire lifecycle — from the Dockerfile through the CI pipeline to runtime monitoring. Start with the basics: non-root, minimal images, CI scanning. Gradually add seccomp, network policies, runtime detection. Each layer makes the attacker’s job harder.
Need help with implementation?
Our experts can help with design, implementation, and operations. From architecture to production.
Contact us