17.1.3 Image Security Practices
A focused guide to Image Security Practices, connecting core concepts with practical Docker and container operations.
Image security practices bring together a set of specific, complementary controls applied at build time and embedded into the image's own runtime configuration, distinct from network-level or host-level security, focused entirely on reducing what an attacker can do if they ever achieve code execution inside a container built from that image, and on preventing sensitive material from ever reaching the image in the first place.
Never baking secrets into image layers
A secret embedded directly in any layer, even one later removed in a subsequent instruction, remains permanently recoverable from that earlier layer's content, since layer history persists regardless of what a later layer's filesystem state appears to show:
ENV API_KEY=sk_live_abc123
RUN rm -f /tmp/credentials.txt
Both of these patterns leave the secret permanently recoverable, either directly in the image's environment metadata or in the now-removed file's still-present layer history; secrets must be supplied at runtime through mounted files or a secrets manager, never embedded in any build-time instruction at all.
docker history my-api --no-trunc | grep -i "API_KEY"
Running a check like this directly against any image believed to be secret-free is a useful, concrete verification step, since it surfaces exactly this kind of accidentally embedded credential if one exists.
Scanning source repositories for secrets before they reach a build
Beyond checking the built image, scanning the source repository itself for accidentally committed secrets, API keys, private keys, credentials in configuration files, catches the problem at its actual origin, before it ever has a chance to be copied into a build context at all:
gitleaks detect --source . --verbose
pre-commit:
hooks:
- id: gitleaks
Running this as a pre-commit hook, and again as a CI pipeline gate, provides two separate opportunities to catch an accidentally committed secret before it ever reaches a Docker build, which is considerably better than only catching it after it has already been baked into a pushed, potentially widely distributed image.
Avoiding ADD with untrusted remote URLs
ADD supports fetching content directly from a URL during the build, but doing so without verifying the fetched content's integrity introduces a supply chain risk: a compromised or hijacked remote source could deliver malicious content directly into the image with no verification step at all:
ADD https://example.com/install.sh /install.sh
RUN sh /install.sh
COPY install.sh /install.sh
RUN sha256sum -c install.sh.sha256 && sh /install.sh
Committing the script directly into the repository, or at minimum verifying a fetched file's checksum against a known, trusted value before executing it, removes the implicit trust placed in whatever content happens to be served from that URL at build time, which could change without any corresponding change to the Dockerfile itself.
Running as a non-root user by default
Configuring a non-root user as the default for any application container, covered more fully elsewhere, remains one of the single most impactful image-level security controls, directly limiting what a compromised process can do to the host filesystem and to other processes sharing the same kernel:
RUN useradd --create-home appuser
USER appuser
Configuring a read-only root filesystem where feasible
Marking a container's root filesystem as read-only at run time, with explicit, narrow writable mounts provided only for the specific paths the application genuinely needs to write to, prevents a compromised process from modifying the application's own code or planting persistent malicious files anywhere outside those explicitly permitted locations:
docker run --read-only --tmpfs /tmp my-api
This requires the image itself to be built with awareness of which paths genuinely need write access, since any unanticipated write attempt outside the explicitly permitted locations fails immediately rather than silently succeeding the way it would against a writable filesystem.
Dropping unnecessary Linux capabilities
Beyond running as non-root, explicitly dropping Linux capabilities not actually needed by the application further restricts what even a non-root but still somewhat privileged process could otherwise do:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-api
Starting from a fully dropped capability set and adding back only what is specifically, demonstrably needed (such as the ability to bind a privileged port) is a more rigorous default than relying on whatever broader capability set a container would otherwise retain.
Signing images for supply chain integrity
Cryptographically signing built images, and verifying those signatures before deployment, provides assurance that an image was genuinely produced by an authorized build process and has not been tampered with or substituted somewhere along its path from build to deployment:
cosign sign --key cosign.key registry.example.com/my-api@sha256:3f29a8c1...
cosign verify --key cosign.pub registry.example.com/my-api@sha256:3f29a8c1...
Enforcing signature verification as a deployment gate, refusing to deploy any image that does not carry a valid, expected signature, converts this from an optional, advisory check into an actual, enforced supply chain control.
Regularly scanning for known vulnerabilities
Vulnerability scanning, covered in image lifecycle practices generally, deserves emphasis here specifically as a security control: an image that was secure at the time it was built can become vulnerable later purely through the disclosure of a new vulnerability in one of its existing, unchanged dependencies, which is why scanning needs to be an ongoing, recurring activity rather than a one-time, build-time-only check.
trivy image --severity HIGH,CRITICAL registry.example.com/my-api:1.4.2
Common mistakes
- Embedding secrets in any build instruction, even one later seemingly removed, leaving them permanently recoverable from earlier layer history.
- Not scanning source repositories for accidentally committed secrets before they have a chance to reach a build context at all.
- Using
ADDwith a remote URL without verifying the fetched content's integrity, introducing an unverified supply chain dependency. - Running every container as root by default rather than treating non-root execution as the expected configuration.
- Treating image signing and vulnerability scanning as optional, advisory checks rather than enforced gates within the actual deployment pipeline.
Image security practices work together as layers of a single, coherent defense: preventing secrets from ever reaching an image, verifying the integrity of anything fetched during a build, restricting what a compromised process can actually do through non-root execution, capability dropping, and read-only filesystems, and verifying both the image's authenticity through signing and its ongoing freedom from known vulnerabilities through recurring scanning.