Container Security
Containers share the host kernel, so their security posture matters enormously. Learn to pick safe base images, run as non-root, drop capabilities, and scan images in the pipeline.
What is this?
Container security is about shrinking the attack surface of the image and the privileges of the running process. Key levers: minimal base images, non-root users, dropping Linux capabilities, seccomp profiles, read-only filesystems and continuous image scanning. Because containers share the host kernel, a container escape is a host compromise.
Why do you need it?
- A container running as root that escapes the namespace becomes root on the host — the worst-case blast radius.
- Attackers scan registries for default images with known vulnerabilities; every unpatched package is a foothold.
- The supply chain is the vector: compromised base images and dependencies poison every build that uses them.
- Images are immutable and versioned, so scanning them is a reproducible, automatable gate.
- Orchestrators amplify mistakes: one bad image deployed everywhere is one big incident.
Mental model
A security guard in a sealed booth
A container
The booth (container) keeps the guard (app) separated from the building (host), but only if the booth has its own locked door — non-root user, dropped capabilities, sealed filesystem.
How it works
- The base image defines the attack surface: distroless and scratch minimize what an attacker can run inside.
- USER in the Dockerfile makes the container run as a non-root user, so escape does not equal root.
- Linux capabilities and seccomp profiles strip the kernel operations a process may perform.
- A read-only root filesystem plus tmpfs for scratch data prevents persistent tampering.
- Scanners (Trivy, Grype) compare image contents against vulnerability databases and fail the build on criticals.
Hardening layers for a container runtime.
Key commands
trivy image --severity HIGH,CRITICAL myapp:1.0Scan an image for known vulnerabilities and filter to the serious ones.
docker run --rm --user 1000:1000 nginx:alpine idRun as an unprivileged user to prove the app works non-root.
docker run --read-only --tmpfs /tmp --cap-drop ALL myapp:1.0Enforce a read-only root filesystem and drop all capabilities.
docker history myapp:1.0Inspect layers to spot bloated or suspicious additions.
docker scout cves myapp:1.0Use Docker Scout to surface CVEs in a local image.
Examples
Scan an image in your pipeline
trivy image --severity HIGH,CRITICAL --no-progress myapp:1.0trivy image --format sarif --output trivy.sarif myapp:1.0docker run --rm --user 1000:1000 --read-only --cap-drop ALL myapp:1.0The first line gates the build on high/critical CVEs, the second emits SARIF for the security dashboard, and the third proves the image still runs with a locked-down runtime profile.
In production
- Every PR builds and scans the image; HIGH/CRITICAL findings block merge and promotion.
- Production images use distroless or scratch bases with no shell, reducing exploitability.
- Kubernetes enforces securityContext (runAsNonRoot, readOnlyRootFilesystem, seccomp) on every pod.
- Signatures (cosign) and pinned base digests protect the supply chain end to end.
- An SBOM is generated at build time and attached to the artifact for compliance and incident response.
Best practices
- Run as a non-root user; set USER in the Dockerfile and runAsNonRoot in Kubernetes.
- Use minimal, pinned base images — never float on :latest tags.
- Drop all capabilities and re-add only the ones the app genuinely needs.
- Make the filesystem read-only and mount scratch space for writes.
- Scan images and their dependencies continuously, gating promotions on severity.
Common mistakes
- Running containers as root because the app needs port 80.
- Building on fat base images that carry shells and compilers into production.
- Scanning images only at build time, never monitoring the registry for new CVEs.
- Copying secrets into images via ENV or build args.
- Ignoring third-party base images from unknown publishers.
Common issues
Container fails to start after adding --read-onlyThe app writes to a path in the root filesystem
Identify the write path from the error, mount it as a volume/tmpfs, or use an app that writes only to /tmp.
Bind to port 80 fails as non-rootUnprivileged processes cannot bind ports below 1024 without NET_BIND_SERVICE
Listen on 8080 and map it, or add the single NET_BIND_SERVICE capability.
Scanner reports CVEs in the OS layerThe base image is out of date
Rebuild on the latest pinned base digest and re-scan; pin by digest so updates are deliberate.
Debugging tips
- Scan the base image and the final image separately to localize which layer introduced a CVE.
- Use trivy image --severity and --ignore-unfixed to separate real risk from noise.
- Test a locked-down image with docker run --cap-drop ALL --read-only and read the error to find what is missing.
- Compare image digests between environments to confirm drift did not change the security posture.
Interview checkpoint
Hands-on lab
- Build an image with a baked-in secret
- Prove the secret is visible at runtime
- Run containers with a hardened runtime profile
- Verify identity inside the container
Harden a Container Image
30 min · intermediate
Quick quiz
What is the primary reason containers should not run as root?
An image with fewer layers is inherently more secure.
When should image scanning happen to be most effective?
Cheat sheet
--user 1000:1000Run as an unprivileged user--read-only --tmpfs /tmpSeal the root filesystem--cap-drop ALLRemove every capability--cap-add NET_BIND_SERVICERe-add only what is neededtrivy image IMGScan a local imagetrivy image --severity HIGH,CRITICALFilter to serious CVEstrivy image --format sarifEmit SARIF for dashboardsdocker scout cves IMGDocker-native CVE report