Saat tam 04:12’ydi. Telefonumun 'Critical' etiketiyle titremesiyle uyandım. testCompany’nin production cluster’ında, dışarıya açık basit bir 'customer-feedback' pod’unun normalde hiç yapmaması gereken bir şey yaptığını gördüm: kube-apiserver üzerinden tüm 'secrets' listesini sorgulamaya çalışıyordu. Gözlerimi ovuştururken ilk düşüncem şuydu: Birileri içeri girdi ve şu an cluster'ın içinde yanlara doğru (lateral movement) genişlemeye çalışıyor.
Bir Red Teamer olarak işin saldırı tarafını bilsem de, o gece o podun içinde neler döndüğünü anlamak tam bir dedektiflik hikayesine dönüştü. Çoğu kişi Kubernetes (K8s) kurduğunda her şeyin kutudan çıktığı gibi güvenli olduğunu sanıyor ama aslında elimizde devasa bir orkestra var ve şef sarhoşsa konser felaketle biter.
İlk Delik: Default ServiceAccount ve 'Fazla Cömert' Roller
O gece saldırganın ilk durağı, podun içine gömülü gelen ServiceAccount token’ıydı. Kubernetes'te her pod, aksi belirtilmedikçe bir service account ile doğar. Eğer siz bu podun yetkilerini düzgün kısıtlamazsanız, saldırgan içeri sızdığı an o token'ı alır ve cluster içinde 'Ben kimim?' diye sormaya başlar.
Saldırganın kullandığı ilk komut muhtemelen şuna benzer bir şeydi:
# Podun içindeki token ile yetki kontrolü
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k -H "Authorization: Bearer $KUBE_TOKEN" https://127.0.0.1:6443/api/v1/namespaces/default/secrets
Normal şartlarda bu isteğin '403 Forbidden' dönmesi gerekir. Ama bizim senaryomuzda, geliştirici ekip 'işler hızlı yürüsün' diye bu service account'a cluster-admin benzeri bir rol tanımlamış. İşte felaket burada başlıyor.
RBAC: Yetkiyi Verirken Elini Korkak Alıştırmamak Lazım
Kubernetes’te Role-Based Access Control (RBAC) en çok hata yapılan yer. Genelde ClusterRoleBinding yapılırken 'Hangi izinlere ihtiyacı var?' yerine 'Tüm izinleri verelim, çalışınca bakarız' mantığı güdülüyor.
Zararlı bir ClusterRoleBinding örneği (Defanged):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: feedback-pod-admin-binding
subjects:
- kind: ServiceAccount
name: default
namespace: production
roleRef:
kind: ClusterRole
name: cluster-admin # İŞTE BURASI PATLADIĞIMIZ YER
apiGroup: rbac.authorization.k8s.io
Buradaki cluster-admin rolü, saldırgana cluster üzerinde tanrı modu verir. Eğer bir podun sadece kendi namespace'indeki configmap'leri okuması gerekiyorsa, ona tüm cluster'ın secret'larını görme yetkisi verirseniz, anahtarı kapının üzerinde bırakmış olursunuz.
Savunma Reçetesi:
automountServiceAccountToken: falseayarını mutlaka kullanın. Eğer podun API ile işi yoksa, token’ı içeriye mount etmenin alemi yok.- 'Least Privilege' prensibini uygulayın. Sadece gerekli fiilleri (get, list) ve gerekli kaynakları (pods, deployments) tanımlayın.
Konteynerden Kaçış: hostPath ve Privileged Podlar
O geceki krizde saldırgan sadece API ile yetinmedi. Bir adım ileri gidip node'un (makinenin) kendisine sızmaya çalıştı. Bunu nasıl mı yaptı? hostPath mount kullanarak.
Bir pod düşünün, node üzerindeki /etc dizinine veya docker soketine erişimi var. Bu, konteynerin o 'güvenli' duvarlarını yıkıp doğrudan fiziksel veya sanal makineye atlaması demektir.
Saldırı simülasyonu için şöyle bir manifest kullanıyorlar:
apiVersion: v1
kind: Pod
metadata:
name: escape-pod
spec:
containers:
- name: exploit-container
image: alpine
command: ["/bin/sh", "-c", "chroot /hostOS /bin/sh"]
volumeMounts:
- name: host-root
mountPath: /hostOS
volumes:
- name: host-root
hostPath:
path: / # TÜM İŞLETİM SİSTEMİ BURADA
Eğer bu podu 'privileged: true' ile çalıştırırsanız, saldırgan artık sadece bir pod kullanıcısı değil, o node’un root kullanıcısıdır. Buradan sonra cluster’ın geri kalanını ele geçirmek çocuk oyuncağı.
Network Policies: İçerideki Trafiği Karantinaya Almak
Kubernetes'in doğasında her pod her podla konuşabilir. Bu, saldırgan için 'açık büfe' demektir. 'customer-feedback' podu neden gidip 'payment-gateway' poduna istek atabiliyor? Cevap basit: Network Policy yazmadığınız için.
Default olarak her şeyi engelleyen (Deny-All) bir politika ile başlamak hayat kurtarır:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Bu kuralı koyduğunuz an, cluster içindeki tüm iletişim kesilir. Sonra sadece ihtiyacı olanlara izin verirsiniz. Zahmetli mi? Evet. Güvenli mi? Kesinlikle.
Runtime Security: Gözün Üstünde Olsun
Saldırgan içeri girdiğinde curl indirdi mi? Bir binary mi çalıştırdı? Yoksa /etc/shadow dosyasını mı okumaya kalktı? Bunları standart loglarda göremezsiniz. Burada sahneye Falco veya Tetragon gibi araçlar giriyor.
O geceki krizde bizi kurtaran şey, A shell was spawned in a container with a sensitive mount uyarısının SIEM ekranına düşmesiydi. Runtime güvenliği, iş işten geçmeden müdahale etmenizi sağlayan son savunma hattıdır.
Ne Yapmalı? (Pratik Tavsiyeler)
- Pod Security Admission (PSA): Kubernetes 1.25+ ile gelen bu özelliği kullanın. 'Restricted' profilini enforce edin. Privileged podlara 'Hayırdır inşallah' diyerek engel olun.
- Secret Management: Secret'ları K8s etcd üzerinde base64 (yani düz metin sayılır) saklamak yerine HashiCorp Vault gibi harici çözümlerle veya KMS entegrasyonuyla şifreleyin.
- Image Scanning: CI/CD sürecinde
trivyveyagrypegibi araçlarla imajları tarayın. İçindeexploit-db’den fırlamış kütüphaneler olan imajları production’a sokmayın. - Kube-Bench: Cluster'ınızı CIS Benchmark standartlarına göre tarayın. 'API server izinleri çok gevşek' uyarısını ciddiye alın.
Sabahın o kör saatinde operasyonu tamamlayıp podu izole ettiğimizde şunu bir kez daha anladım: Kubernetes karmaşıktır. Ve karmaşıklık, güvenliğin en büyük düşmanıdır. Sırf 'çalışıyor' diye bıraktığınız her default ayar, bir gün sizi uykunuzdan edebilir.
Cluster'ınıza iyi bakın, podlarınızı başıboş bırakmayın. Görüşmek üzere!
