Docker güvenliği çoğu ekipte “image scan yaptık, bitti” seviyesinde ele alınıyor. Üretimde ise risk yüzeyi daha geniş: image supply chain, runtime konfigürasyonu ve host/daemon katmanı birlikte düşünülmezse, bir gün “container içinden host’a çıkıldı” vakasıyla uyanırsınız.
Bu rehber, Docker’ı “tek bir tool” değil, bir işletim modeli olarak ele alır. Amacım akademik değil: sahada uygulanabilir, ölçülebilir ve CI/CD’ye bağlanabilir bir kontrol listesi vermek.
1) Tehdit modeli: “Neyi engelliyoruz?”
İlk adım “en iyi pratikler” listesi değil; kendi tehdit modeliniz. Docker güvenliğinde en sık gördüğüm senaryolar:
- Supply chain: Base image’e zehirli layer, dependency typosquatting, CI’da sızan token.
- Secrets sızıntısı: ENV, build arg, layer history, log, crash dump.
- Yetki yükseltme:
--privileged,CAP_SYS_ADMIN,docker.sockmount, kernel escape. - Lateral movement: Aynı host’ta başka container’lara geçiş, iç ağ keşfi, metadata servisleri.
- Operasyonel risk: Yanlış tag/pin, rollback yok, drift var, “benim makinemde çalışıyordu”.
Kendinize şu 4 soruyu sorun:
| Soru | Amaç | Tipik kontrol |
|---|---|---|
| Container içi kod kötü niyetliyse host’a çıkar mı? | Escape riskini azaltmak | Rootless, seccomp, caps drop |
| Image’e zararlı bağımlılık girdiyse prod’a çıkar mı? | Supply chain’i kapatmak | SBOM, scan, sign, policy gate |
| Secret’lar nereye sızabilir? | “Kaza” riskini azaltmak | Build secrets, runtime injection, audit |
| Incident’ta hızlı triage yapabilir miyiz? | Hasarı sınırlamak | Log/metric, immutable image, runbook |
2) Image katmanı: Build-time güvenlik ve hijyen
Üretimdeki pek çok incident, runtime’dan önce başlar: image’in içine giren paketler, build sırasında kullanılan credential’lar, base image seçimi…
Benim minimum standardım:
- Minimal base image (attack surface küçük)
- Digest pinleme (tag drift engeli)
- Multi-stage build (build toolchain runtime’a taşınmaz)
- Non-root user (default olarak root çalışmaz)
- Deterministic build (lockfile + reproducibility)
Örnek: “güvenli varsayılan” 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"]
Bu örnek “en iyi” değil; “en güvenli varsayılanı” hedefliyor. Daha ileri gidecekseniz:
node:alpineyerine distroless (tooling/debug trade-off)apk addgibi paket ekleme minimal ve audit’li- Sürüm pinleme:
FROM node@sha256:...(her build aynı base)
3) Supply chain: SBOM, tarama, imzalama (scan yetmez)
“Scan yaptım” demek, “risk yok” demek değildir. Scan size sadece “bilinen CVE” listesini verir. Üretimde güvenlik için ben 3 parçayı birlikte isterim:
- SBOM (Software Bill of Materials): “İçinde ne var?”
- Vulnerability scan: “Bilinen açık var mı?”
- Signature/attestation: “Bu image’i kim üretti, aynı mı?”
Pratikte “minimum” çizgi:
# 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 mantığı: CI’da “kritik CVE var mı?” + “imza var mı?” + “SBOM üretildi mi?” sorularını merge/deploy kapısına bağlayın.
4) Secrets: Build arg/ENV ile değil, doğru kanalla
Secrets sızıntısı genelde “kötü niyet” değil, yanlış alışkanlık:
ARG NPM_TOKEN=...(image history’de kalır)ENV DB_PASSWORD=...(docker inspect ile görünür).envdosyasını image’e kopyalama
Doğru yaklaşım: Secret’ı image’e gömmemek, runtime’da enjekte etmek.
Build-time secret gerekiyorsa (ör. private registry)
BuildKit ile secret mount kullanın (layer’a yazılmaz):
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
Runtime secret enjekte etme
- Docker secrets (Swarm) / orchestrator secret store
- Kubernetes secret + CSI driver / external secret manager
- Kısa ömürlü token (OIDC) + service identity
5) Runtime hardening: Capabilities, seccomp, read-only FS
Runtime tarafında hedef: container içindeki proses “kötüleşirse” bile etkisini sınırlamak.
Güvenli çalıştırma bayrakları (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:...
Burada yaptıklarımız:
- read-only rootfs: dosya yazma kabiliyetini düşürür (persistence zorlaşır)
- tmpfs: gerekli yazılabilir alanı kontrollü verir (noexec/nosuid)
- pids/memory/cpu limit: fork bomb / DoS etkisini sınırlar
- no-new-privileges: setuid gibi “sonradan yetki alma” yollarını kapatır
- cap-drop: kernel yetkilerini en aza indirir
seccomp / AppArmor / SELinux
Docker’ın default seccomp profili fena değildir ama kritik iş yüklerinde daha sıkı profil gerekir. Minimum yaklaşım:
- Default seccomp’u kapatma (
--security-opt seccomp=unconfined) yasak - Mümkünse AppArmor/SELinux açık
- Prod için “golden profile” çıkar (hangi syscall’lar gerekli?)
6) Host ve daemon: En çok unutulan katman
Container’larınız ne kadar iyi olursa olsun, host tarafı zayıfsa oyun biter.
Benim host tarafında beklediğim minimumlar:
- Kernel patch seviyesi güncel, reboot disiplini var
- Docker daemon erişimi sınırlandırılmış (
dockergrup üyeliği kontrol altında) - Rootless mümkünse tercih edilmiş
- Logging/audit açık ve merkezi
- Image cache ve registry erişimi kontrollü
Docker daemon için örnek sertleştirme ayarları
{
"live-restore": true,
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "5" }
}
Bu dosya genelde /etc/docker/daemon.json altındadır. Her ayarı “kopyala-yapıştır” yapmayın; önce test edin.
7) CI/CD’de güvenlik kapıları: “Deploy hakkı” bir policy kararıdır
Güvenliği “doküman” değil, pipeline kuralı haline getirin:
- PR’da: SAST + secret scan
- Build’te: SBOM + vulnerability scan
- Publish’te: sign + provenance
- Deploy’de: policy check (imza var mı? kritik CVE var mı?)
Bu yaklaşımın iyi yanı: “kimseye güvenmek zorunda kalmazsınız”, sistem zaten izin vermez.
8) Kubernetes’e kısa not: Aynı prensip, farklı mekanizma
Docker’da docker run ile yaptığınız çoğu şey Kubernetes’te securityContext ile gelir:
runAsNonRoot: truereadOnlyRootFilesystem: trueallowPrivilegeEscalation: falsecapabilities: drop: ["ALL"]
Ve politika katmanında:
- Pod Security Standards / PSA
- Admission policy (OPA/Gatekeeper, Kyverno)
- NetworkPolicy ile lateral movement kontrolü
9) Incident runbook: Şüpheli container davranışı
Bir container “garip” davranıyorsa (beklenmeyen outbound, crypto miner şüphesi, spike):
- Ağ izolasyonu: Egress’i kes (ilk 2 dakika)
- Image + digest: Hangi digest koşuyor? Registry’den aynı mı?
- Process list: Beklenmeyen proses var mı? (
ps,ss,lsof) - Filesystem writes: read-only değilse hangi path’e yazmış?
- Credential exposure: ENV/volume/secrets erişimi logla
- Host sinyali: Kernel audit / runtime alerts (Falco/eBPF)
- Kök neden: Supply chain mi, misconfig mi, exploit mi?
Sonuç: Minimum güvenli profil (kopyala + uyarlayın)
Eğer “hepsini bir anda yapamam” diyorsanız, sırayı şöyle koyun:
- Digest pin + multi-stage + non-root
- Secret’ı image’den çıkar (runtime injection)
--cap-drop ALL+no-new-privileges+ read-only rootfs- SBOM + scan + sign + policy gate
- Host patch + rootless standardı + audit/log pipeline
Güvenlik, tek bir ayar değil; tasarım + otomasyon + operasyon ritmi. En iyi gösterge de şudur: Bir gün “kötü image” geldiğinde sisteminiz onu prod’a sokmuyor mu?