2.5Basic security: non-root, secrets and scanning
The essentials in 30 seconds
- Non-root every time: the container runs as root by default — two lines (dedicated USER) change everything.
- Zero secrets in the image: a frozen COPY or ENV stays readable in history and leaves on push — inject at run time, never at build time.
- Official bases + paced scanning: up-to-date slim, scanned CVEs, regular rebuild — security is a rhythm, not a state.
Running mini-case — Idir and the image running as root
Final act: Idir's configurable image (2.4) is ready, but it runs as root, a variables file was lying around in the context, and nobody ever scanned the layers. Idir hardens: official slim base, dedicated user (the one provided by the official image), ownership adjusted at build time, check via exec + whoami — no longer root. He excludes sensitive files from the context (.dockerignore), injects config at run time, then scans and updates the base. Level 2 is delivered: packaged, optimized, versioned, configurable, hardened.
Epilogue of the 2.1-2.4 thread: Idir's app runs non-root, secret-free, scanned — ready for Compose (level 3).
1. No more running as root
By default, the container process is root: if breached, the impact is maximal. Create a dedicated user (the user provided by the official image such as node, ownership adjusted at build time) and declare USER before CMD: two lines, a world of difference. Check with exec … whoami: you must see the dedicated user, not root. Idir adds USER and checks: no more root "to keep it simple".
2. Zero secrets in the image
A COPYed or ENV secret in the Dockerfile stays readable in the layer history and leaves on push with the image. Inject it at run time (-e variable, mounted file, managed secret) and exclude it from the context (.dockerignore). What is never committed nor embedded will never leak from the registry. Idir removes every secret from the Dockerfile and the context: sensitive config travels outside the image.
3. Scan before deploying
docker scout (or Trivy/Grype) lists the layers' CVEs with their severity. Reflex: start from official slim bases, update the tags, rebuild regularly — a 6-month-old image piles up flaws. In production, a variable at run time stays visible via inspect: prefer managed secrets (files, orchestrator, vault). Idir scans, updates, rebuilds, rescans: security is a rhythm.
🔎 Concrete example — 🔎 Concrete example — Hardening the Node app
Idir's app, hardened and verified version:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "server.js"]
docker scout quickview mon-app:1.0
docker exec -it app whoami # → node, not root
Reading: the app runs as "node", layers are scanned, the port stays declared and published at run time.
Lesson: the .dockerignore excluded sensitive files before the build even started — no embedded secret.
Section vocabulary
- USER
- The runtime user declared in the Dockerfile (before CMD): the container no longer runs as root.
Simple example: Idir declares USER node before starting his app.
What it is not: the owner of the code repository.
Reflex: dedicated USER before CMD, always.
- Secret
- Password, key, token: sensitive information that must NEVER enter the image or the repository.
Simple example: Idir injects sensitive config at run time, never at build time.
What it is not: a simple setting like the port.
Reflex: sensitive = out of the image, out of git.
- CVE
- A public reference of a known vulnerability, with its severity: what scanners list layer by layer.
Simple example: Idir notes the critical CVEs from his scan and updates the base.
What it is not: a Dockerfile configuration mistake.
Reflex: scanning = listing CVEs.
- Attack surface
- Everything embedded and attackable in the image: shrink it (slim, multi-stage, rebuild).
Simple example: Idir ships slim with no SDK: less embedded, less exposed.
What it is not: the number of launched containers.
Reflex: less embedded = less exposed.
In practice — In practice — Hardening your image
- Add a non-root USER to your Dockerfile (before CMD) and rebuild.
- Check with exec + whoami (dedicated user, not root).
- Scan the image and note the critical CVEs.
- Update the base, rebuild, rescan: compare.
Script ready to say — announcing a hardened image (30 s)
"The image runs as a dedicated user, checked with whoami — no more root. No secrets inside: sensitive config is injected at run time and excluded from the context. Official up-to-date base, clean scan, regular rebuild planned. We can push and deploy with confidence."
Say while showing Idir's non-root whoami then the scan report.
Copy-paste template — hardened non-root Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Broken permission
USER breaks writing to a volume: Idir adjusts ownership at build time instead of going back to root.
Sensitive file embedded
Variables pushed with the image: Idir removes them, .dockerignore + injection at run time.
Forgotten scan
6-month-old image never rescanned: Idir updates the base, rebuilds, rescans — rhythm resumed.
Visible run-time secret
Sensitive variable readable via inspect: Idir switches to managed secrets (files, orchestrator) in prod.
✓ Best practices
- Systematic dedicated USER, checked with whoami.
- Up-to-date official slim bases.
- Regular rebuilds (CVE = rhythm).
✗ Common pitfalls
- Staying root "to keep it simple".
- Committing a variables file with secrets.
- Scanning once then forgetting.
Key takeaways
- Non-root: dedicated USER before CMD, two lines, huge effect.
- Zero committed or embedded secrets (injected at run time, excluded from the context).
- We scan (CVEs), update, rebuild regularly.
- Smaller official base = smaller attack surface.
48h challenge — apply tomorrow
Harden your 1.0 image: non-root USER + whoami checked, .dockerignore with no sensitive files, scan + update + rebuild compared.
- I can run an image non-root (USER + whoami checked).
- I can exclude every secret from the image and the context (injected at run time).
- I scan, update and rebuild on a steady rhythm.
Frequently asked questions
USER breaks my permissions, what do I do?
Often a volume mounted as root: adjust ownership (chown) at build time or align the UID at run time. Do not go back to root for ease: fix the permission cleanly and recheck with whoami.
Is ENV with a password at run time safe?
Better than in the image, but visible via inspect: in production prefer managed secrets (mounted files, orchestrator, vault). Run time is only a step, not a protection.
Which image will leak a secret on push?