Docker Multi-Stage Build ile Güvenli İmajlar

Docker imajlarını üretim ortamına taşıdığınızda iki sorun aynı anda ortaya çıkar: imaj boyutu ve saldırı yüzeyi. Multi-stage build bu ikisini birlikte...

Docker Multi-Stage Build ile Güvenli İmajlar

Docker imajlarını üretim ortamına taşıdığınızda iki sorun aynı anda ortaya çıkar: imaj boyutu ve saldırı yüzeyi. Multi-stage build bu ikisini birlikte ele almanın en pratik yoludur. Aşağıdaki rehber, kavramı adım adım kurar ve ardından güvenlik sıkılaştırmasını katman katman anlatır.

Multi-Stage Build Neden Gerekli

Tek aşamalı bir Dockerfile'da derleyici, paket yöneticisi, test araçları ve kaynak kod aynı imaja girer. Bu imaj çalışma zamanında bu araçların hiçbirine ihtiyaç duymaz; ama hepsi saldırganın erişebileceği hâlde kalır. Multi-stage build, derleme ortamını çalışma ortamından ayırır. Sonuç imaja yalnızca çalıştırmak için gereken dosyalar kopyalanır.

Mekanizma FROM ... AS <isim> ile başlar. Her FROM yeni bir aşama açar; önceki aşamaların dosya sistemi sonraki aşamalara otomatik olarak taşınmaz. Yalnızca COPY --from=<aşama> ile açıkça seçtiğiniz dosyalar geçer.

Önkoşullar ve Sürüm Kapsamı

  • Docker Engine ve BuildKit tabanlı docker build. Multi-stage sözdizimi uzun süredir kararlıdır; COPY --from desteği standarttır.
  • --mount=type=cache gibi BuildKit'e özgü özellikler için BuildKit etkin olmalıdır. Bu özellikler klasik derleyicide çalışmaz.
  • Aşağıdaki örnekler Node.js tabanlı bir uygulama üzerinden ilerler. Sürüm etiketlerini kendi projenizin gereksinimine göre sabitleyin; burada belirli bir sürüm numarası önermiyorum, çünkü doğru seçim projeye bağlıdır.

Temel Multi-Stage Yapı

İki aşamalı tipik bir akış: birinci aşamada bağımlılıkları kurup derleme yaparsınız, ikinci aşamada yalnızca çıktıyı taşırsınız.

# --- Derleme aşaması ---
FROM node:20-alpine AS builder
WORKDIR /app

# Önce manifest dosyalarını kopyala: katman önbelleği bağımlılıklara bağlanır
COPY package.json package-lock.json ./
RUN npm ci

# Kaynak kodu kopyala ve derle
COPY . .
RUN npm run build

# --- Çalışma aşaması ---
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

# Yalnızca üretim bağımlılıkları
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# Derleme çıktısını builder'dan al
COPY --from=builder /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Burada dikkat edilecek noktalar:

  • COPY package.json package-lock.json ./ satırı COPY . . satırından önce gelir. Manifest değişmediği sürece bağımlılık katmanı önbellekten gelir; yalnızca kaynak değiştiğinde derleme yeniden çalışır.
  • npm ci kilit dosyasına birebir uyar ve npm install'a göre daha öngörülebilirdir.
  • Çalışma aşamasında npm ci --omit=dev kullanılır; geliştirme bağımlılıkları imaja girmez.
  • node imajında hazır gelen node kullanıcısına USER ile geçilir.

Beklenen davranış: docker build sonrası son imaj, builder aşamasındaki derleyici ve geliştirme bağımlılıklarını içermez. docker image ls çıktısında boyutun tek aşamalı sürüme göre belirgin biçimde küçüldüğünü görürsünüz; kesin oran projeye göre değişir.

.dockerignore Olmadan Bu Yapı Yarım Kalır

COPY . . satırı, .dockerignore yoksa node_modules, .git, yerel .env dosyaları ve derleme çıktılarını da bağlama katar. Bu hem derlemeyi yavaşlatır hem de sırların imaja sızma riskini artırır.

node_modules
.git
.env
.env.*
dist
*.log

.git klasörünün imaja girmesi, geçmişteki sırların ve iç yapının dışarı sızması anlamına gelir. .env dosyasının girmesi ise doğrudan kimlik bilgisi sızıntısıdır.

Güvenlik Sıkılaştırma

Kök Olmayan Kullanıcı

Varsayılan olarak konteyner içindeki süreç root olarak çalışır. Konteyner kaçışı senaryosunda bu, host üzerinde geniş yetki demektir. Çalışma aşamasında USER ile ayrıcalıksız bir kullanıcıya geçin. Alpine tabanlı imajlarda node kullanıcısı hazır gelir; Debian tabanlı imajlarda kullanıcıyı siz oluşturmanız gerekir:

