Secret Kubernetes : ni ConfigMap, ni Vault, ni chiffré

Un Secret n’est pas chiffré, seulement encodé en base64. Où mettre un mot de passe, qui peut réellement le lire, et pourquoi Vault ne remplace pas le Secret mais l’alimente.

10 min de lecturekubernetesconteneursdevopssecurite

Un mot de passe n’a rien à faire dans le code, ni dans l’image Docker, ni dans un ConfigMap. Cette phrase est facile à retenir et facile à répéter. Le problème est qu’elle laisse croire que déplacer la valeur dans un Secret règle la question de la confidentialité — alors qu’elle ne fait que la déplacer.

Voyons d’abord la répartition, puis ce que le mot « Secret » promet de trop.

La ligne de partage

text
ConfigMap
├── APP_TITLE   = "Mon application"
├── APP_VERSION = "1.0"
└── BG_COLOR    = "#0f172a"

Secret
├── DB_USER     = "..."
├── DB_PASSWORD = "..."
└── API_KEY     = "..."

Le critère est simple : est-ce que cette valeur, publiée par accident, poserait un problème ? Une couleur de fond, non. Un identifiant de base de données, oui. Le reste des différences en découle.

L’application, elle, ne voit aucune différence :

text
Image Docker  +  ConfigMap  +  Secret

                    Pod

Trois sources, un seul environnement d’exécution. C’est tout l’intérêt : au moment du docker build, l’image ne connaît aucun mot de passe.

Le brancher au Pod

yaml
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: demo-secret
        key: DB_PASSWORD

Et côté application, rien de particulier :

python
import os

password = os.getenv('DB_PASSWORD')

Comme pour un ConfigMap, envFrom permet de tout verser d’un coup :

yaml
envFrom:
  - secretRef:
      name: demo-secret

Pratique, et un peu trop : le conteneur reçoit alors toutes les clés du Secret, y compris celles dont il n’a pas besoin. Nommer explicitement ce qu’on injecte est un meilleur réflexe pour un Secret que pour un ConfigMap.

Le créer sans manipuler de base64

C’est ici que se produit le bug le plus fréquent, et il est vicieux parce que la valeur semble correcte.

bash
echo "mon-mot-de-passe" | base64

Cette commande ajoute un retour à la ligne avant l’encodage. Le Secret contient donc mon-mot-de-passe\n, l’authentification échoue, et le mot de passe affiché à l’écran paraît juste. On cherche alors du côté du réseau, des droits, du pilote de base de données — partout sauf au bon endroit.

bash
printf '%s' 'mon-mot-de-passe' | base64    # correct

Le plus sûr reste de ne jamais encoder à la main :

bash
kubectl create secret generic demo-secret \
  --from-literal=DB_USER=app \
  --from-literal=DB_PASSWORD='mon-mot-de-passe'

kubectl se charge de l’encodage, sans caractère parasite. Dans un fichier YAML, le champ stringData rend le même service :

yaml
apiVersion: v1
kind: Secret
metadata:
  name: demo-secret
type: Opaque
stringData:
  DB_USER: app
  DB_PASSWORD: mon-mot-de-passe

Confortable — et dangereux, parce que ce fichier contient un mot de passe en clair et que le premier réflexe est de le committer. Nous y revenons plus bas.

base64 n’est pas du chiffrement

La démonstration prend cinq secondes :

bash
kubectl get secret demo-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
text
mon-mot-de-passe

base64 est un encodage, pas un chiffrement : c’est une transformation réversible, publique, sans clé. Son rôle est de permettre de transporter n’importe quel octet dans du texte, y compris des données binaires comme un certificat. Il ne cache rien.

Ce qu’un Secret apporte réellement, et qui n’est pas rien :

  • un type de ressource distinct, donc des droits RBAC qu’on peut accorder séparément du reste ;
  • l’absence d’affichage accidentel : kubectl describe montre le nom des clés et la taille des valeurs, pas leur contenu ;
  • pour les fichiers montés, un stockage en mémoire (tmpfs) plutôt que sur le disque du nœud ;
  • un emplacement conventionnel, ce qui permet aux outils de chiffrement et d’audit de savoir quoi protéger.

