Containers share the host kernel, inherit whatever their image contains and are often started with far more privilege than the application needs, so one weak image or one careless runtime flag can expose the host, its neighbours and the credentials they hold. NIST describes containers as "a form of operating system virtualization", which is exactly why isolation depends on configuration rather than on a hypervisor boundary. This guide covers the full container lifecycle: base images and Dockerfile hardening, scanning, SBOMs and signing, registries, the Docker daemon and host, runtime isolation (capabilities, seccomp, AppArmor and SELinux, read-only filesystems, rootless mode), container escape routes and runtime detection. It is written for platform engineers, DevSecOps teams and security architects running Open Container Initiative (OCI) images on Docker, containerd or CRI-O, whether or not Kubernetes is on top. Orchestrator hardening is out of scope here; Kubernetes service account tokens are covered in the Kubernetes NHI Security Guide linked below.
Key takeaways
- Most container risk is decided at build time. A minimal or distroless base, a non-root
USER, pinned versions and no secrets in layers remove whole classes of findings before a scanner ever runs. - Trust images by digest and signature, not by tag. NIST SP 800-190 notes that the
latesttag is "only a label attached to the image and not a guarantee of freshness"; verify signatures and pin digests at deploy time. - The Docker socket is root on the host. Never mount
/var/run/docker.sockinto a container or expose the daemon on TCP without TLS; OWASP warns that mounting the socket read-only does not make the API read-only. - Layer the runtime controls. Drop all capabilities, set
no-new-privileges, keep the default seccomp profile, use AppArmor or SELinux, run read-only and never use--privileged; each blocks a different escape route. - Detect at runtime, because prevention will miss things. Syscall-level tools such as Falco flag shells in containers, writes to
/etcand namespace changes that image scanning cannot see.
Where container risk sits
NIST SP 800-190 groups container risk into five areas; using them stops a programme over-investing in image scanning while the daemon, registry or host stay weak.
| SP 800-190 area | Typical risks (section) | Primary controls |
|---|---|---|
| Images (3.1) | Vulnerable components, configuration defects such as running as root or an SSH daemon, embedded malware, embedded clear text secrets, untrusted images | Minimal bases, scanning with quality gates, secret scanning, signing, trusted image set |
| Registries (3.2) | Insecure connections, stale images, weak authentication and authorisation | TLS only, authenticated pulls, tag immutability, pruning, scoped push rights |
| Orchestrator (3.3) | Unbounded admin access, unauthorised access, flat networks, mixed sensitivity levels, node trust | RBAC, network policy, separating workloads by sensitivity onto different hosts |
| Containers (3.4) | Runtime software vulnerabilities, unbounded network access, insecure runtime configuration, app vulnerabilities, rogue containers | Patched runtimes, egress control, capabilities, seccomp, LSM profiles, runtime detection |
| Host OS (3.5) | Large attack surface, shared kernel, component vulnerabilities, improper user access, filesystem tampering | Container-specific host OS, kernel patching, restricted host access, integrity monitoring |
Two SP 800-190 principles shape everything else: containers should be "operated as stateless entities that are deployed but not changed", so fixes go into a rebuilt image rather than a patched running container, and "a given host kernel only runs containers of a single sensitivity level", because the shared kernel is the weakest boundary.
Building secure images
Base image choice
- Distroless and minimal bases: Google's distroless images contain "only your application and its runtime dependencies" and no package manager or shell. The static image is about 2 MiB, against about 5 MiB for Alpine and 124 MiB for Debian, and the project notes that this "improves the signal to noise of scanners". Use the
:nonrootvariants, and keep:debug(which adds a BusyBox shell) out of production. - Language and OS bases: where a full distribution is needed, use a slim official variant, pin the version, rebuild often and switch to the non-root user the image provides (official Node.js images, for example, include a
nodeuser). - Golden images: SP 800-190 says organisations "should maintain a set of trusted images and registries" and allow only images from that set to run. A small catalogue of approved, regularly rebuilt bases is far easier to patch.
Dockerfile hardening
- Run as non-root: create a user and set
USER; SP 800-190 also says SSH and remote shell tools should not be enabled inside containers. - Multi-stage builds: compile in a builder stage and copy only the artefact into the final stage, so compilers, package caches and test tooling never ship.
- Prefer COPY to ADD, pin versions and avoid curl-pipe-to-shell: OWASP lists all three as Dockerfile checks to automate in CI.
- Never put secrets in layers: Docker's documentation states that "Build arguments and environment variables are inappropriate for passing secrets to your build" because they persist in the image. Deleting a file in a later layer does not remove it from the earlier one. Use BuildKit secret mounts, which appear at
/run/secrets/<id>only for the duration of oneRUNstep.
An illustrative fragment combining these points:
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN --mount=type=secret,id=netrc,target=/root/.netrc \
CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]
Build it with docker build --secret id=netrc,src=$HOME/.netrc . so the credential never enters a layer. See the Secrets Management Guide.
Scanning, SBOMs, signing and provenance
- Scan in the pipeline and in the registry: SP 800-190 recommends container-aware vulnerability tools with policy-driven "quality gates" in build and deployment. Open source scanners include Trivy, Grype and Clair; OWASP also lists Docker Bench for Security for host and daemon checks, and Gitleaks, TruffleHog and ggshield for secrets in images. Rescan stored images daily.
- Generate an SBOM and provenance at build: BuildKit attaches attestations in in-toto format to the image index.
--sbom=trueadds an SPDX SBOM; minimal (mode=min) provenance is added by default and--provenance=mode=maxrecords more build detail. An SBOM answers "which images contain this library?" when the next critical CVE lands. - Sign images and verify before deploy: Sigstore cosign supports keyless signing tied to a CI identity and key-based signing. Verification checks the signature and that the signed digest matches the image, for example
cosign verify <image> --certificate-identity=... --certificate-oidc-issuer=..., andcosign verify-attestationchecks SBOM or provenance attestations. Distroless images are themselves keyless-signed, so verify bases before building on them. - Deploy by digest: SP 800-190 says operations should use "immutable names that specify discrete versions". Pin
image@sha256:...in deployment manifests and let automation bump digests.
See the SLSA and Build Provenance Guide for provenance levels and the Open Source Security Guide for dependency risk inside images.
Registry security
- Authenticate every pull of proprietary images: SP 800-190: "All access to registries that contain proprietary or sensitive images should require authentication." Use TLS only and remove anonymous pull from internal repositories.
- Separate push from pull: only the build pipeline's identity should push to release repositories; developers and clusters get pull-only rights. A push credential is a supply chain key: ATT&CK T1525 Implant Internal Image describes adversaries planting a backdoored image in an organisation's own registry.
- Make tags immutable: in Amazon ECR,
--image-tag-mutability IMMUTABLEmakes a push of an existing tag fail withImageTagAlreadyExistsException;IMMUTABLE_WITH_EXCLUSIONallows exceptions such aslatest. Other registries offer equivalent settings. - Prune stale images: SP 800-190 treats stale images as a risk in their own right; apply lifecycle rules so unpatched old versions cannot be pulled by mistake.
- Control public sources: pull third-party images through a scanning proxy or mirror, not directly from public hubs.
Daemon, runtime and host hardening
Docker's documentation is blunt: "only trusted users should be allowed to control your Docker daemon", because a user who can start containers can mount the host's / and "alter your host filesystem without any restriction". Membership of the docker group is therefore equivalent to root on the host.
- Keep the API local: the daemon listens on a Unix socket by default. If remote access is needed, use TLS on port 2376 or
DOCKER_HOST=ssh://; never the plain text port 2375. Exposed Docker APIs are a standard worm entry point, as the TeamTNT Worm 2020 showed. - Use a container-specific host OS where possible: SP 800-190 recommends them to cut attack surface, with read-only system partitions.
- Patch the runtime stack: runc, containerd, CRI-O and the kernel are all in scope. CVE-2024-21626 in runc 1.1.11 and earlier let a process started with
runc execget a working directory in the host filesystem; runc 1.1.12 fixed it. - Benchmark the host: the CIS Docker Benchmark (version 1.9.0 at the time of writing) is consensus-based guidance covering host, daemon, image and runtime configuration; Docker Bench for Security automates many of its checks.
| Option | What runs as root | Protects against | Trade-offs |
|---|---|---|---|
| Default (rootful daemon) | Daemon and, unless USER is set, the container process | Little beyond namespaces, default capabilities and seccomp | Widest compatibility; worst blast radius |
--userns-remap | Daemon only; container root maps to an unprivileged host UID | Escaped container root lands as an unprivileged user | OWASP notes the daemon still runs as root; some volume ownership issues |
| Rootless mode | Nothing: daemon and containers run inside a user namespace | Daemon and runtime vulnerabilities | Needs newuidmap, newgidmap and at least 65,536 subordinate IDs; OWASP notes it does not protect against kernel vulnerabilities |
| Sandboxed runtime (gVisor, Kata Containers) | Varies; adds a user-space kernel or lightweight VM boundary | Shared-kernel exploits for untrusted or multi-tenant code | Performance and compatibility costs; use for the highest-risk tiers |
Runtime isolation controls
These controls overlap deliberately; treat any exception as a reviewed risk.
| Control | Docker | Kubernetes securityContext | What it blocks |
|---|---|---|---|
| Non-root user | -u 4000 or USER | runAsNonRoot: true, runAsUser | Root-owned host files after an escape; many local escalations |
| Capabilities | --cap-drop all --cap-add CHOWN | capabilities: drop: ["ALL"] | Raw sockets, mounts, module loading, other root powers |
| No privilege escalation | --security-opt=no-new-privileges | allowPrivilegeEscalation: false | setuid binaries gaining privileges |
| Seccomp | Default profile; custom via --security-opt seccomp=profile.json | seccompProfile: type: RuntimeDefault | Dangerous syscalls such as keyctl, bpf, kexec_load |
| AppArmor or SELinux | docker-default AppArmor profile or SELinux labels | appArmorProfile, seLinuxOptions | File and capability access outside the profile |
| Read-only root | --read-only --tmpfs /tmp | readOnlyRootFilesystem: true | Dropping tools and persistence in the container |
| Resource limits | --memory, --pids-limit, --ulimit | requests and limits | Fork bombs, noisy neighbours, some cryptomining impact |
| Never privileged | no --privileged | privileged: false | Full device and capability access to the host |
Docker's default seccomp profile is an allowlist that "disables around 44 system calls out of 300+" and is "moderately protective while providing wide application compatibility". Docker advises against changing it, and OWASP advises against disabling it; running with seccomp=unconfined removes a key escape barrier. For networking, SP 800-190 says organisations "should control the egress network traffic sent by containers". Use user-defined networks rather than the default bridge, and note OWASP's warning that Docker manages its own iptables rules and can bypass host firewalls such as UFW, so bind published ports to 127.0.0.1 when they should be local.
Container escape: routes and blocks
ATT&CK T1611 Escape to Host lists the main routes. Each maps to a control above, so enforce them by policy rather than leaving them to teams.
- Privileged containers: used to run commands or load a kernel module on the host. Block
--privilegedandprivileged: trueoutright. - Host filesystem mounts: a bind or
hostPathmount of/or sensitive paths gives direct host access. Disallow hostPath in shared clusters and review every bind mount. - Mounted management sockets: a compromised container with
docker.sockcan start a new privileged container. ATT&CK also tracks reading credentials from container APIs as T1552.007. - Syscall abuse: T1611 cites
unshareandkeyctl; seccomp and dropped capabilities remove most of this. - Kernel and runtime vulnerabilities: patching, minimal hosts, sensitivity-based placement and sandboxed runtimes.
Runtime detection
Falco, a graduated CNCF project originally created by Sysdig, parses Linux syscalls at runtime through a modern eBPF probe (the default) or a kernel module, and evaluates them against YAML rules. Plugins add sources such as Kubernetes audit events. Documented detections include privileged container activity, namespace changes via setns, writes to /etc, /usr/bin and /usr/sbin, shell and SSH binaries being executed, and unexpected network connections. OWASP also names Tetragon and Cilium's eBPF tooling for runtime monitoring.
| ATT&CK technique (v20) | What it looks like in containers | Signal to collect |
|---|---|---|
| T1610 Deploy Container | New container started from an unknown image, often privileged | Daemon or orchestrator events; image not in the approved registry |
| T1609 Container Administration Command | docker exec or kubectl exec into a running workload | Daemon and audit logs; interactive shell spawned in container |
| T1611 Escape to Host | Mount, unshare, namespace changes, host path access | Falco namespace and mount rules; seccomp denials |
| T1612 Build Image on Host | Image built directly on a host to avoid registry controls | Build API calls on runtime hosts that should never build |
| T1525 Implant Internal Image | Backdoored image pushed to an internal registry | Registry push logs; unsigned digests; push by non-pipeline identity |
| T1613 Container and Resource Discovery | Enumerating containers, images and API endpoints | Unusual API list calls from workloads |
| T1496 Resource Hijacking | Cryptominers in compromised containers | CPU anomalies; connections to mining pools |
Send daemon logs and runtime alerts to the SIEM with image digest, container ID and host attached, and keep the daemon at the default info log level as OWASP recommends. See the Detection Engineering Guide.
Where identity fits
Registry credentials, CI push tokens, cloud keys and API keys are what attackers in a compromised container hunt for. Supply secrets at runtime from a secrets manager or orchestrator mount, as SP 800-190 says ("Secrets should be stored outside of images and provided dynamically at runtime as needed"), prefer short-lived workload identity over static keys, and give the push identity to the pipeline only. See the Cloud Workload Identity Guide and, for service account tokens, the Kubernetes NHI Security Guide.
Implementation roadmap
- Inventory: list container hosts, registries, base images and who can reach each daemon. Fix any daemon on TCP 2375 and any container with
docker.sockmounted or--privilegedset first. - Standardise bases: publish a small set of approved, signed, regularly rebuilt base images (distroless or slim, non-root) and migrate the highest-exposure services first.
- Add pipeline gates in report-only mode: scanning, Dockerfile linting and secret scanning that report without failing builds for two to four weeks, so teams can fix backlogs and you can tune thresholds.
- Enforce gates: fail builds on critical, fixable vulnerabilities and on any secret found; allow time-limited, recorded exceptions.
- Sign, attest and lock the registry: sign in CI, generate SBOMs, turn on tag immutability, restrict push to pipeline identities, then require signature verification at deploy time, starting with one environment.
- Harden runtime defaults: roll out non-root, dropped capabilities,
no-new-privilegesand read-only roots through shared templates; pilot on stateless services before stateful ones. - Deploy runtime detection: start Falco or an equivalent with default rules in alert-only mode, tune noisy rules for a few weeks, then route high-confidence alerts to response playbooks.
- Roll back safely: because images are immutable and deployed by digest, rollback means redeploying the previous signed digest; keep the last known good digests and keep emergency exceptions time-limited and logged.
Decision guidance
- Distroless vs slim distribution bases: choose distroless for compiled and well-packaged runtimes where nobody needs a shell; choose slim distributions where native dependencies need a package manager at build time, and use multi-stage builds to keep it out of the final image.
- Rootless vs userns-remap: rootless removes root from the daemon too and suits developer machines and single-tenant build hosts; userns-remap is a smaller change for existing rootful fleets. Neither replaces kernel patching.
- Sandboxed runtimes: use them for untrusted code, customer-supplied workloads and multi-tenant platforms, not by default for first-party services.
- Small organisations: rely on managed registries with built-in scanning and immutability, official or distroless bases and a handful of enforced runtime defaults. Large organisations: run a golden image programme, central signing, admission policy and fleet-wide runtime detection, often within a CNAPP. See the CNAPP Guide.
Detection and measurement
- Image hygiene: percentage of running containers built from an approved base image, and median age of those bases.
- Vulnerability exposure: count of running images with critical, fixable vulnerabilities, and mean time from fix availability to redeploy.
- Privilege posture: number of running containers that are privileged, run as root, mount
docker.sockor a sensitive host path; the target for the first three is zero outside reviewed exceptions. - Supply chain integrity: percentage of deployments by digest with a verified signature.
- Secrets: secrets found in images per month, and time to revoke each one.
- Runtime alert quality: high-severity runtime alerts per week and the share that were true positives after triage.
Common failure patterns
- Secrets baked into public images: credentials left in layers or build artefacts are recoverable by anyone who pulls the image. The Docker Hub Secrets Leak 2025 found cloud, CI and AI keys in 10,456 public images, and the PyPI Admin GitHub Token Leak 2024 came from a token in a compiled Python file inside a public image.
- Exposed Docker daemons: an unauthenticated API is an open door for container deployment and credential theft, as in the Carbonato Botnet 2026 on exposed Docker hosts.
- Trusting a tag: mutable tags let a poisoned image replace a trusted one silently. The Checkmarx KICS Supply Chain Attack 2026 poisoned Docker images published under a trusted project's name; digest pinning and signature verification limit that exposure.
- "Temporary" privileged containers: a debug container started with
--privilegedor the Docker socket stays for years and becomes the escape path. - Scanning without redeploying: long-running containers keep the old image; measure what is running, not what was built.
- Disabling seccomp or AppArmor to fix one error:
unconfinedbecomes the template default. Write a narrow custom profile instead.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Hardened images and runtime configuration | CM-6, CM-7 | PR.PS-01 | 4.1 | A.8.9 |
| Image and runtime vulnerability scanning | RA-5, SI-2 | ID.RA-01, PR.PS-02 | 7.5, 7.6 | A.8.8 |
| Least privilege for daemon and containers | AC-6 | PR.AA-05 | 5.4 | A.8.2 |
| Signing and integrity verification | SI-7 | PR.PS-06 | 16.1 | A.8.19, A.8.25 |
| Runtime monitoring | SI-4, AU-12 | DE.CM-09 | 8.2, 13.1 | A.8.15, A.8.16 |
Practitioner checklist
- No Docker daemon listens on TCP 2375; remote access uses TLS on 2376 or SSH, and
dockergroup membership is treated as root. - No container mounts
docker.sockor runs--privilegedoutside a reviewed, time-limited exception. - All production images come from an approved, signed base set, run as non-root and are deployed by digest.
- Builds use secret mounts; secret scanning fails any build that leaves a credential in a layer.
- Vulnerability scanning runs in CI and daily against registries, with gates on critical, fixable findings.
- SBOM and provenance attestations are generated for every release image and signatures are verified before deploy.
- Registries require authentication, push is limited to pipeline identities and tags are immutable.
- Runtime defaults drop all capabilities, set
no-new-privileges, keep seccomp at RuntimeDefault and use read-only roots. - runc, containerd or CRI-O and host kernels are on a defined patch cycle; hosts are checked against the CIS Docker Benchmark.
- Runtime detection is deployed on every container host with alerts in the SIEM.
Standards and references
- NIST: SP 800-190 Application Container Security Guide
- CIS: Docker Benchmark
- MITRE ATT&CK: Containers Matrix
- OWASP: Docker Security Cheat Sheet
- Docker: Seccomp security profiles for Docker
- Docker: Rootless mode
- Sigstore: Verifying signatures with cosign
- Falco: Documentation
Related NHI Mgmt Group resources: Kubernetes NHI Security Guide · SLSA and Build Provenance Guide · Secrets Management Guide · Linux Server Hardening Guide · CI/CD Pipeline Identity Security Guide