Container Hardening: Non-Root Users, Read-Only Filesystems, and Safer Mounts

Harden containers with non-root execution, read-only filesystems, narrow mounts, and tested limits without assuming that a container alone isolates every risk.

In this article

A container can package an application cleanly without making it safe to run with broad privileges. The process still needs a user identity, writable storage, network access, and permissions. Defaults that make development convenient can expose more of the host or runtime than the application actually needs.

Container hardening starts by reducing those capabilities and testing what the application really requires. This guide concentrates on non-root execution, read-only filesystems, and narrow mounts. It is a practical baseline, not a claim that these settings eliminate all isolation risks.

Treat the runtime boundary realistically

Containers share important host resources, including the kernel in a typical Linux deployment. A container image is not the same as a fully independent machine. Host patching, runtime configuration, orchestration, and access to the container service all affect the security boundary.

The Docker security overview explains several of these relationships. Use the controls appropriate to your deployment, and don't rely on an image label such as “hardened” without inspecting how the container is actually launched.

Start with an inventory of required capabilities: files read, paths written, ports opened, external services contacted, and any privileged operations. If the team can't explain why a permission is needed, test whether the application can work without it.

Run the application as a non-root user

Choose a dedicated user in the image or runtime configuration and ensure the application's files have appropriate ownership. Avoid solving every permission error by switching the process back to root. Fix the specific path or operation that requires access.

Test startup, normal requests, scheduled jobs, and shutdown under the non-root identity. Some applications write caches or temporary files in surprising places. Discover those needs in staging rather than after a production deployment fails.

Non-root execution reduces certain consequences but doesn't make an application immune to vulnerabilities. A compromised process can still misuse every permission it retains. Pair the user change with capability reduction, restricted mounts, and appropriate runtime controls.

Make the filesystem read-only where practical

A read-only root filesystem can prevent ordinary writes to the image's filesystem at runtime. Provide explicit writable locations for the application where needed, such as a temporary directory or a managed data volume. Keep those locations narrowly scoped.

An illustrative local test for an image designed to support this configuration is:

bash
docker run --rm --user 10001:10001 --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop=ALL \
  --security-opt no-new-privileges=true \
  example-app:test

This is a teaching example, not a universal launch command. The image must support the chosen user and writable paths. The temporary-memory limit may be unsuitable for your application. Verify flags against the Docker run reference and your platform.

Keep mounts narrow and explainable

Mount only the directories the application needs, and use read-only mounts when it only needs to read. Avoid mounting broad host paths for convenience. A compromised application can interact with the mounted data according to the access you provide.

Pay particular attention to runtime control sockets. Giving a container access to a Docker socket can grant powerful control over the runtime and other workloads. Don't treat it as an ordinary file permission. If automation needs runtime administration, design a separate, restricted control path.

For persistent data, define ownership, backup behaviour, and recovery. A read-only root filesystem doesn't protect a writable volume from deletion by the application. Data protection and process hardening are related but distinct responsibilities.

Reduce capabilities and resource exposure

Drop capabilities the application doesn't need, avoid privileged mode, and retain the platform's appropriate isolation controls. Add back only a documented requirement after testing. Broadly disabling controls to make an image start can erase the benefit of containerisation.

Set suitable memory, CPU, and process limits for the workload. Limits can reduce the impact of runaway behaviour, but they also affect reliability. Test the application's response to a limit so failure is observable and doesn't corrupt persistent state.

Restrict network paths where your platform supports it. A read-only filesystem doesn't stop outbound data transfer. Decide which services the application must contact and keep credentials scoped to those operations.

Review configuration and prove behaviour

Use Text Diff for sanitised before-and-after runtime configuration. Look for changes in users, mounts, privilege flags, capabilities, and writable paths. The tool highlights text changes; it doesn't audit a container or prove isolation.

Use JSON Formatter for synthetic runtime-inspection output if useful, avoiding environment variables or configuration containing secrets. Don't upload a real container environment dump into a public tool.

Run functional tests with the hardened configuration. Confirm the process identity, attempt expected and forbidden writes, and exercise failure paths. See acceptance-test design for independent expectations. Also inspect logs so permission failures don't accidentally expose sensitive configuration.

Conclusion

Harden what the container can actually do. Run as a dedicated non-root user, keep the root filesystem read-only when feasible, restrict mounts, and remove unnecessary privileges. Verify the application under those settings and keep host, runtime, data, and network protections in the same review.

Advertisement