A woman uses her laptop in a dimly lit server room, focusing on technology and work.
Docker Güvenliği: Bir DevOps Mühendisinin Kontrol Listesi (İmajdan Runtime’a)

Bir cuma akşamı ekipten bir mesaj geldi: “Production’da bir konteyner garip davranıyor.” Falco alarm çalmıştı — beklenmedik /bin/sh spawn’ı, sonra curl çağrısı, sonra başka bir IP’ye giden trafik. İmaj üç ay önce build edilmişti ve içindeki bir node modülünde sonradan açıklanan zafiyet vardı. Saldırgan modülü tetikleyip içeri girmiş, oradan da yan konteynerlere atlamaya çalışıyordu. Konteyner root değil çalıştığı için iş orada durdu — ama dersi unutmadık. Docker güvenliği öyle “şu üç şeyi yap, tamam” işi değil; her katmanda alacağınız küçük önlemlerin birikimi. Bu yazıda o katmanlarda nelere dikkat ettiğimi tek tek anlatacağım.

İmajdan başlayalım: Az olan az saldırılır

Konteyner güvenliğinin ilk katmanı imajınız. Base image seçimi öncelikle. Standart bir python:3.11 imajı 900 MB civarı; içinde Debian, paket yöneticisi, GCC, perl, curl, vim… Hepsi gerekirse mantıklı; gerekmiyorsa hepsi saldırı yüzeyi. Aynı runtime’ın python:3.11-alpine versiyonu 50 MB. python:3.11-slim ortada bir yerde, ~120 MB. Pratiğim: önce slim, gerçekten daha küçük gerekiyorsa alpine.

Docker güvenliği: build → runtime → monitoring katmanları
Docker güvenliği: build → runtime → monitoring katmanları

Alpine’in tek dezavantajı musl libc kullanması — bazı Python paketleri (pandas, numpy, lxml gibi C bağımlılığı olanlar) prebuilt wheel’i alpine için yok, build zamanı uzuyor. O durumda slim mantıklı.

Tag’i sabitleyin. python:latest yazmayın. Hatta python:3.11 bile yazmayın — minor yükselse tüm dünyanız kırılabilir. python:3.11.7-slim-bookworm gibi spesifik tag kullanın. Build tekrarlanabilir olsun. Güncellemeyi bilinçli yapın, otomatik sürpriz olarak değil.

Multi-stage build’i mutlaka kullanın. Build aşamasında compiler, package manager, geliştirme araçları gerekiyor; final imajda hiçbiri olmamalı. Go uygulaması için klasik örnek: ilk stage’de kaynak kodu derlenir, ikinci stage scratch ya da distroless base’ine sadece binary kopyalanır. Final imaj 10-20 MB, içinde shell bile yok. Saldırgan içeri girse bash çağıramıyor, perl çalıştıramıyor; saldırı hareketi sıfıra yakın.

İmaj taraması: Trivy ile bir günlük rutini

Her imajı tarayın. Trivy en kolayı; ücretsiz, hızlı, CI/CD’ye 5 dakikada giriyor. Snyk Container ve Azure Defender for Containers daha kapsamlı ama ücretli. Pipeline’ınızda şu kuralı koyun: Critical seviyesinde tek bir CVE varsa build düşsün, High’larda günde belli sayıyı geçerse uyarı versin. Sıfır tolerans yapmazsanız ekip görmezden gelmeye başlar; aşırı sıkı yaparsanız üretime hiç çıkamazsınız. Bu eşiği takımınızla konuşarak belirleyin.

Bir de şunu ihmal etmeyin: periyodik tarama. Bir imaj build edildiği gün temiz olabilir; ama üç ay sonra yeni CVE açıklanır, imaj hâlâ üretimde, kimse bilmiyor. Registry’nizi haftada bir tarayın — Azure Container Registry’de bu özellik kutudan çıkıyor, başka registry için cron + Trivy yeterli.

Runtime: “Root değil çalıştır, çoğu şey çözülür”

Yazının başındaki Falco alarm hikâyesinin iyi bitmesinin tek sebebi konteynerin root değil çalıştırılıyor olmasıydı. Saldırgan içeri girdi ama yetki yetmediği için yatay hareket edemedi.

Dockerfile’a USER direktifi eklemek yarım dakikalık iş. RUN useradd -r appuser && USER appuser — tamam. Alpine’de RUN adduser -D appuser && USER appuser. Bunu yapmamak için bir mazeret yok. Eski uygulamalarınız varsa (port 80’e bind eden bir Java app gibi), portu 8080’e taşıyıp önüne ingress/proxy koyun, root gerek kalmasın.

