// Engineering Log

Secure Development: Part 6 — Container Security

Published on 2026-09-22

// Fast route

This article belongs to the topic Security and protection.

A container isolates an application from the host, but the isolation is not absolute: all containers share one kernel. A vulnerability in the image, a process running as root, or excessive privileges can turn a bug in the application into a problem for the entire server. This section is about what to do with an already built image and how to run it. Correct Dockerfile building (cache, multi-stage builds, pinning by digest) is covered in part 5, dependency checking is in part 4.

Where risks come from

An image consists of layers: a base image with distribution packages, application dependencies, and the code itself. Vulnerabilities most often come not from your code but from the base image: a full python:3.13 or node:22 contains a compiler, package manager, shell, and hundreds of libraries that the running application doesn’t need. Each of them is a potential vulnerability and a ready-made tool for an attacker if they get inside the container.

The second group of risks is runtime parameters: process running as root, write access to any part of the filesystem, full set of kernel capabilities, lack of memory and process limits.

Minimal base image

The smaller the image, the smaller the attack surface and the fewer findings from scanners.

  • -slim (python:3.13-slim, node:22-slim) — Debian without build tools and documentation. A sensible default: compatibility with glibc is preserved, size is noticeably smaller than the full image.
  • Alpine — a very small image based on musl libc. Suitable for statically compiled programs; for Python and Node.js with native modules there can be compatibility issues and longer builds.
  • Distroless from Google — the image contains only the runtime, without a shell or package manager. Current images are based on Debian 13: gcr.io/distroless/static-debian13 for static binaries (Go, Rust), gcr.io/distroless/python3-debian13, gcr.io/distroless/nodejs22-debian13 and others. Most have a :nonroot tag — the process starts non-root by default, and a :debug tag with a busybox shell for debugging.

Do not use unsupported versions: Python 3.9 and Node.js 18 no longer receive security fixes. For new images — Python 3.12 or 3.13, Node.js 22 or 24.

The lack of a shell in a distroless image is both protection and inconvenience: docker exec -it … sh will not work. To investigate issues use the :debug variant in a test environment or a separate debug container, rather than adding a shell to the production image.

Run non-root

By default the process in a container runs as root. If an attacker escapes the application, they will have maximal privileges inside the container, and with a vulnerability in the runtime there is a chance to get root on the host.

Create a user in the Dockerfile and switch to it:

dockerfile
FROM python:3.13-slim
RUN useradd --system --uid 10001 --no-create-home app
WORKDIR /app
COPY --chown=app:app . .
USER app
CMD ["python", "main.py"]

For distroless images the :nonroot tag is enough. You can also set the user at runtime: docker run --user 10001:10001 …. An application without root cannot listen on ports below 1024 — use port 8080 and publish it externally as needed.

Runtime restrictions

The docker run options (in Compose — the service keys with the same names) close most typical privilege escalation paths:

bash
docker run -d --name app \
  --read-only --tmpfs /tmp \
  --cap-drop=ALL \
  --security-opt no-new-privileges \
  --memory 512m --cpus 1 --pids-limit 200 \
  --user 10001:10001 \
  registry.example.ru/app@sha256:...
  • --read-only mounts the root filesystem read-only: an attacker won’t be able to replace application files or write tools. Directories the application actually writes to are mounted separately: --tmpfs /tmp or a data volume.
  • --cap-drop=ALL removes all kernel capabilities. If the application needs something — for example, NET_BIND_SERVICE for port 80 — add exactly that with --cap-add.
  • --security-opt no-new-privileges prevents processes from gaining new privileges, including via su, sudo, and setuid binaries.
  • --memory, --cpus, --pids-limit limit resources: a memory leak or fork bomb in one container won’t take down the whole server.

Never run production containers with --privileged and do not mount the Docker socket (/var/run/docker.sock) into them: access to the socket equals root privileges on the host.

Seccomp and rootless mode

Seccomp filters system calls. The Docker default profile works as an allowlist of calls and blocks about 44 out of more than 300 — those that are useful for kernel attacks and are rarely needed by normal applications. Docker documentation does not recommend changing it; the main rule is not to disable it with --security-opt seccomp=unconfined, which is sometimes suggested in instructions “to make it work.”

