A múlt héten belefutottam abba, hogy a Kubernetes projekt csendben újraírta az image promotert, vagyis azt az eszközt, ami a hivatalos image-eket feltolja a registry.k8s.io alá. Nem is maga az átírás volt az érdekes, hanem az, hogy az új verzió már rendes SLSA provenance tanúsítványokat és cosign aláírásokat ad az image-ekhez minden tükörnél.
Ezen a ponton be kellett vallanom magamnak valamit: a saját image-eimet már jó ideje aláírom CI-ben, de a klaszter oldalon semmit nem kényszerítettem ki. Az aláírás ott volt, csak éppen senki nem nézte. Szóval végre rászántam magam, és rendbe tettem.
A kiindulás
A GitHub Actions pipeline-jaimban nagyjából egy éve ott van a cosign. Minden image kapott aláírást build közben:
cosign sign --yes \
--oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/myorg/myapp:${GITHUB_SHA}
Azért szerettem ezt a felállást, mert egyszerű volt. Kulcs nélküli aláírás a Sigstore Fulcio CA-val, vagyis nem kellett hosszú életű kulcsokat menedzselni, nem kellett titkokat forgatni, és nem kellett külön kulcskezelési procedúrát kitalálni. Az aláírás szépen ott élt a registry-ben az image mellett.
Csakhogy ez így inkább csak megnyugtató érzés volt, nem valódi védelem. A klasztereim ugyanúgy boldogan lehúztak volna aláíratlan image-eket is. Megcsináltam a látványos részt, csak az ellenőrzés maradt el.
Policy kikényszerítés Kyverno-val
Kyverno-t választottam, mert már amúgy is futott nálam más admission policy-khoz. Használhattam volna a Sigstore policy-controllerét is, de nem volt kedvem még egy webhookot berakni a láncba, ha nem muszáj.
Ez volt az első policy, amivel a cosign aláírásokat ellenőriztem:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-ghcr-images
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Felraktam, harminc másodpercig elégedetten néztem magam elé, aztán végignéztem, ahogy a staging környezet egy jó része nem tud többé podot ütemezni.
Mi tört el azonnal
Init konténerek. Ez volt az első pofon. A pod nem csak a fő konténer image-ét húzza le, hanem az init konténerekét is, nálam pedig az egyik debug sidecar még aláíratlan volt. Minden pod, ami ettől függött, repült. A javítás egyszerű volt: ezt az image-et is alá kellett írni, vagy külön kivételt adni rá.
Helm chart image-ek. A következő gondot a külsős chartok okozták. Ha egy chart mondjuk Bitnami Redist használ, azt az image-et nem én kontrollálom, és utólag nem tudom aláírni. Ezeket külön kellett kezelni:
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
# ...
- imageReferences:
- "docker.io/bitnami/*"
- "registry.k8s.io/*"
mutateDigest: true
verifyDigest: true
required: false
Itt a required: false végül teljesen vállalható kompromisszumnak bizonyult. A Kyverno továbbra is digestre írja át a tageket, ami véd a tag mutation típusú húzások ellen, de nem vár el olyan aláírást, amit amúgy sem tudnék előállítani. Nem tökéletes, de a valóságban használható.
Cache-elt image-ek. Ez már idegesítőbb volt. Ha az image már ott volt a node-on, és a workload imagePullPolicy: IfNotPresent beállítást használt, akkor nem nagyon volt mit ellenőrizni, mert a kubelet nem húzta le újra. A Kyverno admissionnél vizsgál, nem futás közben. Azoknál a workloadoknál, ahol ez tényleg számított, átálltam imagePullPolicy: Always-ra.
SLSA provenance ellenőrzés hozzáadása
Az aláírás azt mondja meg, ki készítette az image-et. A provenance azt mondja meg, hogyan készült. Amikor láttam, hogy a Kubernetes projekt már SLSA tanúsítványokkal publikálja a saját image-eit, rögtön azt akartam, hogy nálam is ugyanez legyen.
GitHub Actions-ben hozzáadtam az SLSA generátort:
- uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
image: ghcr.io/myorg/myapp
digest: ${{ steps.build.outputs.digest }}
Utána kibővítettem a Kyverno policy-t, hogy a provenance-et is ellenőrizze:
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
attestations:
- type: https://slsa.dev/provenance/v1
conditions:
- all:
- key: "{{ buildDefinition.buildType }}"
operator: Equals
value: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"
Ez már egy sokkal erősebb védőkorlát lett. Ha valaki a saját laptopjáról kézzel buildel egy image-et és feltolja a registry-be, az nem fog átmenni.
A cosign parancs, amit tényleg folyton használtam
Amikor valami nem működött, ezt a parancsot futtattam újra és újra:
cosign verify \
--certificate-identity-regexp "https://github.com/myorg/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp@sha256:abc123...
Attesztációhoz pedig ezt:
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/slsa-framework/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp@sha256:abc123...
A kimenet JSON, ami technikailag hasznos, emberileg kevésbé. Ha átcsövezed jq .payload | base64 -d | jq .-on, már egész jól olvasható, és lehet belőle normálisan debugolni.
Teljesítmény hatás
A valós költség nálam nagyjából 200 ms extra admission késleltetés volt podonként. Ez nem nulla, de nem is akkora ár, hogy ne férjen bele. A Kyvernónak minden új image-nél beszélnie kell a registry-vel és a Rekor transparency loggal, szóval az első deploy tényleg lassabb lesz.
Utána viszont sokat segít a cache. Mivel a Kyverno eltárolja az ellenőrzés eredményét, ugyanannak a digestnek az újradeployolása már sokkal simább. Az első rollout lassabb, a következőek teljesen rendben vannak.
A gyenge pont a Rekor volt. Ha a Sigstore-nak rossz napja van, azt a deploy pipeline rögtön megérzi. Nálam maradt bekapcsolva a Rekor ellenőrzés, de a policy-t úgy hagytam, hogy később könnyen tudjak rajta finomhangolni:
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
ignoreTlog: false
useCache: true
Megérte?
Igen, egyértelműen.
Körülbelül egy héttel azután, hogy ezt bevezettem, az egyik CI job rosszul lett konfigurálva, és elkezdett aláíratlan image-eket pusholni egy új repository-ba. Ha nincs kikényszerítés, ezek valószínűleg szépen besétálnak a productionbe, mielőtt bárki észreveszi. Így viszont a deploy azonnal megállt, a hiba egyértelmű volt, és a fejlesztő gyorsan javította.
Az egész beállítás nagyjából egy nap tényleges munka volt, és ennek a nagy része a külsős image-ek körüli kivételek finomhangolásával ment el. Az alap aláírás-ellenőrzés gyorsan megvolt. Az időt inkább az vitte el, hogy úgy legyen elég szigorú, hogy közben ne törjön el minden chart, ami külső image-et húz.
A supply chain biztonság sokáig nekem is olyasminek tűnt, amit csak a nagyon paranoiás csapatok csinálnak. Ezt már nem gondolom így. Az xz backdoor, a polyfill.io balhé, meg a kompromittált npm csomagok végtelen sora után ez inkább alap higiénia, mint túlzás. Sokkal szívesebben szánok rá egy napot, mint hogy utána egy hétig magyarázzam, hogyan jutott be egy aláíratlan image a production klaszterbe.