Kubernetes'te hiç network policy yazılmamış bir cluster'da her pod her pod'a, her pod da internete gidebiliyor. Güvenlik tarafının ilk isteği genelde "default-deny açalım" oluyor: her şey kapalı, sadece izin verdiklerimiz açık. Bu yazıda bunu kind üzerinde Cilium ile adım adım yaptım ve beklemediğim yerlerden kırıldı. En şaşırtıcısı: sadece izin veren, hiçbir şey yasaklamayan bir cluster-wide policy, hiç dokunmadığım başka bir namespace'in internet çıkışını kesti.
Bu, Cilium serisinin üçüncü yazısı. İlk yazıda gördüğümüz identity'lerin üzerine kural yazıyoruz: önce L3/L4, sonra HTTP metodu ve path'i, sonra DNS adı, en sonda namespace genelinde default-deny. Her adım lab'daki gerçek çıktılarla ve Hubble'ın gösterdiği kararlarla.
Sorun ne, neden gerekli
Kubernetes'in varsayılanı "her şey açık". Bir pod ele geçirildiğinde ya da yanlış yapılandırıldığında, cluster içindeki her servise ve dışarıdaki her adrese ulaşabiliyor. Network policy bunu daraltmanın yolu.
Kubernetes'in kendi NetworkPolicy'si IP ve port seviyesinde çalışıyor. Bu çoğu zaman yetmiyor:
- "Ödeme servisi sadece
POST /chargealsın" gibi bir kural yazamıyorsunuz; port 80 ya açık ya kapalı. - "Bu pod sadece
api.github.com'a çıkabilsin" diyemiyorsunuz; dış servislerin IP'leri sürekli değişiyor. - Bir kuralın neyi kestiğini görmenin standart bir yolu yok.
Asıl zor kısım ise kuralı yazmak değil, açmak. Default-deny'ı canlı bir cluster'da açtığınız an, kimsenin listelemediği bağımlılıklar (DNS, API server, metrik toplama) bir anda kopuyor.
Cilium'un CiliumNetworkPolicy'si üçünü de kapsıyor: L7 HTTP kuralları, DNS adına göre egress, ve Hubble'da her kararın (verdict) görünmesi. Bir de audit mode var: kuralları uygulamadan "uygulasaydım neyi keserdim" diye bakmak için.
Cilium policy neye bakıyor: identity, yön ve katman
Serinin ilk yazısında her pod'un label'larından türeyen bir identity aldığını görmüştük. Cilium policy'leri IP'ye değil bu identity'ye yazılıyor; pod yeniden başlayıp IP değiştirse de kural aynı kalıyor.
Bir policy bir pod'u seçtiğinde, o pod o yön için (ingress ya da egress) default-deny'a geçiyor: policy'de izin verilmeyen her şey düşüyor. Hiçbir policy'nin seçmediği pod'da her şey açık. Bu yüzden default-deny'ı açmak, aslında "bir namespace'teki bütün pod'ları iki yönde birden seçen bir policy yazmak" demek.
Bir isteğin geçmesi için iki uçta da izin gerekiyor: istemcinin egress'i ve sunucunun ingress'i. Lab'ın en önemli dersi bu oldu:

