İçeriğe Atla
Mustafa Erbay
Tutorials · 6 min read · görüntülenme Türkçe oku
100%

Docker Container Security Guide

From image supply chain to runtime hardening, a practical checklist and runbook for running Docker containers safely in production.

Docker Container Security Guide — cover image

In most teams, Docker security is treated at the level of “we ran an image scan, done.” In production, however, the risk surface is much broader: if image supply chain, runtime configuration, and the host/daemon layer aren’t considered together, one day you’ll wake up to a “container escaped to the host” incident.

This guide treats Docker not as “a single tool” but as an operating model. My goal isn’t academic: I want to hand you a checklist that’s applicable in the field, measurable, and wireable into CI/CD.

1) Threat model: “What are we preventing?”

The first step is not a “best practices” list; it’s your own threat model. The scenarios I encounter most often in Docker security:

  • Supply chain: Poisoned layers in the base image, dependency typosquatting, tokens leaked through CI.
  • Secret leakage: ENV, build args, layer history, logs, crash dumps.
  • Privilege escalation: --privileged, CAP_SYS_ADMIN, docker.sock mount, kernel escape.
  • Lateral movement: Pivoting to other containers on the same host, internal network discovery, metadata services.
  • Operational risk: Wrong tag/pin, no rollback, drift, “it worked on my machine.”

Ask yourself these 4 questions:

QuestionGoalTypical control
If code inside the container is malicious, can it escape to the host?Reduce escape riskRootless, seccomp, caps drop
If a malicious dependency entered the image, can it reach prod?Close the supply chainSBOM, scan, sign, policy gate
Where can secrets leak?Reduce “accident” riskBuild secrets, runtime injection, audit
Can we triage quickly during an incident?Limit blast radiusLog/metric, immutable image, runbook

2) Image layer: Build-time security and hygiene

Many production incidents start before runtime: packages that go into the image, credentials used during build, the choice of base image…

My minimum standard:

  1. Minimal base image (small attack surface)
  2. Digest pinning (prevents tag drift)
  3. Multi-stage build (the build toolchain doesn’t carry over to runtime)
  4. Non-root user (don’t run as root by default)
  5. Deterministic build (lockfile + reproducibility)

Example: a “secure default” Dockerfile

# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS build
WORKDIR /app

# 1) Kilitli bağımlılıklar
COPY package.json package-lock.json ./
RUN npm ci

# 2) Kaynak + build
COPY . .
RUN npm run build

# Runtime image: daha küçük, daha az paket
FROM node:22-alpine AS runtime
WORKDIR /app

# 3) Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app

# 4) Sadece gerekli artefact’lar
COPY --from=build /app/dist ./dist

ENV NODE_ENV=production
CMD ["node", "dist/server/entry.mjs"]

This example isn’t “the best”; it aims for “the safest default.” If you want to go further:

  • Distroless instead of node:alpine (tooling/debug trade-off)
  • Package additions like apk add should be minimal and audited
  • Version pinning: FROM node@sha256:... (every build uses the same base)

3) Supply chain: SBOM, scan, signing (a scan isn’t enough)

Saying “I ran a scan” doesn’t mean “no risk.” A scan only gives you a list of “known CVEs.” For production security, I want all 3 pieces together:

  • SBOM (Software Bill of Materials): “What’s inside?”
  • Vulnerability scan: “Are there known holes?”
  • Signature/attestation: “Who built this image, is it the same one?”

In practice, the “minimum” line:

# SBOM + scan örneği (tool seçimi size ait)
trivy image --scanners vuln,secret --format table myapp@sha256:...

# İmzalama örneği (cosign)
cosign sign --key cosign.key myapp@sha256:...
cosign verify --key cosign.pub myapp@sha256:...

Policy gate logic: in CI, tie the questions “are there critical CVEs?” + “is it signed?” + “was an SBOM produced?” to the merge/deploy gate.

4) Secrets: not via build args/ENV, but via the right channel

Secret leakage is usually not “malicious intent” but bad habits:

  • ARG NPM_TOKEN=... (stays in image history)
  • ENV DB_PASSWORD=... (visible via docker inspect)
  • Copying a .env file into the image

The correct approach: don’t bake the secret into the image, inject it at runtime.

If a build-time secret is required (e.g. private registry)

Use BuildKit’s secret mount (it’s not written into a layer):

# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci

Injecting secrets at runtime

  • Docker secrets (Swarm) / orchestrator secret store
  • Kubernetes secret + CSI driver / external secret manager
  • Short-lived token (OIDC) + service identity

