A múlt hét nagy részét azzal töltöttem, hogy image pull hibákat vadásztam egy multi-tenant klaszterben. Végül kiderült, hogy a gond a privát registry mirrorunk körül van. Pull-through cache-ként használtuk, de a credentialök node szinten voltak beállítva. Az egyik csapat rotálta a saját hozzáférését, aztán rövid időn belül három másik namespace podjai is elkezdtek hibázni. Ekkor vált teljesen világossá, hogy megosztott credentialökkel próbálunk együtt élni.
Innen jutottam el a CRI-O credential provideréhez registry mirrorokhoz. Miután összeraktam, elég nehéz lenne visszamenni a korábbi megoldáshoz.
A probléma, amivel szembesültem
Egy privát Harbor instance-ot üzemeltetünk pull-through cache-ként. Jól jön az egress költségeknél, gyorsabbak tőle a pullok, és az air-gap követelményeinkhez is passzol. Ezzel önmagában semmi különös nincs. A probléma ott kezdődik, hogy a Kubernetes nem tud a mirrorokról. A mirror konfiguráció a /etc/containers/registries.conf fájlban él node szinten, teljesen a Kubernetes API-n kívül.
Ha pedig a mirrorhoz hitelesítés kell, a legegyszerűbb megoldás az, hogy a credentialöket is felrakjuk a node-ra. Csakhogy onnantól minden namespace és minden pod azon a node-on potenciálisan ugyanazt a hozzáférést használhatja. Multi-tenant platformnál ezt elég nehéz megvédeni.
Próbáltuk ezt imagePullSecrets-szel megkerülni, de azok csak a forrás registryhez működnek, a mirrorhoz nem. Amikor a CRI-O átirányítja a pullt a mirrorra, a kubelet nem adja tovább ezeket a credentialöket.
CRI-O credential provider a megoldás
Kubernetes 1.33-tól és CRI-O 1.34-től végre van rá normális megoldás. A CRI-O ad egy credential provider plugint, ami standard Kubernetes Secreteket olvas, és azokat használja a mirror hitelesítéséhez. A kulcselem a KubeletServiceAccountTokenForCredentialProviders feature gate.
Nálam ez a felállás működött.
1. lépés: Credential provider bináris telepítése
Töltsük le a binárist a CRI-O credential provider releaseekből és tegyük minden node-ra:
curl -L -o /usr/local/bin/crio-credential-provider \
https://github.com/cri-o/crio-credential-provider/releases/latest/download/crio-credential-provider-linux-amd64
chmod +x /usr/local/bin/crio-credential-provider
2. lépés: Kubelet konfigurálása
Adjuk hozzá a credential provider konfigot a kubelet konfigurációhoz:
# /etc/kubernetes/credential-provider.yaml
apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
- name: crio-credential-provider
matchImages:
- "mirror.internal.company.com/*"
defaultCacheDuration: "10m"
apiVersion: credentialprovider.kubelet.k8s.io/v1
args:
- --mirror-registry=mirror.internal.company.com
Aztán hivatkozzunk rá a kubelet argumentumokban:
--image-credential-provider-config=/etc/kubernetes/credential-provider.yaml
--image-credential-provider-bin-dir=/usr/local/bin/
3. lépés: Feature gate engedélyezése
--feature-gates=KubeletServiceAccountTokenForCredentialProviders=true
4. lépés: Namespace-szintű secretek létrehozása
Itt kezd helyreállni a rend. Minden csapat létrehozhatja a saját registry secretjét a saját namespace-ében:
kubectl create secret docker-registry mirror-creds \
--namespace=team-alpha \
--docker-server=mirror.internal.company.com \
--docker-username=team-alpha-svc \
--docker-password='s3cret' \
--docker-email=[email protected]
Hivatkozzunk rá a service accountban vagy a pod specben a szokásos módon:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: team-alpha
imagePullSecrets:
- name: mirror-creds
Mi történik a háttérben
Amikor egy pod image-et kér, nagyjából ez történik:
- A kubelet látja az image referenciát és ellenőrzi, hogy valamelyik credential provider illeszkedik-e
- A CRI-O credential providere meghívódik a mirror URL-lel
- A provider kikeresi a pod service accountját és a hozzá tartozó
imagePullSecrets-et - Visszaadja az adott namespace-re szkópolt credentialöket
- A CRI-O ezekkel a credentialökkel húzza le az image-et a mirrorról
A lényeg az, hogy a credentialök nem lépik át a namespace határt. Az Alpha csapat mirror credentialei teljesen láthatatlanok maradnak a Beta csapat számára. Nincs több megosztott node-szintű secret.
Amikbe belefutottam
A cache időtartama nagyon nem mindegy. Először 1h-ra állítottam a defaultCacheDuration-t, mert úgy tűnt, így kevesebb lesz az API hívás. Papíron jól hangzott, a gyakorlatban viszont idegesítő volt. Ha valaki rotálta a credentialjeit, akár egy órát is várni kellett, mire az új értékek érvényesültek. Nálam a 10m lett az a pont, ami már jól használható kompromisszum.
A feature gate tényleg kötelező. A KubeletServiceAccountTokenForCredentialProviders nélkül a kubelet nem adja át a service account kontextust a credential providernek. Ilyenkor a pull egyszerűen auth hibával megáll, és a hibaüzenet sem túl segítőkész, ha nem tudod előre, mit keress.
A mirror routing továbbra is node oldali feladat. Ez a megoldás a hitelesítést oldja meg, nem a mirror routingot. A /etc/containers/registries.conf konfigurációjára továbbra is szükség van minden node-on:
[[registry]]
prefix = "docker.io"
location = "docker.io"
[[registry.mirror]]
location = "mirror.internal.company.com/docker-hub"
Ez most még CRI-O specifikus. Ez a credential provider a CRI-O projektből jön. Ha containerd-t használsz, akkor más lesz a megoldás. A containerd-nek is van saját credential provider mechanizmusa, de a mirror-aware, namespace-szintű auth ott még kevésbé kiforrott.
Megérte?
Egyértelműen. A változtatás előtt egyetlen mirror credential készletet osztottunk meg 12 namespace között. Egy kompromittált workload így elérhette volna az összes csapat cache-elt image-eit. Most minden csapat kezeli a saját credentialjeit, a rotáció egymástól független, és a compliance auditok is sokkal nyugodtabban telnek.
A rollout klaszterenként nagyjából fél napot vitt el, és összesen három klaszterünk van. Az idő nagy része a feature gate bevezetésének tesztelésére ment el, illetve arra, hogy ellenőrizzük: a meglévő workloadok a migráció alatt is rendben húzzák az image-eket.
Ha privát registry mirrorokat futtatsz egy multi-tenant Kubernetes klaszterben, szerintem ez bőven megéri a ráfordított energiát. A biztonsági javulás már önmagában elég erős érv, és az, hogy a csapatok végre saját maguk kezelhetik a credentialjeiket node szintű trükközés nélkül, külön plusz.