Skip to content
Tech & IA

Security

A hardened container image, point by point

Non-root user, read-only filesystem, SBOM, signature: what each measure really prevents, in what order to apply them, and what they do not replace.

6 min read

Shipping containers stacked on a quay

A container image is an archive that holds an application and everything it needs to run: libraries, system tools, sometimes a full shell. The more an image contains, the more there is to update, to audit, and to exploit if someone takes control of the application. Hardening an image means removing what is unnecessary and restricting what remains.

Here are the measures that matter, in an order that goes from the most profitable to the most advanced, with for each one what it concretely prevents. None replaces an application without flaws, but each reduces what an attacker can do if they find one.

1. Start from a minimal base

A general-purpose base image ships a whole system: package manager, shell, utilities. Each component adds potential vulnerabilities and handy tools for an intruder, who no longer has to bring their own. Minimalist bases, such as distroless images or an Alpine base, contain only what is needed to run.

Combined with a multi-stage build, this gives a final image containing no compiler, no build tools and no dependency source code. You build in a first stage and copy only the result into the second.

2. Do not run as root

By default, many containers run their process as the container’s administrator. That is not equivalent to being administrator of the machine, but it is too favourable a starting point for an escape or for modifying files the process should not touch.

The remedy is to declare an unprivileged numeric user in the image, with the USER instruction, for example USER 10001. A numeric ID is preferable to a name, because Kubernetes can verify that it is not zero. On the cluster side, you add runAsNonRoot: true to the security context to refuse any start that would contradict this rule. The process should also listen on a port above 1024, which avoids needing special privileges.

3. A read-only filesystem

If the container’s root filesystem cannot be modified, an intruder cannot drop a tool in it, replace a binary or alter a configuration. With Kubernetes, you enable readOnlyRootFilesystem: true.

The application then has to write elsewhere. That is where most implementations fail the first time: the program wants to write to /tmp, to a cache folder, to a data directory. The solution is to explicitly mount volumes for those paths only, often a temporary emptyDir volume for what is disposable, and a persistent volume for data. You thus discover precisely where the application writes, which is useful in itself.

4. Remove unneeded privileges

Linux splits the administrator’s powers into capabilities. A container receives a default set, most of which are useless for an ordinary application. In the security context, you drop everything, then add back case by case what is necessary, which is rare:

  • capabilities.drop: [ALL];
  • allowPrivilegeEscalation: false, which prevents a process from gaining more rights than its parent;
  • a seccomp profile of type RuntimeDefault, which blocks the most dangerous and rarely used system calls.

These settings are made on the deployment side rather than in the image, but a well-designed image is one that supports them without complaint.

5. Leave no secrets in the layers

An image is made of stacked layers, and each one is kept. A password copied and then deleted in a later step stays readable in its original layer. Secrets must therefore never enter the image: they arrive at runtime, through environment variables or files mounted by the orchestrator. A well-kept .dockerignore file also avoids shipping a local configuration folder or a Git history by accident.

6. Pin what you use

A base image referenced by a moving tag, such as node:22, is not the same from one month to the next. To rebuild identically and know exactly what runs, reference it by its digest (@sha256:…), and update deliberately, with a tool that proposes the changes rather than suffering them. Pin the versions of installed packages too when possible.

7. Scan, without drowning

Tools such as Trivy or Grype compare the image’s contents against databases of known vulnerabilities. Run in CI, a scan avoids shipping an image with a critical flaw already fixed upstream. You have to accept that it produces noise, though: a vulnerability in a package that is present but never called does not weigh the same as another in the execution path. What helps is reducing the surface (point 1) and triaging by exploitability instead of aiming for a counter at zero.

8. The SBOM: an inventory of what is inside

An SBOM, for software bill of materials, is the machine-readable list of all the components of an image and their versions. The common formats are SPDX and CycloneDX, and tools such as Syft or BuildKit attestations can produce it at build time.

Its value is very concrete. The day a flaw is announced in a library, you can answer “do we use it, and in which images?” in a few minutes instead of digging by hand. An SBOM prevents no attack, it shortens reaction time.

9. Signing: proving where the image comes from

Without a signature, nothing guarantees that the image your cluster pulls is the one your CI built. With Sigstore and its cosign tool, you can sign an image, and you can do it without managing a private key: the CI’s identity, through OIDC, is enough to obtain a short-lived certificate, and the signature is recorded in a public transparency log, Rekor.

A signature only means something if it is verified. On the cluster side, an admission controller, for example Kyverno or Sigstore’s policy controller, refuses to start any image that is not signed by the expected identity. You can add provenance attestations, which describe how and by what the image was built, in the spirit of the SLSA framework.

An unverified signature is decoration.

10. What you gain, and what you lose

A hardened container is harder to debug: without a shell, you can no longer connect to inspect it. Kubernetes offers ephemeral debug containers for this, which you attach to a running pod with their own tools. It is a good compromise: the production image stays thin, and the tooling exists only for the length of an intervention.

Here is a reasonable order to get there without breaking everything:

  1. Multi-stage build and minimal base.
  2. Non-root user and unprivileged port.
  3. Read-only rootfs with explicit volumes.
  4. Capabilities dropped and a seccomp profile.
  5. Scan in CI, then SBOM.
  6. Signing, then verification at admission.

Hardening does not make an application safe: an application flaw is still a flaw. But it turns a full takeover into a contained incident, and that is exactly what you expect from defence in depth.

Comments (0)

No comments yet.