درس 5 — Secrets, identité et GKE

Secrets et identité : External Secrets, Workload Identity

8 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Secrets et identité : External Secrets, Workload Identity

Chez Ferrand Médical, la clé d'API Gemini était copiée dans trois Secrets de trois namespaces, créée un vendredi par une personne partie depuis, jamais changée. Personne ne savait dire qui pouvait la lire : tous ceux qui ont get secrets sur l'un des trois namespaces, plus quiconque a eu accès au poste où elle a été collée. Il y a trois niveaux de réponse, et le meilleur supprime la clé.

Le mécanisme

flowchart TB
  subgraph N1["Niveau 1 : Secret Kubernetes créé à la main (lab kind)"]
    K1["make secret : kubectl create secret"] --> S1["Secret inference-gemini"] --> P1["Pod : GEMINI_API_KEY"]
  end
  subgraph N2["Niveau 2 : External Secrets Operator + Secret Manager"]
    SM["Secret Manager : lab-k8s-gitops-gemini-api-key (versions, audit)"] -->|"ESO, identité Workload Identity, toutes les heures"| S2["Secret inference-gemini"] --> P2["Pod : GEMINI_API_KEY"]
  end
  subgraph N3["Niveau 3 : Workload Identity, aucune clé"]
    P3["Pod, ServiceAccount assistant/inference"] -->|"jeton du serveur de métadonnées GKE"| STS["Security Token Service"]
    STS -->|"jeton d'accès court (1 h)"| VX["Vertex AI : roles/aiplatform.user"]
  end
  • Niveau 1 : acceptable dans un cluster jetable. Le Secret n'est pas dans Git (aucune trace dans gitops-repo/, vérifié par make e2e), mais sa valeur vit dans etcd et sur le poste qui l'a créée, sans rotation.
  • Niveau 2 : quand une clé est inévitable (fournisseur tiers, API Gemini hors Vertex). La source est Secret Manager, versionnée et auditée ; External Secrets Operator (ESO) recrée le Secret Kubernetes et le rafraîchit. La rotation se fait à un seul endroit.
  • Niveau 3 : pour appeler Google Cloud, il n'y a pas de clé du tout. Le ServiceAccount Kubernetes du pod devient un principal IAM ; Vertex AI reçoit un jeton d'accès de courte durée. Rien à voler, rien à faire tourner.

La mise en œuvre

Niveau 3 : ce que voit le code

Le code ne change pas entre la clé et Workload Identity. Le client google-genai lit les identifiants par défaut (ADC) quand vertexai=True :

files/app/llm.py
        if s.use_vertex:
            self.client = genai.Client(vertexai=True, project=s.project, location=s.location)
        else:
            self.client = genai.Client(api_key=s.api_key)

Sur GKE, les ADC sont fournis par le serveur de métadonnées du nœud. Ailleurs, par gcloud auth application-default login. C'est ce qui rend le mode GKE testable sans cluster : make e2e-vertex démarre l'application en mode Vertex AI, retire explicitement toute clé de l'environnement et fait un appel réel.

files/Makefile
e2e-vertex: ## Test réel du mode GKE : Vertex AI par identifiants ADC (ce que fournit Workload Identity)
	@env -u GEMINI_API_KEY -u GOOGLE_API_KEY GOOGLE_GENAI_USE_VERTEXAI=true GOOGLE_CLOUD_PROJECT=$(PROJECT) \
	  GOOGLE_CLOUD_LOCATION=$(REGION) MODEL=gemini-2.5-flash $(PY) scripts/e2e_vertex.py

Côté IAM, une seule liaison, déclarée en Terraform (main.tf) : le ServiceAccount Kubernetes, désigné par son namespace et son nom, reçoit roles/aiplatform.user. Workload Identity Federation for GKE permet cette liaison directe, sans compte de service Google intermédiaire ni annotation.

files/infra/main.tf
  app_principal = "principal://iam.googleapis.com/projects/${data.google_project.this.number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/${var.app_namespace}/sa/${var.app_ksa}"

resource "google_project_iam_member" "app_vertex" {
  project = var.project_id
  role    = "roles/aiplatform.user"
  member  = local.app_principal
}

Côté chart, l'environnement gke n'a pas de Secret et ouvre le chemin vers le serveur de métadonnées dans la NetworkPolicy (environments/gke/values.yaml) :

files/gitops-source/environments/gke/values.yaml
existingSecret: ""
networkPolicy:
  extraEgress:
    - to: [{ ipBlock: { cidr: 169.254.169.252/32 } }]   # serveur de métadonnées GKE : jetons Workload Identity
      ports: [{ port: 988, protocol: TCP }]
    - to: [{ ipBlock: { cidr: 169.254.169.254/32 } }]
      ports: [{ port: 80, protocol: TCP }]

Workload Identity n'existe pas sur kind : il n'y a ni serveur de métadonnées GKE ni pool d'identités. Monter dans le cluster local une clé de compte de service ou vos propres identifiants recréerait exactement le problème que ce niveau supprime. Le lab garde donc le mode simulé dans le cluster et fait l'appel réel hors cluster.

Niveau 2 : External Secrets, si une clé reste nécessaire

secretstore-gcp.yaml déclare la source et le Secret à produire :

