Pisałem już NetworkPolicy dla kilku aplikacji: najpierw zakaz wszystkiego, potem jawnie dozwolone połączenia. Argo CD pokazywało je jako zsynchronizowane, kubectl get networkpolicy je wypisywał. Problem w tym, że klaster działał na Flannelu, a Flannel w ogóle nie implementuje polityk sieciowych. API Kubernetesa przyjmuje taki obiekt, bo to tylko zasób w bazie, ale nikt go nie wykonuje. Każdy pod mógł rozmawiać z każdym.
Dokładnie po to zrobiłem migrację na Cilium: najpierw klaster produkcyjny 18–19 sierpnia, potem domowy 20 sierpnia.
Co daje Cilium
Cilium to CNI oparte na eBPF. Zamiast reguł iptables programuje jądro bezpośrednio, a do tego może całkowicie zastąpić kube-proxy. W Talosie oznacza to dwie zmiany w konfiguracji maszyny: brak domyślnego CNI i wyłączony kube-proxy. Resztę ustawia chart Helm, którym zarządza Argo CD.
Dla mnie liczyły się trzy rzeczy. Po pierwsze polityki sieciowe są naprawdę egzekwowane. Po drugie Hubble pokazuje każdy przepływ między podami razem z werdyktem, więc widać, co zostało zablokowane i przez którą regułę. Po trzecie Cilium ma własne CiliumNetworkPolicy, które potrafią wyrazić rzeczy niemożliwe w zwykłej NetworkPolicy.
Dowód z warstwy danych, nie ze statusu
Najważniejszą decyzją przy tej migracji było to, że o egzekwowaniu polityk przekonuję się na ruchu, a nie na statusie w Argo. Test był prosty: dwa pody, połączenie działa, nakładam politykę „deny-all”, połączenie musi paść, a Cilium musi zalogować odrzucenie. Potem usuwam politykę i połączenie wraca.
Test kontrolny: pod A łączy się z podem B, potem nakładam deny-all
$ kubectl -n kube-system exec ds/cilium -- cilium-dbg monitor --type drop
# zapis z 20.08.2026; adresy podów podmienione
xx drop (Policy denied) flow … file bpf_lxc.c:2412, identity 5825->64145: <pod-a>:32982 -> <pod-b>:8080 tcp SYN
# po usunięciu polityki połączenie znów zwraca 200; pod Flannelem ta sama polityka nie robiła nic
$ kubectl -n kube-system exec ds/cilium -- cilium-dbg endpoint listcilium-dbg endpoint list pokazuje przy każdym endpoincie, czy polityka na wejściu i wyjściu jest włączona. Na tym polegam, a nie na zielonej ikonce.
Polityki: zakaz domyślny i jawne wyjątki
Bazowy zestaw wprowadzałem namespace po namespace. Tam, gdzie leżą dane, zakaz obejmuje ruch w obie strony. W innych miejscach na początek wystarczy zakaz ruchu przychodzącego, bo na przykład monitoring musi móc łączyć się ze wszystkim. Są też miejsca, w których zakaz domyślny warto przemyśleć dwa razy: komponenty systemowe klastra, bo pomyłka tam kosztuje cały klaster, namespace’y z webhookami wywoływanymi przez API server, bo zła reguła wygląda wtedy jak awaria API, i miejsce, w którym w razie awarii uruchamia się pod do debugowania, bo awaria to zły moment na odkrywanie polityk.
Każda nowa aplikacja przynosi własne polityki w swoim katalogu i startuje w namespace z zakazem w obie strony. Najczęściej zapominana reguła to DNS:
# zakaz w obie strony dla całego namespace'u
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: example}
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
# bez tego nic w namespace nie rozwiąże żadnej nazwy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-dns-egress, namespace: example}
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}
podSelector: {matchLabels: {k8s-app: kube-dns}}
ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
---
# API server działa w sieci hosta, więc żaden ipBlock go nie złapie;
# Cilium nazywa go wprost
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: {name: allow-kube-apiserver-egress, namespace: example}
spec:
endpointSelector: {}
egress:
- toEntities: [kube-apiserver]
Ostatnia reguła ma swoją historię. W namespace z bazami działa Job kopii zapasowej, który rozmawia z API Kubernetesa przez operatora bazy. Pod samym zakazem domyślnym umierałby po cichu, bez błędu widocznego od razu. Reguła CIDR nic by nie dała, bo ruch do adresu węzła Cilium klasyfikuje jako host, a nie jako adres IP z puli. Pomógł dopiero toEntities: [kube-apiserver]. Ta lekcja zostaje: każdą politykę sprawdzam też pod kątem zadań uruchamianych okresowo, a nie tylko tego, co widać od razu.
Hubble: mapa ruchu, której nie wystawiam
Hubble UI rysuje graf połączeń między usługami na żywo i świetnie się sprawdza przy pisaniu polityk: widać, kto z kim rozmawia, zanim cokolwiek się zablokuje. Ma jednak jedną wadę: nie ma własnego logowania. Wystawienie go pod publicznym adresem oznaczałoby pokazanie całej mapy klastra każdemu, kto zgadnie nazwę. Dlatego jest dostępny tylko przez prywatną sieć VPN, a sam pod pośredniczący ma własny zakaz domyślny.
Czego się nauczyłem
- „Synced” to nie „działa”. Obiekt w API to deklaracja. Egzekwowanie trzeba udowodnić na ruchu: odrzucony pakiet w logu i powrót po usunięciu reguły.
- Wyjątki od zakazu domyślnego to decyzje do zapisania, nie zaległości. Przy każdym namespace bez zakazu w repozytorium jest powód, dlaczego tak.
- Nie renderuj chartu sieci ręcznie. Lokalny Helm w innej wersji wyrenderował mi chart Cilium z drobnymi różnicami, które przy zastosowaniu zmieniłyby szablon DaemonSetu i zrestartowały sieć całego klastra. Chartem zarządza Argo CD i tylko on.