Ce qu’il n’apporte pas par défaut : la confidentialité au repos. Dans un cluster sans configuration supplémentaire, les Secrets sont écrits dans etcd tels quels. Une sauvegarde d’etcd contient donc tous vos mots de passe en clair.

Comment activer le chiffrement au repos ?

Par une EncryptionConfiguration fournie au serveur d’API, qui chiffre les ressources choisies avant écriture dans etcd :

yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ['secrets']
    providers:
      - aescbc:
          keys:
            - name: cle-1
              secret: <clé en base64>
      - identity: {}

Deux points à connaître. L’ordre des fournisseurs compte : le premier sert au chiffrement, tous servent au déchiffrement, et identity en fin de liste permet de lire les Secrets écrits avant l’activation. Et la clé se retrouve alors dans un fichier sur les nœuds de contrôle, ce qui déplace le problème plutôt que de le supprimer — d’où l’intérêt d’un fournisseur KMS, où la clé reste chez le service de gestion de clés.

Sur un cluster géré, c’est souvent une case à cocher qui branche le KMS du fournisseur. À vérifier, car ce n’est pas toujours activé par défaut.

Qui peut lire vos Secrets

La question intéressante n’est pas comment ils sont stockés, mais qui y accède.

bash
kubectl auth can-i get secrets --namespace demo

Et le point que presque personne n’énonce : quiconque peut créer un Pod dans un namespace peut lire tous les Secrets de ce namespace. Il suffit de monter le Secret dans un conteneur et d’en afficher le contenu. Rien à contourner, c’est le fonctionnement normal.

Autrement dit, accorder le droit de créer des Pods équivaut à accorder la lecture des Secrets. C’est ce qui fait du namespace la véritable frontière de sécurité : séparer les environnements et les équipes par namespace vaut mieux que d’affiner les droits sur les Secrets à l’intérieur d’un namespace partagé.

Variables d’environnement ou fichiers montés

Les deux fonctionnent, comme pour un ConfigMap — mais pour un Secret, la recommandation s’inverse.

yaml
volumeMounts:
  - name: secrets
    mountPath: /etc/secrets
    readOnly: true
volumes:
  - name: secrets
    secret:
      secretName: demo-secret

Les variables d’environnement ont trois défauts propres aux données sensibles. Elles sont héritées par tous les processus enfants, y compris ceux qui n’ont rien à voir avec la base. Elles apparaissent dans les traces d’exception de nombreux frameworks, dans les rapports de plantage et dans les journaux de diagnostic qui vident l’environnement. Et elles sont figées au démarrage du processus, donc une rotation impose un redémarrage.

Les fichiers montés sont mis à jour par Kubernetes quand le Secret change, ce qui rend la rotation possible sans redémarrer — à condition que l’application relise ses fichiers. Le détail de cette mécanique est le même que pour la mise à jour d’un ConfigMap.

Pour un TP, les variables d’environnement suffisent et restent plus lisibles. Pour de la production, les fichiers sont le choix par défaut.

Le YAML qu’il ne faut pas committer

Le vrai risque, en pratique, n’est ni etcd ni le RBAC : c’est le dépôt Git. Un stringData avec un mot de passe en clair, poussé une fois, y reste dans l’historique même après suppression.

Trois approches, par ordre d’engagement :

  • Créer le Secret à la main avec kubectl create secret, et ne pas versionner de fichier. Simple, mais rien ne documente ce qui existe dans le cluster.
  • Chiffrer le fichier avant de le committer — SOPS, ou Sealed Secrets, qui produit un objet que seul le cluster peut déchiffrer. Le dépôt reste la source de vérité, ce qui est l’esprit du GitOps.
  • Ne stocker que des références, la valeur vivant dans un gestionnaire externe. C’est le rôle de Vault, et c’est le dernier point.

Secret ou Vault ?

La question est mal posée, de la même façon que « Docker ou Kubernetes » : les deux ne se remplacent pas, ils se succèdent.

Un Secret est un objet Kubernetes — une petite boîte dans un namespace, avec un nom, des clés et des valeurs. Il vit dans le cluster et n’existe que pour lui.

