Les secrets
Pourquoi
À la leçon 5, la base PostgreSQL de la recette a été ajoutée au chart avec un mot de passe dans values.yaml. C'était commode pour la démonstration, et c'est exactement ce qu'il ne faut pas faire. Avec GitOps, tout ce que le cluster doit contenir est dans Git, et la tentation est forte d'y mettre aussi les mots de passe. Or un dépôt Git :
- est copié sur chaque poste qui le clone, dans chaque cache de CI, dans le repo-server d'Argo CD ;
- n'oublie rien : supprimer le mot de passe dans un commit le laisse dans l'historique, et réécrire l'historique ne rattrape pas les copies déjà faites ;
- est lu par beaucoup plus de monde que ceux qui devraient connaître le mot de passe de la production.
Il faut donc que Git décrive le secret sans le contenir en clair, et que la valeur arrive dans le cluster par un autre chemin. Cette leçon compare les manières de le faire, et montre celle qu'utilise Lyneko.
Les concepts
Deux familles
La documentation d'Argo CD distingue deux approches, et prend clairement parti :
flowchart TB
subgraph A["Résolution dans le cluster de destination (recommandée)"]
G1[Git : une référence<br/>ou un secret chiffré] --> AC1[Argo CD applique<br/>la référence] --> O[Opérateur dans le cluster<br/>crée le Secret]
end
subgraph B["Injection au rendu (déconseillée)"]
G2[Git : un emplacement] --> AC2[Argo CD lit le coffre<br/>pendant le rendu] --> S[Secret en clair<br/>dans les manifestes rendus]
end
- Résolution dans le cluster de destination : Git contient une référence (External Secrets Operator, pilote CSI Secrets Store) ou un secret chiffré (Sealed Secrets). Un opérateur du cluster produit le vrai Secret. Argo CD ne voit jamais la valeur.
- Injection au rendu : un greffon de rendu (par exemple argocd-vault-plugin) remplace des emplacements par les valeurs du coffre au moment où Argo CD génère les manifestes.
La documentation « met fortement en garde » contre la seconde, pour trois raisons : Argo CD doit avoir accès au coffre ; les manifestes rendus, secrets compris, sont conservés en clair dans le cache Redis d'Argo CD et exposés par l'API du repo-server ; et la mise à jour d'un secret se trouve couplée à une synchronisation, donc peut partir avec une livraison sans rapport. Le projet annonce d'ailleurs qu'il ne priorisera plus les fonctionnalités propres à cette approche.
Les outils
| Outil | Ce qui est dans Git | Où est la valeur | Dépendance |
|---|---|---|---|
| Sealed Secrets | Le secret, chiffré pour un cluster | Dans Git, chiffrée | La clé privée du contrôleur |
| SOPS (avec un greffon) | Le secret, chiffré par des clés KMS ou age | Dans Git, chiffrée | Le déchiffrement se fait au rendu : injection |
| External Secrets Operator | Une référence (ExternalSecret) | Dans un gestionnaire externe | Le gestionnaire et l'identifiant d'accès |
| Pilote CSI Secrets Store | Une SecretProviderClass | Dans un gestionnaire externe, monté en fichier | Le gestionnaire, un volume par pod |
En pratique
Sealed Secrets : chiffrer pour le cluster
Sealed Secrets installe un contrôleur qui possède une paire de clés. La clé publique sert à chiffrer, sur le poste, avec l'outil kubeseal ; seule la clé privée du contrôleur déchiffre, dans le cluster.
Où mettre le résultat ? Pas dans environnements/recette/ comme fichier à part : l'Application de Signalements rend le chart (path: chart), et ce répertoire n'est lu que pour ses fichiers de valeurs. Un manifeste qui y serait déposé ne serait jamais appliqué. On chiffre donc la seule valeur avec kubeseal --raw, on la range dans les valeurs de l'environnement, et le chart produit le SealedSecret :
# Chiffre un nouveau mot de passe pour ce nom et ce namespace exactement (portée strict).
# Le mot de passe en clair ne reste que dans la variable : il servira à l'ALTER ROLE.
mdp=$(openssl rand -base64 24)
printf '%s' "$mdp" | kubeseal --raw --namespace signalements-recette --name signalements-postgresql# environnements/recette/values.yaml (extrait)
postgresql:
enabled: true
motDePasseScelle: "AgB3...le texte chiffré produit par kubeseal..."# chart/templates/secret-postgresql.yaml
{{- if .Values.postgresql.motDePasseScelle }}
# Le mot de passe est chiffré pour ce cluster : seul le contrôleur Sealed Secrets le déchiffre.
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: {{ .Release.Name }}-postgresql
annotations:
# Avant la base (vague -1) : le Secret doit exister quand PostgreSQL démarre.
argocd.argoproj.io/sync-wave: "-2"
spec:
encryptedData:
mot-de-passe: {{ .Values.postgresql.motDePasseScelle | quote }}
template:
metadata:
name: {{ .Release.Name }}-postgresql
{{- end }}Le contrôleur en tire un Secret signalements-postgresql, et Argo CD connaît la santé d'un SealedSecret (script intégré resource_customizations/bitnami.com/SealedSecret/health.lua), si bien que la vague -2 attend son déchiffrement. Ce qu'il faut savoir avant de choisir cet outil :
- La portée. Par défaut (
strict), le nom et le namespace font partie des données chiffrées : un SealedSecret renommé ou déplacé ne se déchiffre plus.namespace-widepermet de renommer dans le namespace,cluster-widepartout. - Le renouvellement des clés. Une nouvelle clé de scellement est créée tous les 30 jours et sert aux nouveaux chiffrements ; les anciennes sont conservées, pour que les anciens SealedSecret restent lisibles. Ce n'est pas une rotation de vos secrets : le README le rappelle, il faut aussi changer régulièrement les mots de passe eux-mêmes.
- La clé est le point unique de défaillance. Sans sauvegarde des clés du contrôleur (des Secrets étiquetés
sealedsecrets.bitnami.com/sealing-keydans son namespace), un cluster reconstruit ne peut plus déchiffrer aucun des secrets du dépôt. Et quiconque obtient ces clés déchiffre tout l'historique du dépôt. - Un secret par cluster. Un même mot de passe déployé sur deux clusters doit être scellé deux fois.
External Secrets Operator : référencer un gestionnaire
External Secrets Operator (ESO) suit l'autre voie : la valeur vit dans un gestionnaire de secrets (Scaleway Secret Manager, Vault, OpenBao, AWS Secrets Manager...), et Git ne contient que la référence. Deux ressources :
- un SecretStore (dans un namespace) ou ClusterSecretStore (pour tout le cluster) décrit comment joindre le gestionnaire, avec quel identifiant ;
- un ExternalSecret décrit quel secret lire et quel Secret Kubernetes produire.
Le magasin, d'abord. Il est installé par l'équipe plateforme, une fois par cluster :
# Le magasin de secrets du cluster : Scaleway Secret Manager, un seul projet.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: scaleway
spec:
provider:
scaleway:
region: fr-par
projectId: <identifiant du projet Infrastructure>
accessKey:
secretRef:
namespace: external-secrets
name: scaleway-acces
key: access-key
secretKey:
secretRef:
namespace: external-secrets
name: scaleway-acces
key: secret-key
# Seuls les namespaces des applications peuvent s'en servir.
conditions:
- namespaceRegexes:
- "^signalements-.*"Le Secret scaleway-acces qui contient la clé d'API est l'œuf de cette poule : il ne peut pas venir d'ESO. On le crée à la main à l'installation du cluster, ou par l'outil qui crée le cluster (Terraform), et c'est le seul. La clé d'API appartient à une application IAM dont la politique ne donne que la lecture des secrets d'un projet.
Warning
Le fournisseur Scaleway d'ESO est classé alpha dans la page de stabilité du projet. Il est utilisé en production chez Lyneko, mais un changement de comportement entre deux versions d'ESO est possible : lisez les notes de version avant de mettre à jour.
Le secret est créé dans Secret Manager, puis le chart de Signalements le référence au lieu de porter le mot de passe. Ce modèle remplace celui de Sealed Secrets (même nom de fichier, même Secret produit) : on choisit l'un ou l'autre.
# chart/templates/secret-postgresql.yaml
{{- if .Values.postgresql.enabled }}
# Le mot de passe vient de Scaleway Secret Manager, pas de Git.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ .Release.Name }}-postgresql
annotations:
# Avant la base (vague -1) : le Secret doit exister quand PostgreSQL démarre.
argocd.argoproj.io/sync-wave: "-2"
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: scaleway
target:
name: {{ .Release.Name }}-postgresql
creationPolicy: Owner
data:
- secretKey: mot-de-passe
remoteRef:
key: name:signalements-{{ .Values.environnement }}-postgresql
version: latest_enabled
{{- end }}Et la base comme l'application lisent le Secret produit :
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: mot-de-passeQuelques détails qui comptent :
- La vague -2. Argo CD connaît la santé d'un ExternalSecret : le script intégré (
resource_customizations/external-secrets.io/ExternalSecret/health.lua) le déclareHealthyquand sa conditionReadyest vraie,Degradedquand elle est fausse,Progressingsinon. Une vague attend la santé de la précédente : PostgreSQL ne démarre qu'une fois le Secret produit. creationPolicy: Owner. Le Secret produit appartient à l'ExternalSecret (référence de propriétaire) : il apparaît sous lui dans l'arbre d'Argo CD, et disparaît avec lui.refreshInterval: 1h. ESO relit la valeur toutes les heures et met à jour le Secret. Mais un pod qui lit le mot de passe par une variable d'environnement ne voit pas le changement avant son redémarrage : la rotation demande aussi de redémarrer les pods (à la main, ou par un outil comme Reloader qui surveille les Secrets).- Forcer une relecture. Argo CD fournit une action
refreshpour les ExternalSecret :argocd app actions run signalements-recette refresh --kind ExternalSecret.
Retirer le mot de passe de Git
Le mot de passe de démonstration a été commité : il faut le considérer comme connu. L'ordre des opérations :
- créer un nouveau mot de passe dans Secret Manager ;
- pousser le chart modifié (ExternalSecret,
secretKeyRef), et retirer le mot de passe devalues.yaml; - changer réellement le mot de passe dans PostgreSQL (
ALTER ROLE) : la variablePOSTGRES_PASSWORDde l'image officielle ne sert qu'à l'initialisation d'un volume vide, elle ne change pas le mot de passe d'une base existante ; - redémarrer l'application pour qu'elle lise le nouveau Secret.
Réécrire l'historique Git (git filter-repo) peut s'ajouter, pour ne pas laisser le mot de passe traîner, mais ne remplace jamais la rotation : clones, forks, caches de CI et repo-server d'Argo CD ont déjà la copie.
Les secrets d'Argo CD lui-même
Argo CD a ses propres secrets, qui méritent le même soin :
| Secret | Rôle | Étiquette |
|---|---|---|
| identifiants de dépôts | lire les dépôts privés | argocd.argoproj.io/secret-type: repository ou repo-creds |
| clusters | écrire dans les clusters cibles (leçon 7) | argocd.argoproj.io/secret-type: cluster |
argocd-secret | clé de signature des sessions, mot de passe admin, secrets de webhooks | |
| secret client OIDC | se connecter au fournisseur d'identité (leçon 9) | app.kubernetes.io/part-of: argocd |
Tous peuvent être produits par ESO : un ExternalSecret dont target.template.metadata.labels pose la bonne étiquette. Pour la configuration SSO, argocd-cm référence une clé d'un autre Secret par la syntaxe $<nom-du-secret>:<clé>, à condition que ce Secret porte l'étiquette app.kubernetes.io/part-of: argocd ; sans elle, Argo CD ne le lit pas et la connexion échoue.
Sous le capot
Pour un ExternalSecret, le contrôleur d'ESO lit le magasin désigné, vérifie que le namespace de l'ExternalSecret est autorisé par les conditions du ClusterSecretStore, s'authentifie auprès du gestionnaire avec l'identifiant du magasin, lit la version demandée, puis crée ou met à jour le Secret cible, et recommence à chaque refreshInterval. Argo CD n'intervient qu'en appliquant l'ExternalSecret : la valeur ne passe ni par le repo-server, ni par Redis.
Quand Argo CD affiche un Secret (diff, interface), il masque les valeurs de data par des + : on voit qu'une valeur a changé, pas laquelle. Ce masquage concerne l'affichage ; il ne protège pas un secret injecté au rendu, qui reste en clair dans le cache.
Pièges courants
Le Secret existe aussi dans Git. Un Secret en clair dans le chart et un ExternalSecret du même nom se disputent l'objet : Argo CD le remet à sa version, ESO à la sienne. Supprimez le Secret du chart.
SecretStore "scaleway" is not ready ou namespace refusé. Vérifiez les conditions du ClusterSecretStore, puis la clé d'API.
Le secret est introuvable dans Secret Manager alors qu'il existe. Le magasin est épinglé sur un projet (projectId). Un secret créé sans préciser de projet atterrit dans le projet par défaut de la clé d'API utilisée pour le créer, qui n'est pas forcément le bon : créez les secrets en indiquant explicitement le projet.
Le mot de passe a changé, l'application utilise l'ancien. Variable d'environnement lue au démarrage : redémarrez les pods.
La base refuse le nouveau mot de passe. POSTGRES_PASSWORD ne sert qu'à l'initialisation : changez-le dans la base.
Un cluster reconstruit ne déchiffre plus aucun SealedSecret. Les clés du contrôleur n'ont pas été sauvegardées.
Sécurité
Pouvoir créer un ExternalSecret, c'est pouvoir lire le magasin. Toute personne autorisée à créer un ExternalSecret dans un namespace autorisé peut demander n'importe quel secret du projet Scaleway, et le lire dans le Secret produit. Le ClusterSecretStore est donc une frontière : restreignez ses namespaces (conditions), et séparez les projets Scaleway par niveau de sensibilité (un magasin pour la recette, un pour la production).
L'identifiant du magasin. Lecture seule, sur un seul projet, et à durée limitée : planifiez sa rotation.
Lire un Secret, c'est lire le mot de passe. Le chiffrement au repos des Secrets dans etcd et des droits RBAC sur secrets restreints restent nécessaires : ESO ne fait que déplacer l'origine de la valeur.
Dans Argo CD, les rôles qui peuvent lire les Secrets d'un cluster (via l'API d'Argo CD ou un accès au namespace) doivent être limités (leçon 9).
En production
Chez Lyneko, les secrets des applications et ceux d'Argo CD viennent de Scaleway Secret Manager par External Secrets Operator, avec un ClusterSecretStore épinglé sur le projet Scaleway « Infrastructure ». Deux leçons tirées de l'expérience :
- les secrets doivent être créés avec l'identifiant de projet explicite, sinon ils atterrissent dans le projet par défaut de la clé utilisée, et le magasin ne les trouve pas ;
- le secret client OIDC de Google utilisé par Argo CD est produit par ESO avec l'étiquette
app.kubernetes.io/part-of: argocd, sans laquelleargocd-cmne peut pas le référencer.
Sealed Secrets ou ESO ? Sealed Secrets n'a besoin d'aucun service externe, ce qui le rend attractif pour un petit cluster isolé, mais attache chaque secret à un cluster et fait de sa clé un point unique de défaillance. ESO demande un gestionnaire de secrets, mais centralise les valeurs, leur historique et leurs droits, et sépare la rotation des livraisons. Dès qu'il y a plusieurs clusters ou un gestionnaire disponible, ESO l'emporte.
Exercices
1. Un collègue propose argocd-vault-plugin « parce que c'est plus simple ». Donnez deux arguments précis contre.
Solution
Les manifestes rendus, secrets compris, sont conservés en clair dans le Redis d'Argo CD et lisibles par l'API du repo-server : compromettre Argo CD donne tous les secrets. Et un changement de secret n'est appliqué qu'à la synchronisation suivante, mélangé à une livraison sans rapport (et inversement, une livraison peut appliquer une modification de secret non prévue). C'est l'approche que la documentation d'Argo CD déconseille explicitement.
2. Écrivez l'ExternalSecret qui produit, dans le namespace argocd, le Secret argocd-google-oidc avec la clé clientSecret, utilisable depuis argocd-cm.
Solution
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: argocd-google-oidc
namespace: argocd
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: scaleway
target:
name: argocd-google-oidc
template:
metadata:
labels:
app.kubernetes.io/part-of: argocd
data:
- secretKey: clientSecret
remoteRef:
key: name:argocd-google-oidcDans argocd-cm, clientSecret: $argocd-google-oidc:clientSecret. Le namespace argocd doit être autorisé par les conditions du magasin.
3. Vous perdez le cluster de recette et le reconstruisez. Que faut-il, avec Sealed Secrets, puis avec ESO, pour que les secrets reviennent ?
Solution
Avec Sealed Secrets : restaurer les clés du contrôleur depuis une sauvegarde avant de laisser Argo CD appliquer les SealedSecret ; sans elles, il faut resceller chaque secret avec la nouvelle clé (et donc connaître toutes les valeurs). Avec ESO : recréer le seul Secret d'accès au gestionnaire (scaleway-acces), puis laisser Argo CD appliquer les ExternalSecret ; les valeurs sont restées dans Secret Manager.
4. Pourquoi l'ExternalSecret de PostgreSQL est-il en vague -2 ? Que se passerait-il en vague 0 ?
Solution
La base est en vague -1 et lit son mot de passe dans le Secret produit. En vague 0, l'ExternalSecret serait appliqué après la base : le pod PostgreSQL resterait en CreateContainerConfigError (Secret introuvable) et la vague -1 ne deviendrait jamais saine, bloquant la synchronisation indéfiniment : il n'y a pas de délai d'expiration par défaut (controller.sync.timeout.seconds: "0"), il faut intervenir ou arrêter l'opération. En vague -2, Argo CD attend que l'ExternalSecret soit Healthy (condition Ready), donc que le Secret existe.
Récapitulatif
- Un secret en clair dans Git est compromis : copié partout, conservé dans l'historique. La seule correction est la rotation.
- Argo CD recommande de résoudre les secrets dans le cluster de destination et déconseille l'injection au rendu (secrets en clair dans Redis et le repo-server).
- Sealed Secrets chiffre pour un cluster ; sa clé privée doit être sauvegardée et protégée.
- External Secrets Operator met une référence dans Git ; un ClusterSecretStore, restreint par ses
conditions, lit le gestionnaire (Scaleway Secret Manager chez Lyneko, fournisseur alpha). - Argo CD connaît la santé des ExternalSecret : une vague suffit pour que le Secret existe avant ses consommateurs.
- Les secrets d'Argo CD lui-même (dépôts, clusters, OIDC) suivent la même voie, avec les bonnes étiquettes.
Pour aller plus loin
- Argo CD, Secret Management : la position du projet et la mitigation des greffons d'injection.
- Kubernetes, Good practices for Kubernetes Secrets : chiffrement au repos, RBAC, montage.
- Leçon suivante : Sécuriser Argo CD.
Sources
- Argo CD, Secret Management
- Sealed Secrets, README (portées, renouvellement des clés, sauvegarde)
- External Secrets Operator, Scaleway Secret Manager
- External Secrets Operator, ClusterSecretStore
- External Secrets Operator, Stability and Support
- argoproj/argo-cd v3.5.3, resource_customizations/external-secrets.io/ExternalSecret/health.lua
- Argo CD, User Management (secrets référencés depuis argocd-cm)
- Kubernetes, Good practices for Kubernetes Secrets