docker run --privileged -v /:/host alpine:latest komutunu çalıştırdığın anda, o 'güvenli' sandbox'ın sadece bir illüzyondan ibaret olduğunu anlarsın; artık host işletim sistemi senin oyun alanındır.
Selam millet. Bugün şu meşhur konteyner güvenliği meselesine, teknik direktör koltuğundan değil, bizzat sahada defans yapan ve o defansı yarmaya çalışan bir Red Teamer gözüyle bakacağız. Konteyner dediğimiz şey, çoğunuzun bildiği üzere bir sanal makine (VM) değil. Kernel'ı paylaşan, sadece namespace ve cgroups ile sınırlandırılmış birer süreç (process) kümesi. Ama biz sanki etraflarında aşılmaz duvarlar varmış gibi davranıyoruz. testCompany'de yaptığımız testlerde gördüğümüz en büyük hata, bu ince çizginin nerede bittiğini kestirememek.
Docker Socket: Evin Anahtarını Paspasın Altına Koymak
Birçok CI/CD pipeline'ında veya izleme aracında docker.sock dosyasının konteyner içine mount edildiğini görüyorum. Eğer bir saldırgan, bu socket'e erişimi olan bir konteynerde kod çalıştırmayı başarırsa, oyun biter. Docker socket, Docker API'sine tam erişim demektir.
Zararsızlaştırılmış bir senaryo düşünelim. Saldırgan konteyner içinden şu komutu çalıştırdığında aslında ne oluyor?
# Docker socket erişimi olan bir saldırganın host'ta root olması
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["/bin/sh", "-c", "chroot /host /bin/sh"], "Binds": ["/:/host"]}' \
http://localhost/containers/create
Bu istek, Docker daemon'a yeni bir konteyner yaratmasını ama host'un root dizinini kendi içine mount etmesini söyler. Sonuç? Host makinede tam yetkili bir shell. Eğer docker.sock dosyasını bir konteynere veriyorsan, o konteynerin artık host'un kendisi olduğunu kabul etmişsin demektir. Çözüm basit: Socket'i asla paylaşma. Eğer Docker API ile konuşman gerekiyorsa, bunu kısıtlı bir proxy arkasından veya mTLS kullanarak yap.
Privileged Konteynerler ve Capabilities
Bir de şu meşhur --privileged bayrağı var. 'Ya uygulama hata veriyor, yetki yetmiyor' diyerek bu bayrağı açan devops'çu arkadaşım, aslında bombanın pimini çektin. Privileged bir konteyner, host üzerindeki neredeyse tüm kernel yeteneklerine (capabilities) sahip olur.
Kernel yetenekleri (Linux Capabilities), root yetkisini parçalara böler. Örneğin CAP_NET_BIND_SERVICE sadece düşük portları açmana izin verirken, CAP_SYS_ADMIN İsviçre çakısı gibidir; mount yapabilir, namespace'leri değiştirebilirsin.
Red Team operasyonlarında en sevdiğimiz şey, gereğinden fazla yetki verilmiş konteynerlerdir. Örneğin, bir konteyner sadece CAP_SYS_PTRACE yetkisine sahipse, host üzerindeki diğer süreçlerin belleğini okuyabilir ve belki de oradaki bir admin parolasını dump edebilir.
Ne yapmalı?
Saldırı yüzeyini daraltmak için 'Least Privilege' prensibi burada da geçerli. Dockerfile'ınızda veya Kubernetes deployment'ınızda securityContext kullanın:
# Kubernetes için güvenli bir örnek
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
İmajların İçindeki Hayaletler: Tedarik Zinciri Saldırıları
Herkes FROM python:3.9 veya FROM node:latest yazıp geçiyor. Ama o imajın içindeki 400 tane paketin kaç tanesinde kritik açık var? Veya daha kötüsü, kullandığın o base image'ı kim hazırladı?
Geçenlerde bir testte, popüler ama resmi olmayan bir imajın içine gömülmüş gizli bir reverse shell scripti ile karşılaştık. Geliştirici bunu 'kolaylık olsun diye' eklemiş ama dışarıdan bir saldırganın o scripti tetiklemesi çocuk oyuncağı.
İmaj güvenliği için şu üç kuralı aklına kazı:
- Distroless İmajlar Kullan: İçinde
curl,wget,pythonhattasholmayan imajlar kullanırsan, saldırgan içeri sızsa bile hareket alanı kalmaz. - Image Signing: Imajlarını imzala (Cosign vb. araçlarla). Pipeline'da sadece imzalı imajların deploy edilmesine izin ver.
- Zafiyet Tarama: Trivy veya Grype gibi araçları CI/CD'ye entegre et. 'Critical' seviyesinde açığı olan imaj production'a çıkamasın.
Secrets: Ortam Değişkenleri Çöp Kutusu Değildir
DATABASE_PASSWORD=123456 şeklinde bir environment variable gördüğümde gözlerim yaşarıyor (mutluluktan değil, Red Team tarafında işim çok kolaylaştığı için). docker inspect komutunu çalıştıran herkes o parolayı görebilir. Daha da kötüsü, /proc/self/environ üzerinden bu bilgilere sızmak çok kolay.
Konteyner dünyasında gizli bilgileri yönetmek için Docker Secrets veya Kubernetes Secrets (ki onlar da default olarak sadece base64'tür, yani şifreli değildir!) kullanın. Ama en sağlamı, HashiCorp Vault gibi bir dış sistemden bu bilgileri runtime'da çekmektir.
Runtime Security: Olay Anında Orada Olmak
Statik taramalar iyidir ama runtime güvenliği hayat kurtarır. Bir konteynerin normalde hiç yapmadığı bir şeyi yaptığını (örneğin /etc/shadow dosyasını okumaya çalışması veya dışarıya garip bir IP'ye bağlantı açması) nasıl anlarsın?
Falco gibi araçlar burada devreye giriyor. Kernel seviyesinde syscall'ları (sistem çağrılarını) izleyerek 'Hey, bu konteyner neden execve yaptı?' diye alarm üretebilirler.
# Örnek bir Falco kuralı mantığı (pseudo)
- rule: Terminal shell in container
desc: Bir konteyner içinde shell açıldı
condition: container.id != host and proc.name = sh
output: "Shell opened in container (user=%user.name %container.info)"
priority: WARNING
Kapanış Yerine: Defansif Kas Hafızası
Konteyner güvenliği bir 'checklist' değil, bir süreçtir. 'Konteyner kullandık, her şey izole' kafasından çıkıp, 'Bu konteyner patlarsa yanındakine nasıl sıçrar?' sorusunu sormaya başladığın an gerçek güvenliğe bir adım yaklaşırsın.
testCompany'deki testlerimizde gördüğümüz üzere, en karmaşık sistemler bile bazen basit bir chmod 777 veya unutulmuş bir yetkiyle devrilebiliyor. Dockerfile yazarken elin --privileged bayrağına gitmesin, root kullanıcısını imajın içinde bırakma ve mutlaka ama mutlaka imajlarını zayıf halkalardan arındır.
Hadi şimdi git ve o docker-compose.yml dosyalarındaki privileged: true satırlarını silmeye başla. Güvenli kodlar!