Read-only filesystem

--read-only flag’iyle başlatılan bir konteyner dosya sistemine yazamıyor. Saldırganın binary indirmesini, persist etmesini engeller. Uygulama log veya geçici dosya yazmak zorundaysa yalnızca o dizini tmpfs olarak mount edersiniz: --tmpfs /tmp --tmpfs /var/run. Logları stdout’a yazıyorsanız (12-factor) zaten read-only sorunsuz çalışır.

Capability kısıtlaması: “Hangi yetkiler gerçekten lazım?”

Linux capability’ler root yetkisini parçalara böler. Docker varsayılanda 14 tane verir — fazlası kaldırılmış. Ama “varsayılan” sizin uygulamanız için gereken minimum değil. --cap-drop=ALL ile hepsini düşürün, sonra ihtiyacınızı tek tek --cap-add ile ekleyin. Bir HTTP sunucusu için tipik olarak sadece NET_BIND_SERVICE gerekir (1024 altı portlara bind için, ki onu da çoğu zaman atlıyoruz). Veritabanı belki SYS_RESOURCE ister. Diğer 95 capability’ye dokunmak zorunda kalmazsanız dokunmayın.

Bir de --security-opt no-new-privileges:true. Konteynerin içinde setuid binary çalıştırılsa bile yeni privilege elde edemez. Default olmamalı mı diyorsunuz — evet, olmalı, ama değil. Siz açın.

Secret yönetimi: “Lütfen .env’i imaja kopyalamayın”

Bir müşteride kod review yaparken Dockerfile’da şunu gördüm: COPY .env /app/.env. “Bu .env içinde production database parolası mı?” diye sordum, “Evet, ama imajımız private registry’de” dediler. Açıkladım: imajınız yıllarca o registry’de duracak, bin tane developer indirecek, geçen ayın imajından çıkarılan layer’lar bin yere kopyalanmış olabilir, ve hayır — “private registry” parola yönetimi değildir.

Kural: secret kesinlikle imaja girmez. Dockerfile’da ENV DB_PASSWORD= da olmaz; layer history’de görünüyor.

Doğru yöntemler: Docker Swarm’da Docker Secrets, Kubernetes’te Kubernetes Secrets (ideal değil ama temel; tercihen Sealed Secrets veya External Secrets Operator), HashiCorp Vault, Azure Key Vault. Runtime’da volume mount ile veya env var ile veriyorsunuz. Volume mount tercihim — env var docker inspect ile okunabiliyor, biraz daha sızıntı vektörü.

Bir not: Azure Key Vault + Workload Identity (AKS’de) kombinasyonu çok güzel çalışıyor. Pod’a credential vermek yok; pod kendi Azure identity’siyle Key Vault’tan secret çekiyor. Rotation otomatik. Bir kez kurun, unutun.

Ağ izolasyonu: “Default bridge’i kullanmayın”

Docker varsayılan olarak tüm konteynerleri aynı bridge ağına atar. İletişim her yöne açık. Üretim için kötü. Her uygulama bileşeni için ayrı network tanımlayın: web, app, db. Web yalnızca app’a, app yalnızca db’ye konuşabilsin. Bu ayrım docker compose’da iki dakikalık iş; Kubernetes’te NetworkPolicy ile yapılır (ama önce CNI’nizin desteklediğinden emin olun — Calico, Cilium destekler, default’ta her zaman değil).

Port yayınlarken -p 8080:80 yerine -p 127.0.0.1:8080:80 kullanın eğer dış dünyaya açmanız gerekmiyorsa. Reverse proxy arkasındaysa konteyner port yayınlamasın hiç — proxy ile internal network üzerinden konuşsun.

Container registry tarafı: “İmajı kim push etti?”

Public Docker Hub’ı production için kullanmayın. Kendi registry’nizi kurun — Azure Container Registry, ECR, GCR, hangisi sizin ortamınızdaysa. Public imajları çekmek istiyorsanız bile, mirror edin. Public Hub kotaları (rate limit) sizi production’da öğretmensiz bırakabilir.

ACR’de content trust açabiliyorsunuz — push edilen imajlar imzalanmak zorunda. Cosign ile imzalayan ekibiniz varsa daha güzel. Kubernetes tarafında admission controller (Kyverno, OPA Gatekeeper) ile “sadece imzalanmış imaj cluster’a girsin” kuralı koyabilirsiniz. Supply chain saldırıları (SolarWinds tipi) bu katmanla durduruluyor.