Rootless mode runs not only containers but also the Docker daemon under an ordinary user. Even if an attacker escapes a container, they gain the rights of that user, not root. Installation requires the utilities newuidmap and newgidmap (package uidmap) and at least 65,536 subordinate IDs in /etc/subuid and /etc/subgid; then run dockerd-rootless-setuptool.sh install as the user. Rootless mode reduces some capabilities (networking, privileged ports), so test it on your stack before switching.

Scanning images

A built image must be checked for known vulnerabilities before deployment and regularly after: new CVEs are found in already published packages. The same Trivy that checks dependencies (see part 4) also scans the whole image:

bash
trivy image --severity HIGH,CRITICAL --exit-code 1 registry.example.ru/app:1.4.2

With --exit-code 1 the scan will fail the pipeline on high-severity findings. Scan results for -slim or distroless images are usually an order of magnitude shorter than for full images — the cheapest way to reduce the list of vulnerabilities.

Signing and verifying images

Signing guarantees that the image deployed to the server is the one built by your CI, not swapped in the registry. The standard tool is cosign from the Sigstore project:

bash
cosign generate-key-pair
cosign sign --key cosign.key registry.example.ru/app@sha256:...
cosign verify --key cosign.pub registry.example.ru/app@sha256:...

Sign and verify by digest, not by tag: a tag can be reassigned to another image. Cosign also supports a keyless mode, with confirmation via an OIDC provider. Signature verification only makes sense if it’s enforced in deployment: the deploy script or a Kubernetes controller refuses to run an unsigned image.

Checklist

  1. Base image -slim or distroless, supported runtime version, pinned by digest.
  2. USER is not root; application port above 1024.
  3. Run with --read-only, --cap-drop=ALL, no-new-privileges, memory/CPU/process limits.
  4. No --privileged, no Docker socket inside the container, and no seccomp=unconfined.
  5. Scan the image in CI and regularly for running images.
  6. Sign the image in CI and verify the signature at deployment.
  7. Where possible — Docker rootless mode.

Common mistakes

  • “A container is a sandbox.” All containers share the host kernel; isolation depends on the rights and restrictions you set.
  • Default root. The most common audit finding — and the easiest to fix.
  • Disabled seccomp or --privileged left from debugging in production configuration.
  • Scanning only at build time. An image built six months ago may have accumulated dozens of new CVEs.
  • Using the latest tag in production. It’s unclear what is actually running, and you cannot verify the signature.

// Similar task

If you are dealing with something similar

This article belongs to one of the main working topics. You can keep reading on the topic, go to the homepage to understand what I do, or open the service pages directly.

Article topic

Security and protection

SSL, hardening, access control, service protection, and secure configurations.

Typical tasks behind this topic

  • Set up SSL, certificates, and secure connections
  • Restrict access and close unnecessary entry points
  • Harden server and service configuration

// Next step

If you need help with this topic, not just another article, it is better to go straight to the service page. The homepage and topic collection stay available as secondary routes.

Open services

// Reviews

Related reviews

I needed to get n8n, Redis, and the database working. I had hired another contractor before and everything kept breaking. I hired Mikhail, and the next day everything was working quickly, like clockwork!

There was a task to get n8n, redis and the database working. I had previously ordered from another contractor, it kept breaking all the time. Ordered from Mikhail, the next day everything started working fast, like …

christ_media

n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram

2025-09-24 · ★ 5/5

Experienced buyer

Quick solution — I highly recommend Mikhail as a contractor! I tried to build a similar configuration myself and even followed AI advice, which ended up costing a lot of time and money (due to server downtime). So my advice: hire professionals — it's cheaper =) Thanks to Mikhail for his professionalism.

Quick fix for the problem, I recommend Mikhail as a contractor to everyone! I tried to assemble a similar configuration myself and following advice from neural networks, which resulted in a lot of wasted effort and …

ladohinpy

n8n installation on your VPS server. Configuration of n8n, Docker, AI, Telegram.

2025-08-25 · ★ 5/5

// Contact

Need help?

Get in touch with me and I'll help solve the problem

I reply within one business day (03:00-13:00 GMT)

Или оставьте заявку здесь:

Confirm that you are not a bot.

Write and get a quick reply