Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

Docker & Containerization

Hardening Containers: Non-Root Users, Capabilities and Seccomp

Container hardening reduces what a compromised container can do. We run workloads as non-root, drop Linux capabilities, apply the runtime seccomp profile, use read-only filesystems and block privilege escalation, then enforce those settings with policy so they cannot quietly regress.

Key takeaways

  • Most container escapes and lateral movement need privileges that a normal application never uses.
  • Non-root, no privilege escalation, dropped capabilities and a seccomp profile are a strong, low-cost baseline.
  • Hardening is set in the Dockerfile and again in the runtime spec, because each layer protects against different mistakes.
  • Policy enforcement keeps new services from shipping with weaker settings.
  • Some workloads genuinely need extra privileges, and we document those exceptions rather than hide them.

Why default containers are too permissive

A container is not a virtual machine. It is a process that shares the host kernel, wrapped in namespaces and limited by cgroups. Out of the box, many containers run as root, keep a set of Linux capabilities the application never uses, can write anywhere in their filesystem and can gain more privileges at run time. None of that is needed to serve an API, and all of it helps an attacker who finds a remote code execution bug in your application.

Hardening is the practice of removing those unneeded powers. It is the security half of our Docker containerisation service and carries over directly to Kubernetes consulting.

What good looks like

For a typical stateless service, the baseline we aim for is:

  • Runs as a non-root user with a fixed numeric user id.
  • allowPrivilegeEscalation is false.
  • All capabilities are dropped, with specific ones added only when justified.
  • The runtime default seccomp profile is applied.
  • The root filesystem is read-only, with explicit writable volumes for temp data.
  • No privileged mode, no host networking, no host path mounts.
  • Resource requests and limits are set, so one container cannot starve a node.

Step one: the image

Hardening starts in the Dockerfile, because the image decides the user and file ownership:

FROM debian:bookworm-slim
RUN groupadd --system --gid 10001 app \
 && useradd --system --uid 10001 --gid app --no-create-home app
WORKDIR /app
COPY --chown=10001:10001 --from=build /out/app /app/app
USER 10001:10001
ENTRYPOINT ["/app/app"]

Use a numeric id so Kubernetes can verify runAsNonRoot without resolving names. Avoid installing a shell, curl or a package manager in the final stage unless a requirement says otherwise: an attacker uses what they find.

Step two: the runtime spec

In Kubernetes the same intent is expressed in the pod spec, so it is enforced even if someone builds a careless image:

apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: api
      image: registry.example.com/platform/api:1.4.2
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

The equivalent for plain Docker uses --user, --cap-drop=ALL, --read-only, --security-opt no-new-privileges and a tmpfs mount for writable paths.

Enforcing it

Settings that rely on every developer remembering them will eventually be forgotten. We enforce the baseline at admission time. Kubernetes Pod Security Admission can apply the restricted profile per namespace, and policy engines such as Kyverno or OPA Gatekeeper add custom rules, such as requiring resource limits or blocking images from unapproved registries. Combine this with network policies and RBAC so that a compromised pod has little to talk to and little authority.

Decisions and trade-offs

Rootless runtime or just non-root containers? Running the container runtime itself rootless adds protection against runtime bugs but is more complex to operate. For most teams, hardening the workloads comes first.

Distroless or a normal base? A distroless image removes shells and package managers, reducing what an attacker can use. The cost is harder debugging, which you handle with ephemeral debug containers. See base image standardisation.

Custom seccomp or the runtime default? The runtime default is the right start. Custom profiles are justified for high-risk workloads and need care, because an overly tight profile fails in surprising ways.

When to allow exceptions. Some tools legitimately need more: a node agent that reads host metrics, or a CNI plugin. Put them in a dedicated namespace with explicit approval rather than weakening the baseline for everyone.

Rolling it out without breaking production

Turning on every setting at once is how hardening earns a bad reputation. We stage it. First, the restricted profile runs in warn mode, so Kubernetes reports violations without blocking anything. Then audit mode records them in the audit log. Only after owners have fixed or excepted their workloads do we switch to enforce, and we do it namespace by namespace. In Pod Security Admission, this is a matter of labelling a namespace with the warn, audit and enforce modes at the restricted level:

kubectl label namespace payments \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