files/external-secrets/secretstore-gcp.yaml
kind: ExternalSecret
spec:
  refreshInterval: 1h
  secretStoreRef: { kind: ClusterSecretStore, name: gcp-secret-manager }
  target: { name: inference-gemini, creationPolicy: Owner }
  data:
    - secretKey: GEMINI_API_KEY
      remoteRef: { key: lab-k8s-gitops-gemini-api-key, version: latest }

L'opérateur s'authentifie lui aussi par Workload Identity : son ServiceAccount (external-secrets/external-secrets) reçoit roles/secretmanager.secretAccessor sur ce secret seulement, par la même syntaxe principal://. Ce fichier n'est pas appliqué dans le lab : ESO sur kind exigerait une clé de compte de service pour lire Secret Manager, c'est-à-dire un secret pour protéger un secret.

Ce qui casse en production

SymptômeCauseParade
403 Permission denied on resource ... aiplatform.endpoints.predict sur GKEle principal IAM désigne assistant/inference mais le pod tourne avec un autre ServiceAccount (nom de release différent, namespace renommé)releaseName fixe (leçon 1), test de rendu sur serviceAccountName
Failed to retrieve ... metadata ou délai dépassé au premier appelNetworkPolicy en refus par défaut sans sortie vers 169.254.169.252:988extraEgress de l'environnement gke
Appels Vertex AI facturés au mauvais projet, avertissement without a quota projectADC d'utilisateur sans projet de quota (poste du développeur)gcloud auth application-default set-quota-project ; sur GKE, le projet du pool est utilisé
Clé tournée dans Secret Manager, ancienne clé encore utiliséevariables d'environnement figées au démarrageredémarrage piloté après rotation
ESO en erreur SecretSyncedErrorrôle accordé au projet au lieu du secret, ou nom de secret fauxliaison IAM sur la ressource secret, kubectl describe externalsecret

La règle de décision

  • Appel à une API Google Cloud depuis GKE : Workload Identity, jamais de clé. C'est le cas de Vertex AI.
  • Clé inévitable : Secret Manager comme source unique, ESO pour la livrer, rotation documentée avec redémarrage.
  • Secret créé par kubectl : seulement dans un cluster jetable, et jamais dans le dépôt GitOps (même chiffré, sauf outil dédié comme Sealed Secrets ou SOPS, que ce projet n'utilise pas).
  • À surveiller : les journaux d'audit d'accès aux secrets, et l'âge de chaque version de clé encore active.

Pratique

Un développeur propose, pour tester Workload Identity sur kind, de créer une clé JSON de compte de service et de la monter dans le pod. Que répondez-vous ?

Après la rotation de la clé dans Secret Manager, ESO a mis à jour le Secret inference-gemini à 10 h 02. À 10 h 30, l'équipe révoque l'ancienne clé ; à 10 h 31, tous les appels échouent en 403. Pourquoi ?

Exercice

Écrivez la commande gcloud qui donne à l'opérateur ESO (ServiceAccount external-secrets du namespace external-secrets) le droit de lire uniquement le secret lab-k8s-gitops-gemini-api-key, sans compte de service Google.

Voir la solution
terminal
PROJECT=votre-projet
NUM=$(gcloud projects describe $PROJECT --format='value(projectNumber)')
gcloud secrets add-iam-policy-binding lab-k8s-gitops-gemini-api-key --project $PROJECT \
  --role roles/secretmanager.secretAccessor \
  --member "principal://iam.googleapis.com/projects/$NUM/locations/global/workloadIdentityPools/$PROJECT.svc.id.goog/subject/ns/external-secrets/sa/external-secrets"

La liaison est posée sur la ressource secret, pas sur le projet : l'opérateur ne lit aucun autre secret. En Terraform, c'est une ressource google_secret_manager_secret_iam_member avec le même member. Le ClusterSecretStore n'a alors besoin d'aucune référence à une clé : le fournisseur gcpsm utilise l'identité du pod de l'opérateur.

Résultat obtenu

Exécution du 2026-10-16, hors cluster, identifiants ADC, aucune clé dans l'environnement (identifiant de projet masqué) :

$ make e2e-vertex PROJECT=VOTRE-PROJET
readiness : {'ready': True, 'problems': []}
UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK without a quota project. ...
Vertex AI (europe-west1) modèle=gemini-2.5-flash latence=3493 ms
Pour un problème de badge ne fonctionnant plus, veuillez contacter le service de sécurité ou la gestion des bâtiments. Ils sont en charge de l'activation et de la gestion des droits d'accès.
e2e-vertex : OK

L'avertissement de projet de quota est celui de la ligne 3 du tableau : sur un poste de développeur, les ADC d'utilisateur n'ont pas de projet de facturation des quotas. Sur GKE, il n'apparaît pas.

Dans le cluster kind, aucune valeur secrète :

$ kubectl -n assistant get secret
No resources found in assistant namespace.
$ make e2e
[OK] aucun Secret dans le dépôt Git
[OK] réponse via l'Ingress : HTTP 200
    backend=fake modèle=fake version=0.2.0 ...

Je sais faire

  • Choisir entre Secret manuel, External Secrets et Workload Identity selon le cas
  • Lier un ServiceAccount Kubernetes à un rôle IAM par un principal principal://
  • Ouvrir la NetworkPolicy vers le serveur de métadonnées GKE
  • Tester le mode Vertex AI sans cluster et sans clé avec make e2e-vertex
  • Ordonner une rotation de clé sans coupure
التالي