Yıllar önce, henüz konteyner teknolojileriyle yeni tanıştığım dönemlerde, 'testCompany' bünyesinde bir microservice mimarisi kurmaya çalışıyordum. Bir türlü ayağa kalkmayan bir imaj vardı; sürekli izin hataları (permission denied) veriyordu. O zamanlar 'en hızlı çözüm en iyi çözümdür' kafasındayım tabii. Stack Overflow'da bir cevap gördüm: '--privileged'. Komutu ekledim, her şey bir anda çalışmaya başladı. Harika, değil mi? Meğer o gün, sistemin kalbine giden yolu ardına kadar açmışım. Olayın vehametini, bir hafta sonra yaptığımız iç sızma testinde bir arkadaşımın tek bir komutla host makinede root shell aldığını gördüğümde anladım. O günden beri konteyner güvenliği denince tüylerim diken diken olur.
Bugün sizinle, o günlerde yaptığım o acemice hatalardan ve sahadaki Red Team operasyonlarımızda konteynerlerin içinden nasıl tereyağından kıl çeker gibi sızdığımızdan bahsedeceğim. Kahvelerinizi tazeleyin, çünkü bu konu sadece 'docker build' demekle bitmiyor.
İmajların Şişkinliği ve Gizli Tehlikeler
Birçoğumuz Dockerfile yazarken en kolayımıza geleni seçiyoruz: FROM ubuntu:latest. Ancak bu, beraberinde devasa bir saldırı yüzeyi getiriyor. İhtiyacınız olmayan her bir kütüphane, her bir araç (curl, netcat, python, compiler'lar), saldırgan için birer 'post-exploitation' aracıdır. Eğer imajınızın içinde gcc veya curl varsa, sisteme sızan bir saldırganın zararlı yazılım indirmesini ve derlemesini kolaylaştırıyorsunuz demektir.
Şu örneğe bir bakalım:
# ZAYIF ÖRNEK
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3 curl netcat
COPY ./app /app
WORKDIR /app
# Uygulamayı root kullanıcısı olarak çalıştırıyor
CMD ["python3", "app.py"]
Buradaki problem sadece şişkinlik değil; uygulamanın varsayılan olarak root yetkileriyle çalışması. Eğer app.py üzerinde bir Remote Code Execution (RCE) açığı varsa, saldırgan doğrudan konteynerin root'u olur. Buradan host'a atlamak ise çocuk oyuncağı.
Capabilities: Sessiz Ama Derin
Konteynerler, Linux çekirdeğinin (kernel) yeteneklerini paylaşır. Docker varsayılan olarak bazı 'capabilities'leri kısıtlar ama biz bazen (benim o eski hatamda olduğu gibi) her şeyi açarız. --privileged bayrağını kullanmak, konteynerin host makinesindeki tüm donanıma ve kernel özelliklerine erişmesi demektir. Bu, izolasyonu tamamen ortadan kaldırır.
Bir saldırganın perspektifinden bakalım. Eğer bir konteynerde CAP_SYS_ADMIN yetkisi varsa, saldırgan host'un disklerini mount edebilir.
# Saldırganın konteyner içinde çalıştırabileceği bir 'escape' örneği (defanged)
# Host'un ana diskini bularak kendi içine mount etmeye çalışır
mount /dev/sda1 /mnt/host_root
# Artık host makinenin /etc/shadow dosyasına erişebilir!
cat /mnt/host_root/etc/shadow
Bunun yerine yapılması gereken, 'Least Privilege' prensibini uygulamaktır. Sadece ihtiyacınız olan yetkiyi verin, fazlasını değil.
docker run --clear-capabilities --add-capability CAP_NET_BIND_SERVICE example-image
Ortam Değişkenlerinde (Env Vars) Saklanan Sırlar
Bu en klasik hata. docker inspect komutunu çalıştırdığımda karşıma çıkan veritabanı şifreleri, API anahtarları beni benden alıyor. Konteyner imajı içine veya runtime'da environment variable olarak şifre basmak, o şifreyi tüm dünyaya ilan etmekle eşdeğerdir.
Konteyner üzerinde şu komutu koşturan biri her şeyi görebilir:
# Konteyner metadata'sını inceleme
docker inspect <container_id> | grep -iE "PASS|SECRET|TOKEN"
Bunun yerine Kubernetes Secrets, HashiCorp Vault veya AWS Secrets Manager gibi 'production-ready' çözümler kullanın. Kodun içine gömülü (hardcoded) sırlar ise zaten Red Team ekiplerinin en sevdiği atıştırmalıktır.
Runtime Güvenliği ve Seccomp
Sistem çağrılarını (syscalls) hiç duydunuz mu? Bir uygulama dosyayı açmak, ağ üzerinden veri göndermek veya yeni bir süreç başlatmak istediğinde kernel'e bir syscall gönderir. Docker, varsayılan bir Seccomp (Secure Computing) profili ile gelir ve yaklaşık 300 küsur syscall'dan kritik olanlarını engeller. Ancak çoğu kişi bunu devre dışı bırakır veya daha sıkı hale getirmez.
Konteynerin sadece yapması gereken çağrıları yapmasına izin vermek, sıfırıncı gün (zero-day) saldırılarını bile engelleyebilir. Eğer web uygulamanız asla mount veya ptrace yapmıyorsa, bu çağrıları Seccomp profiliyle yasaklayın.
Savunma Hattını Nasıl Kurarız?
Şimdi biraz 'Defensive Security' konuşalım. 'testCompany' gibi bir yerde şu adımları izlemeden hiçbir konteyner prod ortamına çıkmamalı:
- Distroless İmajlar: Sadece uygulamanızın çalışması için gereken runtime'ı içeren, içinde shell bile bulunmayan imajlar kullanın. Saldırgan sızsa bile 'ls' komutunu bile çalıştıramayacağı bir ortamda eli kolu bağlanacaktır.
- Read-Only Root Filesystem: Konteynerin dosya sistemini salt-okunur yapın. Zararlı bir script indirilip çalıştırılamasın.
docker run --read-only --tmpfs /tmp example-image - İmaj Tarama (Scanning): CI/CD süreçlerinize
trivyveyagrypegibi araçlar entegre edin. Kritik bir zafiyet içeren kütüphane varsa build'i patlatın. - User Namespace: Host'taki root ile konteynerdeki root'u birbirinden ayırın (UID mapping). Konteynerde root olan biri, host'ta sadece 'nobody' kullanıcısı olsun.
Olayın Özü
Konteynerler birer 'güvenlik duvarı' değildir; onlar sadece süreçleri (processes) birbirinden ayıran mantıksal sınırlardır. Bu sınırları kağıttan yaparsanız, ilk rüzgarda yıkılırlar. Red Team olarak biz, bu sınırların ne kadar zayıf olduğunu test etmek için oradayız. Ama günün sonunda amacımız, sizin bu sınırları çelikten örmenizi sağlamak.
Bir sonraki microservice deployment'ınızda o meşhur --privileged bayrağına eliniz giderse, lütfen o günkü acemiliğimi hatırlayın. O bayrak, aslında kendi kalenize attığınız bir goldür. Güvende kalın, açıklarınızı biz bulmadan siz kapatın!
