Polityki na klastrze: po co Kyverno i CEL
- data
- kategoria
- Infrastructure & Cloud Security
- także w
- Containers
- czytanie
- 4 min / 761 słów
Problem zwykle wygląda niewinnie. Ktoś ma prawo utworzyć Deployment, RBAC mówi "tak", a request przechodzi dalej. Dopiero w treści manifestu widać, że samo uprawnienie nie wystarcza:
securityContext:
privileged: true
albo:
containers:
- image: nginx:latest
RBAC odpowiada na pytanie:
czy ta osoba albo ten pipeline może utworzyć Deployment?
Nie odpowiada dobrze na pytanie:
czy ten konkretny Deployment spełnia zasady klastra?
Do tego powstały polityki na klastrze.
Co było wcześniej
Na początku kontrola była często poza klastrem:
review w pull requeście
check w CI
skrypt walidujący YAML
lista zasad w dokumentacji
To pomaga, ale ma dziury. Nie wszystko trafia przez ten sam pipeline, nie każdy manifest ma ten sam proces review, operator może utworzyć zasób ręcznie, a controller może wygenerować obiekt, którego nikt nie widział w pull requeście.
W Kubernetes prawdziwym miejscem decyzji jest API server. Każda zmiana stanu klastra przechodzi przez request do API, więc kontrola musiała podejść bliżej tego miejsca.
Admission control
Admission control działa po uwierzytelnieniu i autoryzacji, ale przed zapisaniem obiektu w klastrze.
Uproszczony przepływ:
request
|
v
authentication
|
v
authorization / RBAC
|
v
admission
|
v
etcd
Admission odpowiada na inne pytanie niż RBAC:
RBAC:
czy wolno ci utworzyć Pod?
admission:
czy ten Pod może zostać utworzony w tej postaci?
To jest różnica między uprawnieniem a polityką.
Po co Kyverno
Kyverno powstało jako policy engine zaprojektowany pod Kubernetes. Zamiast pisać własny admission webhook w Go, można zapisać politykę jako zasób Kubernetes i trzymać ją razem z resztą konfiguracji platformy.
Przykładowe zasady:
nie pozwalaj na obrazy z tagiem latest
wymagaj labela owner
wymagaj requests i limits
blokuj privileged containers
dodawaj domyślne labelki
weryfikuj podpis obrazu
generuj NetworkPolicy dla namespace
Kyverno działa jako admission controller: API server wysyła do niego request, a Kyverno sprawdza polityki i zwraca decyzję. Ważne jest to, że polityka nie musi być tylko zakazem. Kyverno może walidować, mutować, generować zasoby, sprzątać zasoby i weryfikować obrazy:
validate
mutate
generate
cleanup
verify images
Czasem chcemy odrzucić zasób. Czasem chcemy go uzupełnić. Czasem chcemy wygenerować zasób towarzyszący, na przykład domyślną NetworkPolicy dla nowego namespace.
Kyverno i CEL razem
CEL nie jest konkurencją dla Kyverno. CEL, czyli Common Expression Language, jest językiem wyrażeń. Może być używany bezpośrednio przez Kubernetes w ValidatingAdmissionPolicy, ale może też być używany w Kyverno, bo nowsze typy polityk Kyverno są oparte właśnie o CEL.
Minimalna polityka Kyverno może wymagać labela owner na każdym Podzie:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-owner-label
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels.owner)"
message: "Pod must have an owner label"
To nie jest zabezpieczenie aplikacji. To jest zabezpieczenie procesu zmiany stanu klastra: jeżeli ktoś próbuje utworzyć Poda bez wymaganego labela, request zostaje odrzucony przed zapisaniem obiektu.
CEL wbudowany w Kubernetes
Kubernetes dodał mechanizmy oparte o CEL, żeby część walidacji mogła działać bez zewnętrznego webhooka i osobnego kontrolera. To ma sens przy prostych regułach lokalnych wobec requestu:
ten field musi istnieć
ta wartość nie może być true
ten obraz nie może używać tagu latest
Ten sam warunek można zapisać jako ValidatingAdmissionPolicy:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-owner-label
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels.owner)"
message: "Pod must have an owner label"
Różnica nie polega na tym, że "Kyverno albo CEL". Różnica polega na tym, gdzie używamy CEL i ile mechaniki platformowej jest potrzebne obok samego wyrażenia.
Kiedy wystarcza CEL w Kubernetes
Wbudowane CEL-owe admission policies są dobrym miejscem dla prostych walidacji:
wymagany label
zakaz privileged
zakaz hostNetwork
limit wartości pola
prosty warunek na obrazie
Jeżeli polityka patrzy tylko na obiekt z requestu i daje odpowiedź "przepuść albo odrzuć", wbudowany mechanizm Kubernetes może być wystarczający. Ma mniej ruchomych części, bo decyzja jest wykonywana w API serverze.
Kiedy Kyverno daje więcej
Kyverno zaczyna mieć sens, gdy polityka nie kończy się na prostym CEL-owym warunku:
mutacji zasobów
generowania zasobów
raportów polityk
weryfikacji obrazów
wyjątków
testowania polityk
większego workflow policy as code
W praktyce oba poziomy mogą żyć razem. Proste reguły można trzymać w natywnych admission policies Kubernetes, a pełniejszy workflow polityk w Kyverno. Albo można pisać CEL w Kyverno i korzystać z raportów, wyjątków, testów oraz innych elementów, których sam ValidatingAdmissionPolicy nie próbuje zastępować.
Przykład: podpis obrazu przez Sigstore
Dobry przykład różnicy to weryfikacja podpisu obrazu. W samym YAML-u widzimy tylko:
image: ghcr.io/example/api:1.4.2
To mówi, jaki obraz ma zostać uruchomiony. Nie mówi, kto go zbudował, czy przeszedł właściwy pipeline i czy ktoś nie podmienił artefaktu w registry.
W takim modelu pipeline buduje obraz i podpisuje go przez Sigstore/Cosign. Klaster nie ufa wtedy samemu stringowi ghcr.io/example/api:1.4.2. Przy admission Kyverno sprawdza, czy obraz ma poprawny podpis od zaufanego attestora.
Minimalny szkic polityki:
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
name: require-signed-images
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchImageReferences:
- glob: "ghcr.io/example/*"
attestors:
- name: githubActions
cosign:
keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/example/api/.github/workflows/release.yml@refs/heads/main"
ctlog:
url: "https://rekor.sigstore.dev"
validations:
- expression: >-
images.containers.map(image,
verifyImageSignatures(image, [attestors.githubActions])
).all(result, result > 0)
message: "Container image must be signed by the release workflow"
To jest inny typ kontroli niż has(object.metadata.labels.owner). Polityka musi sprawdzić obraz, podpis, tożsamość wystawcy i transparency log. CEL nadal występuje w wyrażeniu walidującym, ale Kyverno dostarcza integrację z Cosign, attestorami i obrazami kontenerów.
Po to używa się Kyverno w takich przypadkach: nie dlatego, że CEL jest za słaby jako składnia, tylko dlatego, że sama składnia wyrażeń nie wystarcza do całego workflow supply chain.
Czego polityki nie rozwiążą
Polityki nie naprawiają złego ownershipu. Jeżeli nikt nie wie, kto odpowiada za namespace, wymagany label owner będzie tylko kolejnym polem do obejścia.
Polityki nie zastępują edukacji zespołów. Jeżeli komunikat błędu mówi tylko:
denied by policy
to platforma produkuje frustrację, nie bezpieczeństwo. Polityki nie powinny też zastępować dobrych domyślnych ustawień. Jeżeli każdy zespół musi ręcznie pamiętać o dziesięciu polach, lepiej część z nich wygenerować albo dostarczyć w szablonie.
Po co to realnie jest
Polityki na klastrze są po to, żeby zasady platformy były wykonywane w miejscu, którego nie da się łatwo ominąć: nie tylko w dokumentacji, nie tylko w CI, nie tylko w review, ale w samym API Kubernetes.
Dobre polityki robią trzy rzeczy:
blokują oczywiście niebezpieczne konfiguracje
dają czytelny komunikat, co poprawić
automatyzują nudne, powtarzalne wymagania
Złe polityki robią jedną rzecz:
zamieniają klaster w czarną skrzynkę, która czasem mówi "nie"
Różnica rzadko leży w samym narzędziu. Leży w tym, czy polityka opisuje realną zasadę platformy, czy tylko lęk zapisany w YAML-u.