ACR Tasks bir başka iyi özellik: base image güncellendiğinde, üzerine inşa eden imajlarınızı otomatik rebuild eder. Alpine 3.18 için patch çıktığında, ona dayanan 50 imajınız otomatik yeniden derlenir, yeni güvenli versiyonlar registry’ye girer. Manuel müdahale yok.

Runtime izleme: “Falco gerçekten alarm çalıyor mu?”

Yazının başındaki hikayemizin kahramanı Falco’ydu. Ücretsiz, açık kaynak, kernel-level events izliyor. “Beklenmeyen process spawn”, “hassas dosyaya yazma”, “yeni dış bağlantı” gibi olayları yakalıyor ve uyarı gönderiyor. Sysdig Secure ticari versiyonu, Azure Defender for Containers Microsoft tarafının cevabı.

Hangisini kullanırsanız kullanın, kuralları kalibre edin. İlk hafta inanılmaz çok false positive geliyor — herkes Falco’yu kapatıyor. Kapatmayın. Bir hafta gözlemleyin, normal davranışı baseline yapın, kuralları daraltın. İkinci hafta sonunda gerçek saldırı sinyali ile gürültü ayırt edilmeye başlıyor.

Docker Compose tarafında bile aynı disiplin

Geliştirme ortamı production’a giden alışkanlıkların doğum yeridir. Compose dosyasında security_opt: ["no-new-privileges:true"], read_only: true, cap_drop: [ALL], mem_limit, cpus bunları yazın. Geliştirici makinesinde bile read-only filesystem’le çalışmak zor olabilir başlarda; ama ekibi alıştırırsanız production’a geçişte sürpriz yok.

Dockerfile: Pratik kontrol listesi

Her yeni Dockerfile yazarken zihnimden geçen liste şu:

Base image — resmi mi, slim/alpine mi, tag spesifik mi? RUN’lar birleştirilmiş mi (layer az), apt cache temizleniyor mu? COPY mı kullanıyoruz, ADD’den uzak mıyız? Multi-stage var mı? USER direktifi var mı? HEALTHCHECK tanımlı mı? .dockerignore dosyası var mı (yoksa .git klasörü imaja girer, bin türlü gereksiz şey gelir)? EXPOSE belgeleyici amaçlı yazılmış mı?

Bu listenin tamamı işaretlendiğinde, çoğu güvenlik denetimini çoktan geçtiniz demektir.

Üç yaygın soru

Docker Hub’dan çekilen imajlara güvenilir mi? Resmi (Official) işaretli imajlar Docker tarafından yönetiliyor; makul güven verir, ama production için bile mirror edip kendi registry’nizden çekin. Community imajları? Kim yayınladıysa o sorumlu — yani zorla güvenmeyin. Resmi olsa bile zafiyet çıkar; periyodik tarayın.

Rootless Docker hayatımı kolaylaştırır mı? Daemon’ı root değil çalıştırma yöntemi. Daemon’a saldırı geldiğinde host yetkisi kazanılamıyor. Docker 20.10+ destekliyor. Podman alternatif olarak baştan rootless çalışıyor. Yeni projelerde Podman’ı bir düşünün — özellikle Kubernetes’e geçeceğiniz uzun vadeli senaryolarda.

Docker Bench Security nedir, çalıştırayım mı? CIS Docker Benchmark’a göre host’unuzu denetleyen bir script. Bir kez çalıştırın, çıkan uyarıları gözden geçirin. Üretime almadan önce mutlaka bir kez geçirin — bedava ve yarım saatlik bir iş.

Sonuç

Docker güvenliği tek bir büyük çözüm değil; her katmanda alınan küçük önlemlerin birikimi. İmajınız temiz, kullanıcınız root değil, dosya sisteminiz read-only, capability’leriniz dar, secret’larınız Vault’ta, ağınız segmented, runtime izleyiciniz alarm çalıyor. Bu altı maddenin her biri tek başına bir koruma değil; birlikte yapısal bir kalkan oluyor. O cuma akşamı bizim ekibin geceyi rahat geçirmesinin sebebi de buydu — saldırgan içeri girdi ama bütün katmanları aşamadı.

CloudSpark konteyner güvenliği ve DevSecOps hizmetleri kapsamında Docker imaj sertleştirmeden Falco kalibrasyonuna, Azure Container Registry yapılandırmasından Workload Identity entegrasyonuna kadar ekiplerle çalışıyoruz.

🇹🇷 Türkçe🇬🇧 English🇩🇪 Deutsch🇫🇷 Français🇸🇦 العربية🇷🇺 Русский🇪🇸 Español