Vault est un service externe, spécialisé dans la conservation, la distribution et la rotation des secrets, pour plusieurs applications et plusieurs clusters à la fois. Il apporte ce que Kubernetes ne fait pas : un journal d’accès complet, des secrets à durée de vie limitée, une politique de rotation, des identifiants engendrés à la demande.

Et dans le montage habituel, Vault alimente des Secrets Kubernetes :

Le point remarquable est à droite du schéma : le code de l’application ne change pas. Elle lit toujours une variable ou un fichier. Ce qui change, c’est l’origine de la valeur — et c’est précisément pourquoi commencer par un simple Secret n’est pas une impasse. Le jour où Vault arrive, on remplace la source sans toucher à l’application.

D’où la progression raisonnable : Secret pour apprendre et pour un TP ; Secret chiffré dans Git dès qu’une équipe travaille sur le dépôt ; gestionnaire externe quand plusieurs applications, plusieurs clusters ou des exigences d’audit entrent en jeu.

Deux Secrets que vous utiliserez sans le savoir

Le champ type ne sert pas qu’à décorer. Deux valeurs sont d’usage courant.

kubernetes.io/dockerconfigjson contient les identifiants d’un registre privé :

bash
kubectl create secret docker-registry regcred \
  --docker-server=registre.example.com \
  --docker-username=robot \
  --docker-password='...'
yaml
spec:
  imagePullSecrets:
    - name: regcred

Sans lui, une image hébergée dans un registre privé produit un ImagePullBackOff — la même erreur qu’une image absente, pour une raison entièrement différente. La distinction se lit dans kubectl describe pod, qui mentionne alors une authentification refusée.

kubernetes.io/tls contient un certificat et sa clé privée, et c’est ce que consomme un Ingress pour servir en HTTPS.

Deux réglages qui coûtent une ligne

yaml
immutable: true

Un Secret immuable ne peut plus être modifié, seulement supprimé et recréé. Cela évite un changement accidentel sur une valeur critique, et allège la charge du serveur d’API, qui n’a plus à surveiller cet objet.

yaml
spec:
  automountServiceAccountToken: false

Par défaut, chaque Pod reçoit le jeton de son compte de service, monté dans son système de fichiers. Une application qui ne parle pas à l’API Kubernetes n’en a aucun besoin, et ce jeton est un moyen d’accès de plus en cas de compromission du conteneur. Le désactiver quand il est inutile est gratuit.

Faire tourner un mot de passe

Le raisonnement est exactement celui du rolling update avec migration, et pour la même raison : pendant le remplacement des Pods, deux générations coexistent.

Un mot de passe remplacé d’un coup casse tous les Pods qui tournent encore avec l’ancien. La séquence correcte est donc :

  1. Ajouter le nouvel identifiant côté base, sans retirer l’ancien — les deux sont valides.
  2. Mettre à jour le Secret.
  3. Redémarrer les Pods progressivement, avec kubectl rollout restart.
  4. Vérifier que plus aucun Pod n’utilise l’ancien identifiant.
  5. Révoquer l’ancien.

Ajouter avant de retirer, une fois de plus. C’est la même forme que la migration de schéma, appliquée aux identifiants.

Ce qu’il faut retenir

text
ConfigMap = réglages NON sensibles
Secret    = réglages SENSIBLES

La règle est juste, mais elle mérite sa suite : un Secret n’est pas chiffré parce qu’il s’appelle Secret. Il vous donne un emplacement conventionnel, des droits séparés et la discrétion à l’affichage. La confidentialité réelle vient du chiffrement au repos, du cloisonnement par namespace, et de la discipline qui empêche le mot de passe d’atterrir dans le dépôt Git.

Quant à Vault, ce n’est pas l’étape suivante d’un Secret : c’est ce qui le remplit. Le Pod, lui, continue de lire une variable d’environnement — et c’est ce qui rend la transition possible sans réécrire l’application.

Pour la suite : le ConfigMap et ses deux modes de consommation, le Deployment qui les injecte, et le projet complet où ces objets fonctionnent ensemble.

Ce sujet fait partie d’un cours complet

Développement et déploiement de solutions de données — les premiers modules sont en accès libre.

Voir le plan du cours

Continuer sur le même sujet