Lab: ne kurdum
Apple Silicon bir Mac'te, Colima (4 CPU / 8 GB, kernel 6.8) üzerinde, serinin ilk yazısındaki cluster'ın aynısı:
- kind v0.33 ile 1 control plane + 2 worker, Kubernetes v1.37.0, varsayılan CNI ve kube-proxy kapalı.
- Cilium 1.20.2, kube-proxy replacement, VXLAN, Hubble Relay ve UI açık.
- Demo yine Cilium'un Star Wars uygulaması: iki replikalı
deathstarservisi,tiefighter(İmparatorluk,org=empire) vexwing(İttifak,org=alliance). Yanına dışarıya çıkan bir pod:mediabot. - Altı policy dosyası, sırayla uygulanıyor; her adımdan sonra aynı üç istek: tiefighter iniş, xwing iniş, tiefighter'ın
PUT /v1/exhaust-portçağrısı (Ölüm Yıldızı'nı patlatan istek).
Cluster ve Cilium sıfırdan 79 saniye, bütün policy adımları 72 saniye, audit mode turu 73 saniye.
Adım 1 — L3/L4 policy: kim kime, hangi porttan
İlk policy deathstar'ı seçiyor ve sadece org=empire label'lı pod'lardan, sadece 80/TCP'ye gelen trafiğe izin veriyor. Ardından L7 kuralı ekleyen ikinci policy:

Policy yokken üç istek de geçti; tiefighter'ın PUT isteği deathstar'ı "patlattı". 01'den sonra xwing'in isteği hiç cevap almadı (5 saniyede zaman aşımı), çünkü paket daha deathstar'a varmadan düşüyor. Ama tiefighter hâlâ deathstar'ı patlatabiliyor: L3/L4 kuralı "80 portu açık" diyor, o porttan ne geçtiğine bakmıyor.
Adım 2 — L7 HTTP policy: metot ve path seviyesinde kural
02 aynı kurala bir HTTP bölümü ekliyor: sadece POST /v1/request-landing. Bu kural varken Cilium o porta gelen trafiği node'daki Envoy proxy'den geçiriyor; proxy isteği okuyup kurala uymayanı Access denied ile geri çeviriyor. tiefighter hâlâ iniyor, ama artık exhaust-port'a erişemiyor.
Hubble iki katmanı ayrı ayrı gösteriyor. L3/L4 kararları policy-verdict satırları, L7 kararları http-request satırları:

Satırlarda kaynak ve hedefin identity'leri de var (ID:14317 xwing, ID:25112 deathstar). Bir kuralın kimi neden durdurduğunu görmek için node'a bağlanıp paket yakalamaya gerek kalmıyor.
Adım 3 — DNS tabanlı egress (toFQDNs): sadece GitHub'a çıkış
03 dışarıya giden trafiği DNS adına göre kısıtlıyor: mediabot sadece *.github.com ve github.com'a, sadece 443'ten çıkabilir. IP yazmıyoruz; Cilium pod'un DNS sorgularını kendi DNS proxy'sinden geçiriyor, cevapta gelen IP'leri öğreniyor ve kuralı o IP'lere uyguluyor.

GitHub'a giden istekler 200 döndü, docs.cilium.io zaman aşımına düştü. Hubble'da hedef (world) olarak görünüyor ve kararı EGRESS DENIED. Ajanın FQDN cache'inde de Cilium'un DNS cevaplarından öğrendiği IP'ler duruyor.
Bu policy'de ikinci bir kural da var: kube-dns'e 53 portundan izin. O olmasa pod DNS sorgusu bile atamıyor ve toFQDNs hiçbir zaman IP öğrenemiyor. Bu, ileride daha büyük bir sorun olarak geri dönecek.
Adım 4 — Namespace genelinde default-deny ve egress tuzağı
Şimdi asıl adım. 04, default namespace'indeki bütün pod'ları seçip iki yönü de kapatıyor, sadece kube-dns'e DNS'i açık bırakıyor. 05 ise cluster-wide bir baseline: her namespace'e (kube-system hariç) DNS ve API server erişimi. İkisini uygulayıp aynı üç isteği attım:

Üç istek birden düştü, tiefighter'ınki de. Deathstar'ın ingress kuralı tiefighter'a hâlâ izin veriyordu; sorun öbür uçtaydı. Hubble net söyledi: EGRESS DENIED. Default-deny tiefighter'ın çıkışını da kapatmıştı ve ona "deathstar'a gidebilirsin" diyen bir kural yoktu.
06 bu egress kuralını ekliyor: tiefighter'dan deathstar'ın 80 portuna. Uygulayınca tablo eski haline döndü: tiefighter iniyor, xwing düşüyor, exhaust-port reddediliyor. Ders: default-deny'dan sonra her bağlantı için iki kural lazım, biri sunucunun ingress'i, biri istemcinin egress'i. Kubernetes'in kendi NetworkPolicy'sinde de durum aynı.
Adım 5 — Audit mode: uygulamadan önce görmek
Canlı bir cluster'da 04 ve 06'yı bu sırayla uygulamak, aradaki süre boyunca kesinti demek. Cilium'un audit mode'u bunun için var: policy'ler değerlendiriliyor, Hubble'a "düşürürdüm" diye yazılıyor ama paket düşmüyor.
Denemek için 06'yı sildim ve audit mode'u Helm ile açtım:

Audit mode açılmadı. ConfigMap true diyor, ama ajanlar hâlâ trafiği kesiyor ve log'da sebebi yazıyor: Mismatch found ... policy-audit-mode actual=false. Bir önceki yazıda L2 announcement'ta gördüğüm durumun aynısı: helm upgrade ayarı yazıyor, çalışan ajan onu sadece açılırken okuyor. Burada sonucu daha tehlikeli: "audit'teyim, bir şey kesilmez" diye düşünüp gerçekte enforce ediyorsunuz.
Ajanları yeniden başlatınca audit mode gerçekten açıldı:

Şimdi xwing bile iniyor ve Hubble'da AUDITED satırları var: tiefighter'ın egress'i (06 eksik olduğu için) ve xwing'in iki yönü. Bu liste, enforce'a geçmeden önce yazılması gereken kuralların listesi.
Bir şey dikkatimi çekti: exhaust-port isteği audit mode'da da Access denied aldı. Audit mode L3/L4 kararlarını gevşetiyor, ama L7 HTTP kuralları Envoy'da uygulanmaya devam ediyor. Audit'e güvenerek L7 kuralı eklerseniz, o kural audit'te de keser.
Son durum: hangi pod'u hangi policy seçiyor
Audit'i kapatıp 06'yı geri koyunca son durum:

Her endpoint iki yönde policy altında; namespace'te dört policy, cluster genelinde bir baseline var. Ajan her node'da sadece kendi pod'larını bildiği için bu tabloyu her node'daki ajandan toplayan küçük bir script kullandım.
Nerede patladı: 4 gerçek hata
1. policy-verdict:none EGRESS DENIED (UDP): DNS kuralı olmayan default-deny
Default-deny'ın en sade hali, iki yön için de boş bir kural: hiçbir şeye izin yok. Bunu (baseline olmadan) uyguladım ve tiefighter'dan deathstar'a servis adıyla istek attım:

curl "adı çözemedim" demedi, düz bir zaman aşımı gibi görünüyordu. Asıl sebep Hubble'da: kube-dns'e giden UDP 53 düşmüş. Gerçek bir uygulamada bu, her servisin aynı anda "bağlanamıyorum" demesi olarak görünür ve ilk akla gelen DNS olmaz. 04 ve 05'te DNS kuralının olmasının sebebi bu.
2. Sadece izin veren bir CCNP başka bir namespace'i kilitledi
Baseline (05) bir CiliumClusterwideNetworkPolicy: içinde sadece izinler var, DNS ve API server. "Sadece izin veriyor, bir şeyi bozamaz" diye düşünmek kolay. Bunu test etmek için hiç policy'si olmayan ayrı bir namespace (team-b) açıp oradan GitHub'a istek attım:

enableDefaultDeny alanı olmadan uygulanan baseline, team-b'deki pod'un internet çıkışını kesti. Sebep yukarıdaki kural: bir policy bir pod'u egress yönünde seçtiği an, o pod egress'te default-deny'a geçiyor. Baseline kube-system dışındaki her pod'u seçiyor, yani cluster'daki her namespace'i. enableDefaultDeny: {ingress: false, egress: false} eklenince policy sadece izinlerini ekliyor, kimseyi default-deny'a sokmuyor. Cluster-wide bir policy'yi bu alan olmadan yazmayın.
3. Mismatch found ... key=policy-audit-mode actual=false: audit mode açılmadı
Adım 5'teki durum. helm upgrade sonrası ConfigMap doğru, ajanlar eski ayarla çalışıyor. Lab'daki audit-on ve audit-off hedefleri artık ajanları yeniden başlatıyor.
4. Makefile'daki demo ara testleri hiç koşturmamış
İlk demo hedefim her policy adımından sonra aynı test hedefini çağırıyordu. Çıktıda sadece ilk test vardı: make, bir hedefi tek bir çalıştırmada sadece bir kez koşturuyor; tekrar eden bağımlılıkları sessizce atlıyor. Yani "her adımdan sonra test" diye düşündüğüm şey, tek bir testti. Demo artık bir script ve her adımı gerçekten test ediyor.
Taslakta bunlardan başka iki madde vardı, lab'da doğrulanmadı ve çıkardım: kind'da kube-apiserver entity'si düzgün eşleşti (API server'a giden trafik Hubble'da (kube-apiserver) olarak görünüyor, baseline kaldırılınca düşüyor), toFQDNs kuralı da ilk istekten itibaren çalıştı.

Default-deny'ı canlı cluster'da açmak için sıra
Lab'dan çıkan sıra, büyük bir cluster için bir öneri olarak:
- Cluster-wide baseline: DNS ve API server,
enableDefaultDeny: falseile. - Audit mode'u aç, ajanları yeniden başlat,
Mismatch founduyarısı kalmadığını kontrol et. - Tek bir namespace'e default-deny. Hubble'da
--verdict AUDITile neyin kesileceğine bak. - Her
AUDITEDakış için amaç başına bir policy yaz, iki uçta da (sunucunun ingress'i, istemcinin egress'i). - Sonraki namespace'ler, birkaç tane birden.
- Hepsi temizlenince audit'i kapat, yine ajanları yeniden başlatarak.
L7 kuralları audit mode'da da uyguladığı için, onları audit turu bittikten sonra eklemek daha güvenli.
Doğrulama
kubectl get cnp,ccnp -A:deathstar-l3l4,default-deny,empire-to-deathstar,mediabot-fqdnve cluster-widebaseline-allow, hepsiVALID True.defaultnamespace'indeki her endpoint iki yönde policy altında.- tiefighter iniyor, xwing zaman aşımına düşüyor, exhaust-port
Access denied. - mediabot GitHub'a 200,
docs.cilium.io'ya ulaşamıyor. hubble observe --type policy-verdicther kararı kaynak ve hedef identity'siyle gösteriyor.- Audit mode açıkken L3/L4 kararları
AUDITED, ajan log'undaMismatch foundyok.
Kaynaklar: Cilium dokümanları Layer 3 Examples, Layer 4 Examples, Layer 7 Examples, DNS based policy (toFQDNs), Policy Enforcement Modes, Policy Audit Mode ve CiliumClusterwideNetworkPolicy (enableDefaultDeny); Kubernetes Network Policies.
Seride sonraki
Bu, Cilium Uçtan Uca serisinin üçüncü yazısı. Sonraki: Cilium Gateway API: Envoy Gömülü, Ayrı Ingress Controller'sız HTTP, TLS ve Traffic Split. Önceki yazı: Cilium IPAM: Pod CIDR Modları, LB-IPAM ve L2 Announcement ile Bare-Metal LoadBalancer.
Bu lab'ın kodu (kind yapılandırması, altı policy, iki tuzak senaryosu, audit turu, script'ler ve Makefile) Buy Me a Coffee'deki Lab Access üyeleriyle paylaşılıyor; üye olduktan sonra repo daveti e-postayla geliyor: https://buymeacoffee.com/miraccanyilmaz/membership