Saat gece 03:14, ekranımda akan loglarda tuhaf bir syscall anomalisi belirdi. testCompany altyapısındaki bir Kubernetes pod'u, normalde hiç işi olmayan /proc/kcore dosyasına erişmeye çalışıyordu. O an kahve kupasını masaya bırakıp terminale eğildim. Birileri mikroservis mimarisinin o tatlı esnekliğini, ana makineye (host) sızmak için bir basamak olarak kullanmaya çalışıyordu. Konteyner güvenliği dediğimiz şeyin sadece 'image scan' yapmaktan ibaret olmadığını anladığımız o kritik anlardan biriydi bu.
Birçok kişi konteynerları, içindeki her şeyi dış dünyadan tamamen izole eden büyülü birer kutu sanıyor. Ama işin aslı pek öyle değil kanka. Konteynerlar, sanal makineler (VM) gibi kendi çekirdeğine (kernel) sahip değiller; hepsi aynı kernel'ı paylaşıyor. Bu da demek oluyor ki, eğer kapıyı tam kapatmazsan, içerideki 'root' kullanıcı bir gün gelir senin host sisteminin de efendisi olur.
O Meşhur 'Privileged' Modu: Kendi Ayağına Sıkmak
Bakın, sahada en çok gördüğüm hata ne biliyor musunuz? Geliştiricinin işi kolaylaşsın diye pod'a ya da konteynere privileged: true yetkisi verilmesi. Bu, saldırgana 'Al kardeşim, bu sistemin anahtarları sende, ne istersen yap' demekle aynı şey.
Bir Red Teamer olarak benim ilk baktığım yer /dev klasörüdür. Eğer bir konteyner içerisinde ls /dev dediğimde ana makinenin disklerini görüyorsam, o iş bitmiştir.
Zararsızlaştırılmış bir breakout örneğine bakalım (Sakın prod ortamında deneme!):
# Zararlı bir konteyner başlatma senaryosu (Temsili)
# docker run --privileged --net=host -it alpine sh
# Konteyner içinden host diskine erişim denemesi
mkdir /mnt/host_root
mount /dev/sda1 /mnt/host_root
# Artık host sistemin /etc/shadow dosyasına ulaştık bile!
cat /mnt/host_root/etc/shadow
Buradaki olay şu: Konteyner içindeki kullanıcı root ise ve CAP_SYS_ADMIN gibi tehlikeli capability'lere sahipse, izolasyon bir anda kağıttan bir kuleye dönüşüyor.
Docker Socket Faciası
Bir diğer 'klasik' hata ise docker.sock dosyasının konteyner içine mount edilmesi. Genellikle monitoring araçları ya da CI/CD tool'ları için yapılır bu. Ama saldırgan o socket'e ulaştığı an, Docker API'si üzerinden yeni bir konteyner yaratıp host üzerinde kod çalıştırabilir.
Defanged bir payload mantığı şöyle işler:
# Docker socket erişimi varsa
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["cat", "/etc/hostname"], "HostConfig": {"Binds": ["/:/host"]}}' \
-X POST http://localhost/v1.24/containers/create
Buradaki Binds: ["/:/host"] kısmı her şeyi özetliyor. Host'un root dizinini yeni konteynerin içine bağla ve içeri gir. Oyun bitti.
Savunma Hattını Nasıl Kurarız?
Şimdi biraz da 'nasıl korunalım?' kısmına odaklanalım. testCompany tarafında bu tür krizlerin önüne geçmek için uyguladığımız birkaç altın kural var.
1. Rootless Konteynerlar
Eğer bir konteyneri root kullanıcısıyla çalıştırmıyorsan, saldırganın manevra alanı %90 oranında kısıtlanır. Dockerfile'ına şu basit satırı eklemek hayat kurtarır:
# Güvenli bir Dockerfile örneği
FROM node:18-alpine
# Gereksiz paketleri sil, sadece runtime'ı bırak
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /home/appuser
COPY --chown=appuser:appgroup . .
CMD ["node", "index.js"]
2. Capability'leri Budamak
Linux kernel'ı, root'un yetkilerini 'capabilities' dediğimiz parçalara böler. Varsayılan olarak Docker birçok capability ile gelir. Bizim yapmamız gereken 'en az yetki prensibi' (Least Privilege) gereği hepsini drop edip, sadece gerekeni vermek.
Kubernetes için örnek bir SecurityContext:
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Sadece 80 portunu açman gerekiyorsa bunu ekle
3. Image Scanning ve Distroless
Kullandığın base image ne kadar büyükse, içinde o kadar çok açık (CVE) barındırır. ubuntu:latest kullanmak yerine distroless image'ları tercih etmelisin. Distroless image'larda shell (/bin/sh veya /bin/bash) bile yoktur. Saldırgan içeri sızsa bile çalıştırabileceği bir komut satırı bulamaz. Karşısında sadece binary dosyan olur.
Ağ Katmanında İzolasyon (Network Policies)
Konteynerlar arasındaki trafik varsayılan olarak serbesttir. Yani 'frontend' pod'una sızan biri, ağ üzerinden direkt 'database' pod'una zıplayabilir (Lateral Movement). Bunu engellemek için Kubernetes Network Policy kullanmak şart.
Örneğin, sadece belirli bir etiketli (label) servisin database'e erişmesine izin veren bir kural:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db
ingress:
- from:
- podSelector:
matchLabels:
role: api
Runtime Security: Canlı İzleme
O gece 03:14'teki saldırıyı nasıl fark ettiğimize gelelim. Statik taramalar (Image scan) çalışmayan kodun açığını bulur. Ama asıl tehlike 'runtime'da, yani uygulama çalışırken ortaya çıkar. Falco gibi araçlar, kernel seviyesindeki syscall'ları izleyerek 'Bir konteyner neden /etc/shadow dosyasını okumaya çalışıyor?' diye bağırır.
Siber güvenlik bir varış noktası değil, bitmek bilmeyen bir yolculuk. Konteynerlar bize müthiş bir hız kazandırıyor ama bu hızı yaparken emniyet kemerini takmayı unutmamak lazım. İzolasyonun bir illüzyon olduğunu kabul edip, savunma katmanlarını (defense in depth) bu bilinçle inşa etmeliyiz.
Bir sonraki krizde görüşmek üzere, sistemlerinizi güncel tutun.