Once the warnings stop, add the enforce label. Because the label change is the whole rollout, going back is as quick as removing it.

What hardening does not cover

A hardened container still runs on a node, and a vulnerable kernel or a permissive node role can undermine it. The settings above also do nothing about a vulnerable dependency inside your application, so scanning stays necessary. If your clusters are on a managed service, check which node-level controls the provider handles and which remain yours. For EKS specifically, that boundary is covered in our EKS consulting work.

A note on sidecars and third-party charts

Hardening your own services is the easy part. Vendor Helm charts often ship with root users, writable filesystems or broad capabilities, and service mesh sidecars and init containers may need extra privileges to set up networking. We review each third-party chart, override the values where the chart allows it, and record an exception where it does not. Skipping this step leaves the most privileged workloads in the cluster outside the policy you just wrote.

Checking your own estate quickly

Before any engagement you can get a rough picture yourself. List pods whose security context lacks runAsNonRoot, count containers that do not drop capabilities, and look for privileged: true or hostPath volumes. Open-source tools such as kube-bench and Trivy’s Kubernetes misconfiguration scanning can automate part of this. The results tell you whether hardening is a small clean-up or a larger programme.

Common failure modes

  • Writable-path crashes after enabling a read-only filesystem. Fix by mounting emptyDir volumes at the paths the application writes.
  • Binding to port 80 or 443 as non-root. Listen on a high port and let the Service map it.
  • Volume permission errors. Use fsGroup or fix ownership at build time.
  • Quiet drift as new services copy old manifests. Admission policy catches it.
  • Hardened pods, powerful service accounts. Cloud credentials attached to a pod can undo all the isolation. Scope them tightly.

Delivery and handover

We deliver hardened Dockerfile templates, pod spec baselines (or Helm chart defaults), admission policies and a testing checklist. Findings from image scanning and configuration checks feed the same backlog.

How to tell it is working

A scan of the cluster shows no privileged or root containers outside documented exceptions, new deployments that break the baseline are rejected with a clear message, and a penetration test or tabletop exercise finds a compromised pod with very little to work with. To scope a hardening review, book a consultation.

References

How we deliver it

  1. 01

    Inventory current privileges

    We check which containers run as root, with extra capabilities, privileged mode, host mounts or writable root filesystems.

  2. 02

    Harden the image

    A dedicated user is created, file ownership is fixed, and unneeded shells and tools are removed where practical.

  3. 03

    Harden the runtime spec

    Kubernetes securityContext or Docker run flags drop capabilities, block escalation and apply seccomp.

  4. 04

    Test under the new limits

    Applications are exercised with the restricted settings so failures are found in staging, not production.

  5. 05

    Enforce with policy

    Pod Security Admission or a policy engine rejects workloads that do not meet the baseline.

  6. 06

    Document exceptions

    Any workload that needs more access gets a written reason, the narrowest possible permission and a review date.

Tools and technologies

DockerKubernetesPod Security AdmissionKyvernoOPA GatekeeperseccompAppArmorTrivykube-benchFalcohadolint

Frequently asked questions

Why shouldn't containers run as root?
A container's root user is still root on the kernel that the container shares with the host. If an attacker exploits the application and a kernel or runtime weakness, running as root makes escape and damage far easier. Non-root users remove a large class of attacks for almost no cost.
What are Linux capabilities and which should we drop?
Capabilities split root's powers into small units, such as binding low ports or changing file ownership. Most applications need none. The safe pattern is to drop all capabilities and add back only what a specific workload demonstrably requires, for example NET_BIND_SERVICE for ports below 1024.
What does seccomp do?
Seccomp filters which system calls a container may make. The default runtime profile blocks a set of rarely needed, risky calls. In Kubernetes you enable it with seccompProfile type RuntimeDefault. It is a low-effort safeguard that rarely affects normal applications.
Will hardening break our applications?
Occasionally, and that is why we test first. Common issues are apps that write to the root filesystem, bind to port 80 or expect a particular user id. Each has a known fix, such as an emptyDir volume, a higher port behind a Service, or explicit file ownership.
Is hardening enough to call a cluster secure?
No. It is one layer. You also need patched images, network policy, secrets management, least-privilege cloud permissions and monitoring. Hardening limits the damage from a compromised container but does not stop the compromise.

Talk to an engineer about container security hardening

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.