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:

  1. A kubelet látja az image referenciát és ellenőrzi, hogy valamelyik credential provider illeszkedik-e
  2. A CRI-O credential providere meghívódik a mirror URL-lel
  3. A provider kikeresi a pod service accountját és a hozzá tartozó imagePullSecrets-et
  4. Visszaadja az adott namespace-re szkópolt credentialöket
  5. 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.