5) Runtime hardening: capabilities, seccomp, read-only FS

The runtime goal: even if the process inside the container “goes bad,” limit its impact.

Safe run flags (Docker run)

docker run --rm \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --pids-limit 256 \
  --memory 512m --cpus 1 \
  --security-opt no-new-privileges:true \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --user 10001:10001 \
  --network myapp-net \
  myapp@sha256:...

What we did here:

  • read-only rootfs: cuts file write capability (persistence becomes harder)
  • tmpfs: provides the writable area you need, but controlled (noexec/nosuid)
  • pids/memory/cpu limit: limits the impact of fork bombs / DoS
  • no-new-privileges: closes off “gain privileges later” paths like setuid
  • cap-drop: minimizes kernel privileges

seccomp / AppArmor / SELinux

Docker’s default seccomp profile isn’t bad, but for critical workloads you need a tighter profile. Minimum approach:

  • Disabling default seccomp (--security-opt seccomp=unconfined) is forbidden
  • AppArmor/SELinux on whenever possible
  • For prod, derive a “golden profile” (which syscalls do you actually need?)

6) Host and daemon: the most-forgotten layer

No matter how good your containers are, if the host side is weak, the game is over.

What I expect at minimum on the host side:

  • Kernel patches kept current, with a reboot discipline
  • Docker daemon access restricted (docker group membership under control)
  • Rootless preferred where possible
  • Logging/audit on and centralized
  • Image cache and registry access controlled

Sample hardening settings for the Docker daemon

{
  "live-restore": true,
  "no-new-privileges": true,
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "5" }
}

This file usually lives at /etc/docker/daemon.json. Don’t “copy-paste” every setting; test first.

7) Security gates in CI/CD: “the right to deploy” is a policy decision

Make security a pipeline rule, not a doc:

  1. On PR: SAST + secret scan
  2. On build: SBOM + vulnerability scan
  3. On publish: sign + provenance
  4. On deploy: policy check (signed? critical CVEs?)

The good thing about this approach: “you don’t have to trust anyone,” because the system already won’t allow it.

8) Quick note on Kubernetes: same principle, different mechanism

What you do in Docker with docker run, in Kubernetes you express via securityContext:

  • runAsNonRoot: true
  • readOnlyRootFilesystem: true
  • allowPrivilegeEscalation: false
  • capabilities: drop: ["ALL"]

And at the policy layer:

  • Pod Security Standards / PSA
  • Admission policy (OPA/Gatekeeper, Kyverno)
  • Lateral movement control with NetworkPolicy

9) Incident runbook: suspicious container behavior

If a container is behaving “weird” (unexpected outbound traffic, suspected crypto miner, spike):

  1. Network isolation: cut egress (in the first 2 minutes)
  2. Image + digest: which digest is running? Is it the same as in the registry?
  3. Process list: any unexpected processes? (ps, ss, lsof)
  4. Filesystem writes: if not read-only, which path was written to?
  5. Credential exposure: log access to ENV/volume/secrets
  6. Host signal: kernel audit / runtime alerts (Falco/eBPF)
  7. Root cause: supply chain, misconfig, or exploit?

Conclusion: Minimum secure profile (copy + adapt)

If you’re saying “I can’t do all of this at once,” sequence it like this:

  1. Digest pin + multi-stage + non-root
  2. Pull secrets out of the image (runtime injection)
  3. --cap-drop ALL + no-new-privileges + read-only rootfs
  4. SBOM + scan + sign + policy gate
  5. Host patching + rootless standard + audit/log pipeline

Security isn’t a single setting; it’s design + automation + operational rhythm. The best indicator: when a “bad image” arrives one day, does your system refuse to admit it to prod?

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Comments

Server-side AI Moderation

Comments are AI-moderated server-side and stored permanently.

?
0/2000

Server-side AI moderation

✉️ Free · No spam · Unsubscribe anytime

Curated digest, hand-picked by me — not the AI

Once a week: the most important post of the week, behind-the-scenes notes, and a "what I actually used this week" section. Less noise, more signal.

  • 📌
    Best of the week Single most-worth-reading post
  • 🔧
    Toolbox notes Real tools I used this week
  • 🧠
    Behind-the-scenes Notes that don't make it to blog

We don't spam. Unsubscribe anytime. · Tracked only by Umami (self-hosted, no Google).

Your Reading Stats

0

Posts Read

0m

Reading Time

0

Day Streak

-

Favorite Category

Related Posts