RUN groupadd --system app && useradd --system --gid app app
USER app

Sabitlenmiş Taban İmaj

node:latest gibi kayan etiketler, derlemenin zaman içinde sessizce değişmesine yol açar. Bunun yerine belirli bir sürüm etiketi kullanın. Daha katı bir yaklaşım, imajı digest ile sabitlemektir:

FROM node:20-alpine@sha256:<digest>

Digest sabitleme, etiketin arkasındaki içeriğin değişmemesini garanti eder. Takas: güvenlik güncellemelerini almak için digest'i elle güncellemeniz gerekir.

Derleme Sırlarını İmaja Yazmamak

ARG ve ENV ile verilen değerler imaj geçmişinde görünür. Özel bir npm registry token'ı veya benzeri bir sır gerekiyorsa, BuildKit'in secret mount mekanizmasını kullanın:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

Derleme sırasında docker build --secret id=npmrc,src=.npmrc . ile verilir. Sır, katmana yazılmaz ve son imajda bulunmaz.

Gereksiz Araçları Çıkarmak

Çalışma imajına kabuk, paket yöneticisi veya ağ araçları bırakmak saldırgana işini kolaylaştırır. Alpine tabanlı imajlar bu açıdan görece yalındır. Daha ileri bir adım, yalnızca çalışma zamanı kütüphanelerini içeren distroless benzeri taban imajlara geçmektir. Bu imajlarda kabuk bulunmaz; hata ayıklama zorlaşır, bu yüzden geçiş bilinçli bir tercih olmalıdır.

Sağlık Kontrolü ve Çalışma Zamanı Kısıtları

Dockerfile'a HEALTHCHECK eklemek, bozuk bir konteynerin trafik almasını engellemeye yardımcı olur:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:3000/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"

Çalıştırma sırasında ek kısıtlar uygulanabilir: kök dosya sistemini salt okunur yapmak, geçici dizinleri tmpfs olarak bağlamak, Linux yeteneklerini düşürmek. Bunlar Dockerfile'ın değil, çalıştırma yapılandırmasının konusudur; ama imaj tasarımı bunları mümkün kılacak şekilde yapılmalıdır. Örneğin uygulama yazma gereksinimini yalnızca belirli bir dizine yönlendirmek, salt okunur kök dosya sistemini uygulanabilir kılar.

Yaygın Hatalar ve Etkileri

  • COPY . . satırını manifest kopyalamadan önce kullanmak. Her kaynak değişikliğinde bağımlılıklar yeniden kurulur; derleme süresi artar. Güvenlik etkisi yoktur, ama geri bildirim döngüsü uzar.
  • Sırları ARG/ENV ile geçirmek. Değer imaj geçmişinde kalır ve imajı çeken herkes görebilir. docker history ile ortaya çıkar.
  • Çalışma aşamasında USER belirtmemek. Konteyner root çalışır; kaçış senaryosunda etki alanı büyür.
  • Geliştirme bağımlılıklarını üretim imajına bırakmak. Hem boyut artar hem de ek araçlar saldırı yüzeyi oluşturur.
  • .dockerignore'u atlamak. .git ve .env imaja girer; sızıntı riski doğar.
  • Kayan taban etiketi kullanmak. Aynı Dockerfile farklı zamanlarda farklı imaj üretir; üretimde tekrarlanabilirlik kaybolur.

Doğrulama

İmajı oluşturduktan sonra şu kontrolleri yapabilirsiniz:

  • docker image history <imaj> ile katmanları ve boyutlarını inceleyin; beklenmedik katman var mı bakın.
  • docker run --rm <imaj> id ile sürecin hangi kullanıcıyla çalıştığını doğrulayın.
  • docker run --rm <imaj> ls /app ile çalışma aşamasında beklenmeyen dosya olup olmadığını kontrol edin.
  • İmaj taraması için docker scout cves <imaj> veya tercih ettiğiniz bir tarayıcı kullanın.

Bu kontroller, yapılandırmanın gerçekten amaçlandığı gibi sonuçlandığını gösterir; varsayımla ilerlemek yerine imajın kendisini ölçmek gerekir.

Özet

Multi-stage build iki işi birden yapar: son imajı yalnızca çalışma zamanı gereksinimleriyle sınırlar ve derleme araçlarını üretimden uzak tutar. Güvenlik sıkılaştırması bunun üzerine eklenir: ayrıcalıksız kullanıcı, sabitlenmiş taban imaj, imaja yazılmayan sırlar ve gereksiz araçların çıkarılması. Her adımı imajın kendisini inceleyerek doğrulayın; boyut ve katman içeriği, yapılandırmanın doğru çalıştığının en somut göstergesidir.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz