Aller au contenu

Les secrets

À la fin, vous saurez

  • Expliquer pourquoi un secret ne doit jamais apparaître en clair dans un dépôt GitOps, même privé
  • Comparer les deux familles d'approches : secrets résolus dans le cluster ou injectés au rendu
  • Chiffrer un secret pour Git avec Sealed Secrets, et préserver sa clé
  • Référencer un secret de Scaleway Secret Manager avec External Secrets Operator
  • Gérer les secrets d'Argo CD lui-même et organiser la rotation

Prérequis

Testé avec argocd 3.5.3 external-secrets 2.11.0 sealed-secrets 0.40.0 , vérifié le 2 octobre 2026

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

OutilCe qui est dans GitOù est la valeurDépendance
Sealed SecretsLe secret, chiffré pour un clusterDans Git, chiffréeLa clé privée du contrôleur
SOPS (avec un greffon)Le secret, chiffré par des clés KMS ou ageDans Git, chiffréeLe déchiffrement se fait au rendu : injection
External Secrets OperatorUne référence (ExternalSecret)Dans un gestionnaire externeLe gestionnaire et l'identifiant d'accès
Pilote CSI Secrets StoreUne SecretProviderClassDans un gestionnaire externe, monté en fichierLe 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-wide permet de renommer dans le namespace, cluster-wide partout.
  • 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-key dans 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-passe

Quelques 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éclare Healthy quand sa condition Ready est vraie, Degraded quand elle est fausse, Progressing sinon. 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 refresh pour 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 :

  1. créer un nouveau mot de passe dans Secret Manager ;
  2. pousser le chart modifié (ExternalSecret, secretKeyRef), et retirer le mot de passe de values.yaml ;
  3. changer réellement le mot de passe dans PostgreSQL (ALTER ROLE) : la variable POSTGRES_PASSWORD de l'image officielle ne sert qu'à l'initialisation d'un volume vide, elle ne change pas le mot de passe d'une base existante ;
  4. 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 :

SecretRôleÉtiquette
identifiants de dépôtslire les dépôts privésargocd.argoproj.io/secret-type: repository ou repo-creds
clustersécrire dans les clusters cibles (leçon 7)argocd.argoproj.io/secret-type: cluster
argocd-secretclé de signature des sessions, mot de passe admin, secrets de webhooks
secret client OIDCse 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 laquelle argocd-cm ne 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-oidc

Dans 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

Voir ma constellation →

Sources