CiliumNetworkPolicy ile Default-Deny'a Giden Yol: L3/L4, DNS ve L7 HTTP Kuralları

Cilium ile default-deny'ı adım adım açtım: L3/L4, L7 HTTP ve DNS kuralları, audit mode ve Hubble. Sadece izin veren bir policy başka namespace'i kilitledi.

Yazan: Mirac Can Yılmaz 11 dk okuma
CiliumNetworkPolicy ile Default-Deny'a Giden Yol: L3/L4, DNS ve L7 HTTP Kuralları - Security yazısının kapak görseli

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 /charge alsı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:

Diyagram: tiefighter'dan deathstar'a giden istek üç kapıdan geçiyor: tiefighter'ın egress kuralı (06), deathstar'ın L3/L4 ingress kuralı (01) ve L7 HTTP kuralı (02). Altta üç policy katmanı: cluster-wide baseline, namespace default-deny ve amaç başına bir policy

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ı deathstar servisi, tiefighter (İmparatorluk, org=empire) ve xwing (İ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çiyor ve deathstar patlıyor; 01 L3/L4 sonrası xwing exit code 28 ile zaman aşımına düşüyor; 02 L7 sonrası exhaust-port isteği Access denied alıyor

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ı:

hubble observe: xwing'den deathstar'a INGRESS DENIED, tiefighter'dan INGRESS ALLOWED; http satırlarında POST request-landing FORWARDED, PUT exhaust-port DROPPED

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.

mediabot: api.github.com ve github.com 200, docs.cilium.io zaman aşımı; Hubble'da 104.16.254.120:443 (world) EGRESS DENIED; cilium-dbg fqdn cache list'te api.github.com ve github.com'un IP'leri

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:

05 ve 04 sonrası üç istek de exit code 28; Hubble'da tiefighter'dan deathstar'a EGRESS DENIED; 06 empire-egress sonrası tiefighter yine iniyor, xwing düşüyor, exhaust-port Access denied

Üç 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:

make audit-on sonrası ConfigMap'te policy-audit-mode true, ama üç istek de zaman aşımı; ajan log'unda Mismatch found key=policy-audit-mode actual=false

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ı:

Ajanlar yeniden başlatıldıktan sonra tiefighter ve xwing iniyor; Hubble'da EGRESS AUDITED ve INGRESS AUDITED satırları; ama exhaust-port hâlâ Access denied ve http-request DROPPED

Ş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:

enforcement.sh: default namespace'indeki her endpoint için policy iki yönde de seçili (both); kubectl get cnp,ccnp: deathstar-l3l4, default-deny, empire-to-deathstar, mediabot-fqdn ve cluster-wide baseline-allow

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:

DNS kuralı olmayan default-deny: curl exit code 28; Hubble'da tiefighter'dan coredns:53'e EGRESS DENIED (UDP)

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:

team-b: CCNP yokken github.com 200; enableDefaultDeny olmadan baseline uygulanınca exit code 28; enableDefaultDeny false ile 200; Hubble'da team-b/probe EGRESS DENIED

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ı.

Baseline varken tiefighter'dan kubernetes.default'a 200 ve Hubble'da (kube-apiserver) FORWARDED; baseline silinince exit code 28 ve Policy denied DROPPED

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:

  1. Cluster-wide baseline: DNS ve API server, enableDefaultDeny: false ile.
  2. Audit mode'u aç, ajanları yeniden başlat, Mismatch found uyarısı kalmadığını kontrol et.
  3. Tek bir namespace'e default-deny. Hubble'da --verdict AUDIT ile neyin kesileceğine bak.
  4. Her AUDITED akış için amaç başına bir policy yaz, iki uçta da (sunucunun ingress'i, istemcinin egress'i).
  5. Sonraki namespace'ler, birkaç tane birden.
  6. 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-fqdn ve cluster-wide baseline-allow, hepsi VALID True.
  • default namespace'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-verdict her kararı kaynak ve hedef identity'siyle gösteriyor.
  • Audit mode açıkken L3/L4 kararları AUDITED, ajan log'unda Mismatch found yok.

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

Hayatta en hakiki mürşit ilimdir.
Mustafa Kemal Atatürk