Cloud Security Infrastructure

Hardening Docker Containers for Production: Enforcing Non-Root Users, Read-Only Root Filesystems, and Seccomp Profiles

Master Docker container hardening in production. Learn step-by-step how to enforce non-root users, read-only root filesystems, and custom seccomp profiles.

Hardening Docker Containers for Production: Enforcing Non-Root Users, Read-Only Root Filesystems, and Seccomp Profiles - editorial cover photograph

Quick Summary / Direct Answer: Hardening Docker containers for production requires neutralizing the blast radius of potential compromises by stripping root privileges, locking the root filesystem against runtime modifications, and filtering dangerous kernel system calls using custom seccomp profiles. Together, these defense-in-depth measures transform standard fragile containers into secure, immutable runtime units.

Key Takeaways:

  • Running containers as root grants attackers full namespace escape potential if a kernel vulnerability exists.
  • Read-only root filesystems prevent attackers from writing malware binaries or modifying system files post-exploit.
  • Seccomp profiles restrict the container to a minimal set of kernel system calls, blocking privilege escalation vectors.

The Root User Trap in Container Architecture

By default, Docker builds and runs your containers as the root user. It’s convenient. It works out of the box. Package managers install cleanly, files write without permission denied errors, and developers rarely think twice about it. It failed us in practice.

When an application runs as UID 0 inside a container, it often maps directly to UID 0 on the host if user namespaces aren’t configured. If an attacker achieves Remote Code Execution (RCE) via a vulnerable web framework or unpatched library, they own the container as root. From there, escaping the container or pivoting into adjacent cluster workloads becomes vastly simpler.

We need to drop privileges immediately. Here is how you enforce a non-root user in your Dockerfile:

FROM node:20-alpine

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER 1001

EXPOSE 3000
CMD ["node", "server.js"]

Notice that we create a dedicated system user and group with explicit IDs. We copy application files using the --chown flag. Most tutorials gloss over this edge case: if you copy files as root and then switch users, your application might lack read permissions, or worse, write permissions where it shouldn’t have them.

Enforcing Immutability via Read-Only Root Filesystems

Most applications do not need to write to their root filesystem during normal execution. Logs should stream to stdout. Temporary files belong in designated volumes or memory-backed tmpfs mounts. Yet, standard Docker setups leave the entire root filesystem writable.

If an attacker injects a web shell or drops a cryptominer onto your disk, a writable root filesystem makes persistence trivial. We stop this cold by making the root filesystem read-only.

Here is a production-grade Kubernetes pod security specification enforcing read-only roots alongside non-root execution:

apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    runAsGroup: 1001
    fsGroup: 1001
  containers:
  - name: web
    image: my-secure-app:latest
    securityContext:
      readOnlyRootFilesystem: true
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL
    volumeMounts:
    - name: tmp-dir
      mountPath: /tmp
    - name: cache-dir
      mountPath: /var/cache
  volumes:
  - name: tmp-dir
    emptyDir: {}
  - name: cache-dir
    emptyDir: {}

When you enable readOnlyRootFilesystem: true, applications trying to write to /app or standard system directories will crash instantly. This forces developers to structure temporary writes correctly using emptyDir volumes backed by RAM or ephemeral storage.

Restricting Kernel Attack Surfaces with Seccomp Profiles

Linux kernels expose over 300 system calls (syscalls). Your web application probably uses fewer than 40. Every unused syscall represents a potential kernel vulnerability waiting to be triggered by an attacker.

Secure Computing Mode (Seccomp) filters these syscalls. Docker applies a default seccomp profile that blocks around 44 dangerous calls, but production workloads often demand stricter hardening. Writing a custom JSON seccomp profile lets you whitelist only the exact syscalls your application requires.

Security Control Default Docker Behavior Hardened Production State
Container User Runs as root (UID 0) Enforced non-root UID/GID (> 10000)
Root Filesystem Read-Write (mutable) Read-Only with explicit tmpfs mounts
Linux Capabilities Retains default capability bounding set All capabilities dropped; only required ones added
Syscall Filtering Standard Docker default profile Custom restrictive whitelist seccomp profile

Let’s look at a snippet of a custom seccomp profile that blocks raw socket creation and module loading:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "epoll_wait",
        "epoll_ctl",
        "epoll_create1",
        "read",
        "write",
        "close"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Testing custom seccomp profiles requires patience. Run your containers in audit mode first using tools like oci-seccomp-bpf-generator to observe exact syscall requirements under load before enforcing strict blocks.

Frequently Asked Questions

How do I handle applications that insist on writing to /tmp or cache directories when the root filesystem is read-only?

You mount ephemeral volumes (such as Kubernetes emptyDir or Docker tmpfs mounts) specifically over those application write paths. This satisfies the application’s runtime needs while keeping the underlying container image strictly immutable.

Does dropping all Linux capabilities break standard networking tools like ping inside containers?

Yes. Tools like ping require the CAP_NET_RAW capability to create raw sockets. In a hardened production environment, diagnostic utilities should be stripped from production images entirely, adhering to minimal base image principles like distroless.

The Bottom Line: Actionable Next Steps

Security hardening isn’t a single checkbox; it’s a systematic reduction of risk. Start by auditing your current images for root execution using static analysis tools. Next, introduce read-only root filesystems in staging environments to identify hardcoded temporary file dependencies. Finally, layer custom seccomp profiles and capability drops to achieve true enterprise-grade container posture.

Leave a Reply