L'objet Secret de Kubernetes est sans doute la primitive la plus mal comprise de l'API. Elle a l'apparence d'une fonctionnalité de sécurité, mais par défaut il ne s'agit que de blobs encodés en base64 stockés dans etcd, lisibles par quiconque dispose du verbe get sur les secrets du namespace, et totalement statiques. Ajoutez-y le GitOps — où l'état du cluster est censé vivre intégralement dans Git — et vous obtenez le problème classique de l'œuf et de la poule : comment déclarer un secret qu'on ne doit jamais committer ?
La combinaison HashiCorp Vault (ou son fork open source OpenBao) et de l'External Secrets Operator (ESO) s'est imposée comme la réponse standard des équipes plateforme. Cet article explique comment cela fonctionne réellement, comment cela se compare aux alternatives, et quels sont les pièges opérationnels que les quickstarts passent sous silence.
Ce qui cloche vraiment avec les Secrets natifs
Soyons justes : les Secrets natifs ne sont pas inutiles. Ils restent le mécanisme de livraison, et quasiment tous les outils du domaine finissent par en produire un. Les problèmes sont autour :
- Stockage : sauf à activer explicitement le chiffrement at-rest via un provider KMS dans la configuration de l'API server, les secrets reposent dans etcd en base64 lisible. Quiconque récupère une sauvegarde etcd obtient vos identifiants de base de données.
- Contrôle d'accès : le RBAC sur les secrets est à la granularité du namespace. Un workload qui a besoin d'un seul secret se retrouve souvent dans un namespace où douze autres sont lisibles.
- Aucun cycle de vie : pas de rotation, pas de TTL, pas de révocation. Un identifiant compromis reste valide jusqu'à ce qu'un humain s'en aperçoive.
- Aucune traçabilité métier : l'audit log de l'API server indique qu'un secret a été lu, pas quelle valeur ni qui l'a consommée en aval.
- Incompatible GitOps : impossible de les committer, ils deviennent donc une étape manuelle hors bande qui dérive inévitablement.
Le paysage des solutions
| Approche | Source de vérité | Rotation | Pertinent pour | Principal inconvénient |
|---|---|---|---|---|
| Sealed Secrets | Git (chiffré) | Re-scellage manuel | Petits clusters, pas de coffre externe | Clé liée au cluster, PRA et multi-cluster pénibles |
| SOPS + age/KMS (Flux, kustomize) | Git (chiffré) | Manuelle | Équipes déjà très GitOps, peu de secrets | Une copie du secret reste dans Git, distribution des clés |
| Vault Agent Injector | Vault | Re-rendu du template | Apps lisant des fichiers, secrets dynamiques | Sidecar par pod, webhook mutant, rechargement applicatif requis |
| Secrets Store CSI Driver | Vault / KMS cloud | Au remontage | Éviter tout objet Secret Kubernetes | Volume uniquement ; les variables d'env imposent une synchro |
| External Secrets Operator | Vault / fournisseur cloud | Polling + webhooks | La plupart des équipes plateforme, multi-backend | Matérialise quand même un Secret Kubernetes |
Notre position assumée : ESO est le choix par défaut pour une plateforme qui sert plusieurs équipes, parce qu'il est déclaratif, agnostique du backend et ne change rien à la manière dont les applications consomment leurs secrets. Vault Agent Injector reste pertinent quand il faut des identifiants dynamiques très courts écrits dans un fichier, avec une application capable de les recharger. Les deux peuvent cohabiter dans le même cluster.
Brancher Vault sur Kubernetes
La décision structurante est l'authentification. Ne donnez jamais un token Vault longue durée à ESO. Utilisez la méthode d'auth kubernetes, où Vault valide un token de ServiceAccount via l'API TokenReview du cluster.
vault auth enable -path=k8s-prod kubernetes
vault write auth/k8s-prod/config \
kubernetes_host="https://kube-api.prod.internal:6443" \
kubernetes_ca_cert=@ca.crt
# Politique de moindre privilège : une équipe, un préfixe de chemin
vault policy write paiements-ro - <<EOF
path "kv/data/paiements/*" {
capabilities = ["read"]
}
path "database/creds/paiements-app" {
capabilities = ["read"]
}
EOF
vault write auth/k8s-prod/role/paiements \
bound_service_account_names="eso-paiements" \
bound_service_account_namespaces="paiements" \
policies="paiements-ro" \
audience="vault" \
ttl=1h
Notez le bound_service_account_namespaces : c'est lui qui rend la multi-tenance réelle. Un ServiceAccount du namespace detection-fraude ne pourra pas endosser le rôle paiements, même si un développeur recopie le YAML.
Préférez les tokens de ServiceAccount projetés, à durée courte et avec une audience dédiée, plutôt que l'ancien JWT de reviewer longue durée. Cela supprime un identifiant permanent du cluster.
SecretStore ou ClusterSecretStore ?
ESO expose deux portées. Un SecretStore est namespacé et s'authentifie avec un ServiceAccount de ce namespace : idéal pour l'isolation entre équipes. Un ClusterSecretStore est global et pratique, mais il s'authentifie avec une identité unique — l'opérateur devient alors un « deputy confus » si vous ne le contraignez pas avec des conditions.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-paiements
namespace: paiements
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "k8s-prod"
role: "paiements"
serviceAccountRef:
name: eso-paiements
Si vous utilisez malgré tout un ClusterSecretStore, restreignez-le :
spec:
conditions:
- namespaceSelector:
matchLabels:
tenant: paiements
L'ExternalSecret, et l'importance du templating
Un ExternalSecret naïf mappe une clé Vault vers une clé de Secret. En pratique, les applications veulent une chaîne de connexion, un fichier .env ou une configuration JSON. Le moteur de templating d'ESO évite d'ajouter un init container juste pour concaténer des chaînes.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: paiements-db
namespace: paiements
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-paiements
kind: SecretStore
target:
name: paiements-db
creationPolicy: Owner
deletionPolicy: Delete
template:
engineVersion: v2
data:
DATABASE_URL: "postgresql://{{ .user }}:{{ .password }}@pg.internal:5432/paiements?sslmode=require"
data:
- secretKey: user
remoteRef:
key: paiements/db
property: username
- secretKey: password
remoteRef:
key: paiements/db
property: password
Utilisez dataFrom avec extract pour récupérer toutes les clés d'un chemin Vault, et find avec une regex pour une famille de secrets. Attention : find à grande échelle génère beaucoup d'opérations LIST sur Vault.
Secrets dynamiques : là où Vault justifie son coût
Synchroniser des secrets KV statiques dans Kubernetes relève du confort. Le vrai gain de sécurité, ce sont les identifiants dynamiques : Vault crée un utilisateur de base à la demande, avec un TTL, et le révoque automatiquement.
vault write database/roles/paiements-app \
db_name=pg-prod \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT app_rw TO \"{{name}}\";" \
default_ttl="4h" max_ttl="24h"
ESO sait le consommer, mais il faut aligner le refreshInterval sur le TTL (typiquement rafraîchir à la moitié du TTL) et s'assurer que l'application prend en compte la nouvelle valeur. Deux schémas fonctionnent :
- Reloader (Stakater) : on annote le Deployment pour qu'un changement de Secret déclenche un rolling restart. Simple, mais un redémarrage toutes les quelques heures n'est pas toujours acceptable.
- Rechargement applicatif : surveiller le fichier monté (le kubelet met à jour les volumes de Secret projetés en place) et reconstruire le pool de connexions. Meilleur comportement, mais nécessite un support côté code.
Si aucune des deux options n'est envisageable, conservez des identifiants statiques dans KV et faites-les tourner à une cadence plus lente via les mécanismes de rotation de Vault : réduire le rayon d'impact reste mieux que rien.
Intégration GitOps sans dérive permanente
Avec ArgoCD, l'ExternalSecret est committé dans Git, le Secret généré ne l'est pas. Dites à Argo de l'ignorer, sinon vous aurez un bruit OutOfSync permanent :
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
ignoreDifferences:
- group: ""
kind: Secret
jsonPointers:
- /data
Le meilleur schéma reste de laisser ESO posséder entièrement le Secret (creationPolicy: Owner) et de ne jamais le référencer comme ressource gérée par Argo. Ajoutez également des sync waves pour que le SecretStore soit créé avant les ExternalSecret, eux-mêmes avant les Deployments qui montent le résultat — faute de quoi le premier déploiement échoue sur un Secret manquant.
Pièges opérationnels
Le Secret existe toujours dans etcd. ESO ne supprime pas ce risque, il gère le cycle de vie. Activez le chiffrement at-rest d'etcd via un provider KMS, verrouillez le RBAC sur secrets, et désactivez l'automount des tokens de ServiceAccount là où ils sont inutiles. Si vous ne pouvez vraiment pas tolérer d'objet Secret, passez par le driver CSI ou Vault Agent avec un volume en mémoire.
Vault devient une dépendance du cluster. ESO met en cache les valeurs dans le Secret : une indisponibilité de Vault ne casse pas immédiatement les pods en cours d'exécution, mais les nouveaux pods dépendant d'un secret fraîchement synchronisé échoueront, et les rafraîchissements s'arrêteront. Exploitez Vault en HA avec Raft réparti sur plusieurs zones, surveillez l'état de scellement, et disposez d'un runbook d'unseal testé (auto-unseal via un KMS/HSM, parts de clé conservées hors ligne).
Tempêtes de rafraîchissement. Mille ExternalSecrets avec refreshInterval: 1m vont saturer Vault. Utilisez des intervalles réalistes (15 min à 1 h pour des valeurs statiques) et exploitez le cache du provider. Surveillez les métriques externalsecret_sync_calls_error et externalsecret_status_condition, et alertez sur tout secret qui n'a pas synchronisé avec succès au-delà de son intervalle.
Audit. Activez l'audit device de Vault et expédiez-le vers votre SIEM. Croisé avec les logs ESO et l'audit de l'API Kubernetes, vous pouvez répondre à « quel workload a lu quel identifiant, et quand » — exactement la question posée lors d'un audit ISO 27001 ou d'une revue DORA.
Note souveraineté : OpenBao
Depuis le changement de licence d'HashiCorp, beaucoup d'équipes européennes se sont tournées vers OpenBao, le fork de Vault hébergé par la Linux Foundation. Il est compatible API sur les fonctions évoquées ici — KV v2, auth Kubernetes, moteur de secrets base de données — et ESO le pilote via le même provider Vault. Si votre posture de conformité exige un coffre auto-hébergé sous licence OSI, exploité sur une infrastructure européenne, c'est une alternative crédible. Validez toutefois les moteurs de secrets dont vous dépendez avant de vous engager : certaines fonctions entreprise (namespaces, auto-unseal HSM dans certaines configurations) diffèrent.
Par où commencer
N'essayez pas de tout migrer d'un coup. Choisissez un namespace, créez le mount d'auth Kubernetes avec une politique restreinte, convertissez ses secrets en objets ExternalSecret, puis supprimez la page de runbook « kubectl create secret ». Une fois le motif éprouvé, industrialisez-le : une library chart Helm ou un module Terraform qui provisionne la politique Vault, le rôle, le ServiceAccount et le SecretStore pour chaque nouveau tenant — c'est ce qui transforme un outil en véritable capacité de plateforme.
