Aller au contenu

ConfigMaps et Secrets

200 Pratiquer ⏱ 1 h 15 kuberneteskubectlpostgresql

À la fin, vous saurez

  • Créer une ConfigMap et un Secret, à partir de valeurs ou de fichiers, et les consommer en variables d'environnement ou en fichiers
  • Prévoir ce qui se passe quand on modifie une ConfigMap ou un Secret déjà utilisé par des pods
  • Montrer que l'encodage base64 d'un Secret ne protège rien, et dire ce qui le protège réellement
  • Identifier qui peut lire un Secret dans un namespace, directement ou indirectement
  • Fournir à un Deployment les identifiants d'un registre privé

Prérequis

Testé avec kubectl 1.36.0 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

À la leçon 5, le Deployment de Signalements démarre deux pods à partir de l'image ghcr.io/lyneko-formation/signalements:1.2.0. Ils répondent sur /, mais /sante échoue : l'application ne sait pas où est sa base. Il lui manque DATABASE_URL, et accessoirement APP_VERSION.

La solution la plus rapide serait d'écrire ces valeurs dans le manifeste du Deployment, à la ligne env:. Elle a deux défauts, de gravité très différente :

  • La configuration et le code se mélangent. La même image doit tourner en préproduction et en production, contre deux bases différentes. Si l'adresse de la base est écrite dans le manifeste du Deployment, chaque environnement a son propre manifeste, et la moindre différence de configuration devient une différence de déploiement. Le manifeste The Twelve-Factor App, qui a fixé en 2011 une bonne partie des usages des applications hébergées, en fait sa troisième règle : la configuration, c'est-à-dire tout ce qui varie d'un déploiement à l'autre, se sépare strictement du code.
  • Le mot de passe de la base finit dans Git. DATABASE_URL contient postgresql://signalements:<mot de passe>@.... Un manifeste versionné est lu par toute l'équipe, copié dans chaque clone, conservé dans l'historique pour toujours. Le cours GitOps avec Argo CD a montré ce que coûte un secret commité.

Kubernetes fournit deux objets pour sortir ces valeurs du manifeste de l'application : la ConfigMap pour la configuration ordinaire, le Secret pour ce qui doit rester confidentiel. Ils se ressemblent beaucoup, et c'est le piège : un Secret n'est pas, par lui-même, beaucoup plus protégé qu'une ConfigMap. Cette leçon montre ce qu'il protège vraiment, et ce qu'il faut ajouter autour.

Les concepts

La ConfigMap

Une ConfigMap est un objet de l'API qui contient des paires clé-valeur, en texte (champ data) ou en binaire encodé en base64 (champ binaryData). Elle ne fait rien par elle-même : ce sont les pods qui la consomment. Elle vit dans un namespace, et seuls les pods du même namespace peuvent l'utiliser.

Quelques règles, tirées de la documentation :

  • les clés ne contiennent que des caractères alphanumériques, -, _ et . ; une clé ne peut pas figurer à la fois dans data et dans binaryData ;
  • les données d'une ConfigMap ne peuvent pas dépasser 1 Mio : au-delà, il faut un volume ou un service de fichiers ;
  • une ConfigMap n'apporte aucune confidentialité : tout ce qui y est écrit se lit en clair par quiconque peut lire l'objet.

Un pod consomme une ConfigMap de quatre façons : dans la commande ou les arguments d'un conteneur, en variables d'environnement, en fichiers dans un volume en lecture seule, ou en lisant l'API depuis le code de l'application. Les trois premières sont prises en charge par le kubelet au démarrage du conteneur ; la quatrième demande que l'application parle à l'API, ce qui est rare et demande des droits.

Le Secret

Un Secret a la même forme qu'une ConfigMap : des paires clé-valeur dans data, consommées en variables ou en fichiers. Les différences sont ailleurs :

  • les valeurs du champ data sont encodées en base64 ; un champ stringData permet d'écrire les valeurs en clair dans le manifeste, et l'API les encode elle-même ;
  • un Secret a un type, qui annonce le format attendu des clés ;
  • le kubelet ne copie un Secret que sur les nœuds où tourne un pod qui l'utilise, et le monte dans un système de fichiers en mémoire (tmpfs), jamais sur le disque du nœud ; il en efface sa copie quand le pod disparaît ;
  • l'API permet de le chiffrer au repos dans etcd, et les politiques de contrôle d'accès le traitent à part.

Un Secret est lui aussi limité à 1 Mio, pour décourager l'usage de Secrets géants qui épuiseraient la mémoire du serveur d'API et du kubelet.

Les types prédéfinis sont les suivants :

TypeUsage
OpaqueDonnées quelconques : le type par défaut
kubernetes.io/dockerconfigjsonIdentifiants d'un registre d'images, au format de ~/.docker/config.json
kubernetes.io/tlsUn certificat et sa clé privée (tls.crt, tls.key)
kubernetes.io/basic-authUn nom d'utilisateur et un mot de passe
kubernetes.io/ssh-authUne clé privée SSH
kubernetes.io/service-account-tokenUn jeton de compte de service à longue durée, à éviter aujourd'hui
kubernetes.io/dockercfgL'ancien format de configuration de Docker
bootstrap.kubernetes.io/tokenLes jetons d'amorçage des nœuds

Le type ne change rien au stockage ni à la protection : il permet à l'API de valider la présence des clés attendues, et aux outils (le kubelet pour un registre, un contrôleur d'entrée pour un certificat) de savoir quoi en faire.

Base64 n'est pas du chiffrement

C'est la confusion la plus répandue à propos des Secrets, et la documentation la combat dès son premier encadré : les Secrets sont, par défaut, stockés en clair dans etcd, la base de données du plan de contrôle. Le base64 est un encodage, réversible par n'importe qui, sans clé. Il existe parce que les valeurs d'un Secret peuvent être binaires (une clé, un certificat) et qu'un document JSON ou YAML ne transporte proprement que du texte.

La démonstration est immédiate, et la section En pratique la fait sur la vraie valeur de DATABASE_URL.

Ce qui protège un Secret

Puisque l'encodage ne protège rien, la protection d'un Secret repose sur quatre couches, qui ne relèvent pas toutes de vous :

  1. Le chiffrement au repos dans etcd. Le serveur d'API peut chiffrer les Secrets avant de les écrire dans etcd, grâce à un fichier EncryptionConfiguration. Il protège contre la fuite d'une sauvegarde d'etcd ou d'un disque du plan de contrôle. Il ne protège pas contre quelqu'un qui lit le Secret par l'API : le serveur d'API le déchiffre pour lui.
  2. Le contrôle d'accès (RBAC). Qui peut faire get, list ou watch sur les Secrets d'un namespace. La documentation insiste : un droit list ou watch sur les Secrets permet de lire leur contenu, pas seulement leurs noms.
  3. Le contrôle de ce qui tourne. Toute personne autorisée à créer un pod dans un namespace peut monter n'importe quel Secret de ce namespace dans son pod, et le lire. Le droit de créer un Deployment suffit, puisque le Deployment crée des pods. Un conteneur privilégié peut lire tous les Secrets utilisés sur son nœud.
  4. L'application elle-même. Une fois le Secret dans le conteneur, il n'est protégé que par l'application : un mot de passe écrit dans un journal, renvoyé dans un message d'erreur ou transmis à un service tiers est perdu, quel que soit le soin mis dans les trois couches précédentes.

Important

Le cloisonnement des Secrets, c'est le namespace. Puisque créer un pod dans un namespace donne accès à tous ses Secrets, deux applications qui ne doivent pas partager leurs secrets ne doivent pas partager de namespace, ni d'équipe ayant le droit de déployer dans les deux.

Ce qui se met à jour, et ce qui ne se met pas à jour

On modifie une ConfigMap ou un Secret déjà consommé par des pods en marche. Que voient les pods ? La réponse dépend de la façon dont ils le consomment, et c'est l'une des sources de confusion les plus fréquentes :

ConsommationEffet d'une modification
Variables d'environnement (env, envFrom)Aucun. Les variables sont fixées au démarrage du conteneur. Il faut redémarrer les pods
Fichiers dans un volumeLe kubelet met à jour les fichiers après un délai, sans redémarrer le conteneur. L'application doit relire le fichier pour en tenir compte
Fichier monté avec subPathAucun. Le fichier est figé au démarrage
Objet marqué immutable: trueModification refusée par l'API : il faut créer un nouvel objet

Le délai de mise à jour d'un volume dépend de la stratégie de détection des changements du kubelet (Watch par défaut) : il peut aller jusqu'à la période de synchronisation du kubelet plus le délai de propagation de son cache. Comptez de quelques secondes à environ une minute, sans garantie.

Les objets immuables

Depuis Kubernetes 1.21, une ConfigMap ou un Secret peut être marqué immutable: true. Plus personne ne peut en modifier le contenu ; on ne peut que le supprimer et en créer un autre. Deux bénéfices : on se protège d'une modification accidentelle qui toucherait d'un coup tous les pods qui l'utilisent, et l'on soulage le plan de contrôle, puisque les kubelets cessent de surveiller ces objets. Le prix : chaque changement de configuration passe par un nouveau nom (par exemple signalements-config-v2) et une mise à jour du Deployment qui le référence, donc un déploiement progressif, que l'on peut annuler. C'est une discipline saine.

En pratique

Les commandes qui suivent s'exécutent sur le cluster formation de la leçon 3, dans le namespace signalements. Celles qui portent l'option --dry-run=client ne contactent pas le cluster : elles fabriquent le manifeste localement et l'affichent, et leurs sorties ci-dessous sont réelles, produites avec kubectl 1.36. Les autres décrivent ce que vous devez observer, sans sortie inventée.

La base PostgreSQL de formation est installée à la leçon 9, avec le Service postgres du namespace. Vous pouvez faire cette leçon avant : /sante échouera simplement tant que la base n'existe pas.

Une ConfigMap pour la configuration ordinaire

$ kubectl create configmap signalements-config -n signalements \
    --from-literal=APP_VERSION=1.2.0 --from-literal=LOG_LEVEL=info \
    --dry-run=client -o yaml
apiVersion: v1
data:
  APP_VERSION: 1.2.0
  LOG_LEVEL: info
kind: ConfigMap
metadata:
  name: signalements-config
  namespace: signalements

--from-literal ajoute une clé avec sa valeur ; --dry-run=client -o yaml montre l'objet sans le créer. C'est la meilleure façon d'écrire un manifeste juste du premier coup : on le génère, on le relit, on l'enregistre dans le dépôt (> configmap.yaml), et on l'applique avec kubectl apply -f.

Une ConfigMap peut aussi porter un fichier entier. Pour régler gunicorn par un fichier plutôt que par des options de ligne de commande :

$ cat gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 2
accesslog = "-"
$ kubectl create configmap signalements-gunicorn -n signalements \
    --from-file=gunicorn.conf.py --dry-run=client -o yaml
apiVersion: v1
data:
  gunicorn.conf.py: |
    bind = "0.0.0.0:8000"
    workers = 2
    accesslog = "-"
kind: ConfigMap
metadata:
  name: signalements-gunicorn
  namespace: signalements

Le nom du fichier devient la clé, son contenu la valeur. Monté en volume, ce sera de nouveau un fichier.

Un Secret pour l'adresse de la base

$ kubectl create secret generic signalements-db -n signalements \
    --from-literal=DATABASE_URL='postgresql://signalements:Exemple-2026@postgres:5432/signalements' \
    --dry-run=client -o yaml
apiVersion: v1
data:
  DATABASE_URL: cG9zdGdyZXNxbDovL3NpZ25hbGVtZW50czpFeGVtcGxlLTIwMjZAcG9zdGdyZXM6NTQzMi9zaWduYWxlbWVudHM=
kind: Secret
metadata:
  name: signalements-db
  namespace: signalements

Remarquez deux choses. La valeur est encodée en base64. Et le manifeste n'a pas de champ type : kubectl ne l'écrit pas pour un Secret générique, et le serveur d'API lui donnera le type Opaque à la création.

Voici ce que « encodé » veut dire :

$ echo 'cG9zdGdyZXNxbDovL3NpZ25hbGVtZW50czpFeGVtcGxlLTIwMjZAcG9zdGdyZXM6NTQzMi9zaWduYWxlbWVudHM=' | base64 -d
postgresql://signalements:Exemple-2026@postgres:5432/signalements

Une commande, aucune clé, le mot de passe en clair. Sur le cluster, kubectl get secret signalements-db -o jsonpath='{.data.DATABASE_URL}' | base64 -d fait la même chose pour quiconque a le droit get sur ce Secret. Un manifeste de Secret commité dans Git est donc un mot de passe commité dans Git.

Warning

Le mot de passe d'exemple passe ici en argument de commande, donc dans l'historique du shell, et ce manifeste ne doit pas être enregistré dans un dépôt. Pour un vrai secret, lisez la valeur dans un fichier aux droits restreints (--from-file=DATABASE_URL=chemin) ou, mieux, laissez un outil la fournir (voir En production).

Créez réellement les deux objets, sans le --dry-run, puis vérifiez :

$ kubectl create configmap signalements-config -n signalements \
    --from-literal=APP_VERSION=1.2.0 --from-literal=LOG_LEVEL=info
$ kubectl create secret generic signalements-db -n signalements \
    --from-file=DATABASE_URL=./database-url.txt
$ kubectl get configmap,secret -n signalements
$ kubectl describe secret signalements-db -n signalements

kubectl describe affiche le nom, le type Opaque et, pour chaque clé, sa taille en octets, mais pas sa valeur : c'est une politesse de l'outil, pas une protection, puisque kubectl get -o yaml montre la valeur encodée.

Le fichier database-url.txt ne doit contenir que la valeur, sans saut de ligne final (sinon le saut de ligne fait partie du mot de passe) : créez-le avec printf '%s' '...' > database-url.txt, puis supprimez-le.

Consommer les deux dans le Deployment

Dans le manifeste du Deployment de la leçon 5, le conteneur reçoit maintenant sa configuration :

    spec:
      containers:
        - name: signalements
          image: ghcr.io/lyneko-formation/signalements:1.2.0
          ports:
            - containerPort: 8000
          envFrom:
            - configMapRef:
                name: signalements-config
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: signalements-db
                  key: DATABASE_URL
  • envFrom avec configMapRef transforme chaque clé de la ConfigMap en variable d'environnement : APP_VERSION et LOG_LEVEL. C'est pratique, mais l'on ne voit plus, dans le manifeste, la liste des variables reçues.
  • env avec valueFrom.secretKeyRef prend une clé précise d'un Secret. Pour un secret, préférez cette forme explicite : on sait exactement quelle valeur entre dans quel conteneur.

Appliquez, puis observez le déploiement progressif : modifier le modèle de pod (ajouter des variables en fait partie) crée un nouveau ReplicaSet et remplace les pods un par un.

$ kubectl apply -f deployment.yaml
$ kubectl rollout status deployment/signalements -n signalements
$ kubectl exec -n signalements deploy/signalements -- printenv APP_VERSION LOG_LEVEL

La dernière commande affiche 1.2.0 et info. Une fois la base installée (leçon 9), GET /sante répond enfin 200.

Monter un fichier de configuration

Pour la ConfigMap signalements-gunicorn, la consommation naturelle est un fichier :

    spec:
      containers:
        - name: signalements
          # ... comme plus haut
          volumeMounts:
            - name: config-gunicorn
              mountPath: /etc/gunicorn
              readOnly: true
      volumes:
        - name: config-gunicorn
          configMap:
            name: signalements-gunicorn

Le fichier apparaît dans le conteneur sous /etc/gunicorn/gunicorn.conf.py, et gunicorn le lit avec l'option --config /etc/gunicorn/gunicorn.conf.py. Le point de montage remplace tout le contenu du répertoire /etc/gunicorn de l'image : on monte donc une ConfigMap dans un répertoire qui lui est dédié.

Modifier, et constater ce qui change

Passez LOG_LEVEL à debug :

$ kubectl patch configmap signalements-config -n signalements \
    --type merge -p '{"data":{"LOG_LEVEL":"debug"}}'
$ kubectl exec -n signalements deploy/signalements -- printenv LOG_LEVEL

La variable vaut toujours info : les pods en marche ne voient rien. Pour qu'ils prennent la nouvelle valeur, il faut les recréer :

$ kubectl rollout restart deployment/signalements -n signalements
$ kubectl rollout status deployment/signalements -n signalements
$ kubectl exec -n signalements deploy/signalements -- printenv LOG_LEVEL

rollout restart ajoute au modèle de pod une annotation contenant l'heure du redémarrage. Le modèle ayant changé, le Deployment remplace les pods progressivement, sans coupure, comme pour une nouvelle version.

Faites la même expérience sur la ConfigMap montée en fichier : modifiez workers = 2 en workers = 3, attendez une minute, puis kubectl exec ... -- cat /etc/gunicorn/gunicorn.conf.py. Le fichier a changé dans le conteneur, sans redémarrage. Mais gunicorn, lui, ne relit sa configuration qu'au démarrage ou sur un signal SIGHUP : il tourne toujours avec deux processus. Un fichier mis à jour n'est utile qu'à une application qui le relit.

Tip

Les outils de déploiement automatisent ce redémarrage. Les modèles Helm ajoutent souvent au pod une annotation contenant la somme de contrôle de la ConfigMap : quand la ConfigMap change, l'annotation change, donc le modèle de pod, donc les pods sont remplacés. Avec des ConfigMaps immuables à nom versionné, le changement de nom fait le même travail. Le cours Helm y reviendra.

Tirer une image d'un registre privé

Les images de Lyneko sont dans le registre privé de Scaleway (rg.fr-par.scw.cloud), qui exige une authentification (cours Scaleway en pratique, leçon 7). Le kubelet, qui tire les images, a besoin d'identifiants : c'est le rôle d'un Secret de type kubernetes.io/dockerconfigjson.

$ kubectl create secret docker-registry registre-scw -n signalements \
    --docker-server=rg.fr-par.scw.cloud --docker-username=nologin \
    --docker-password=EXEMPLE-CLE-SECRETE --dry-run=client -o yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJyZy5mci1wYXIuc2N3LmNsb3VkIjp7InVzZXJuYW1lIjoibm9sb2dpbiIsInBhc3N3b3JkIjoiRVhFTVBMRS1DTEUtU0VDUkVURSIsImF1dGgiOiJibTlzYjJkcGJqcEZXRVZOVUV4RkxVTk1SUzFUUlVOU1JWUkYifX19
kind: Secret
metadata:
  name: registre-scw
  namespace: signalements
type: kubernetes.io/dockerconfigjson

Cette fois, kubectl écrit le type. Décodée, la valeur est exactement un fichier de configuration Docker :

{
    "auths": {
        "rg.fr-par.scw.cloud": {
            "username": "nologin",
            "password": "EXEMPLE-CLE-SECRETE",
            "auth": "bm9sb2dpbjpFWEVNUExFLUNMRS1TRUNSRVRF"
        }
    }
}

Le champ auth est lui aussi du base64, celui de nologin:EXEMPLE-CLE-SECRETE. Le pod y fait référence par imagePullSecrets :

    spec:
      imagePullSecrets:
        - name: registre-scw
      containers:
        - name: signalements
          image: rg.fr-par.scw.cloud/signalements-exemple/signalements:1.2.0

Le mot de passe est ici une clé d'API Scaleway : donnez-la à une application IAM qui n'a que le jeu de permissions ContainerRegistryReadOnly (cours Le cloud : les fondamentaux, leçon 7). Si ce Secret fuit, l'attaquant lit vos images, rien de plus.

Une ConfigMap immuable

apiVersion: v1
kind: ConfigMap
metadata:
  name: signalements-config-v2
  namespace: signalements
immutable: true
data:
  APP_VERSION: "1.2.0"
  LOG_LEVEL: info

Après kubectl apply, toute tentative de modifier data est refusée par le serveur d'API. Pour changer la configuration, on crée signalements-config-v3, on modifie configMapRef.name dans le Deployment, et l'on applique : c'est un déploiement progressif ordinaire, que kubectl rollout undo sait annuler. L'ancienne ConfigMap se supprime quand plus aucun ReplicaSet conservé ne la référence.

Sous le capot

Le chemin d'un Secret jusqu'au conteneur

  1. kubectl create secret envoie l'objet au serveur d'API. Celui-ci le valide, le chiffre si une configuration de chiffrement est active, et l'écrit dans etcd sous la clé /registry/secrets/signalements/signalements-db.
  2. Le planificateur place un pod du Deployment sur un nœud. Le kubelet de ce nœud voit que le pod référence le Secret, et le demande au serveur d'API. Le kubelet d'un nœud n'est autorisé à lire que les Secrets des pods qui lui sont attribués : c'est le rôle de l'autorisation Node du serveur d'API.
  3. Pour une variable d'environnement, le kubelet passe la valeur décodée au moteur de conteneurs, qui la place dans l'environnement du processus au démarrage. Elle y restera, figée, jusqu'à la mort du conteneur.
  4. Pour un volume, le kubelet écrit les fichiers dans un tmpfs du nœud, puis le monte dans le conteneur.

Pourquoi les fichiers changent d'un coup

Les fichiers d'une ConfigMap ou d'un Secret montés en volume sont écrits par un composant du kubelet, l'atomic writer, dont le code explique le mécanisme. Les fichiers visibles dans le conteneur ne sont pas de vrais fichiers, mais des liens symboliques vers un répertoire ..data, lui-même lien vers un répertoire horodaté qui contient les vraies données. Pour une mise à jour, le kubelet écrit le nouveau contenu dans un nouveau répertoire horodaté, crée un lien ..data_tmp vers lui, puis le renomme en ..data. Le renommage est atomique : l'application voit l'ancienne version de tous les fichiers ou la nouvelle version de tous les fichiers, jamais un mélange.

Ce mécanisme explique aussi le piège de subPath : un montage subPath monte directement le fichier pointé au moment du démarrage, et non le répertoire de liens. Quand ..data change de cible, le fichier monté, lui, ne change pas.

Si vous listez le répertoire avec ls -la /etc/gunicorn dans le conteneur, vous verrez ces liens et le répertoire ..data. Une application qui surveille ses fichiers de configuration avec inotify doit surveiller le lien ..data, pas le fichier, que le kubelet ne modifie jamais en place.

Le chiffrement au repos

Le serveur d'API lit un fichier EncryptionConfiguration (option --encryption-provider-config), qui liste, pour chaque type de ressource, des fournisseurs dans l'ordre :

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          apiVersion: v2
          name: kms-externe
          endpoint: unix:///var/run/kms/socket.sock
      - identity: {}

Le premier fournisseur chiffre toute nouvelle écriture ; tous sont essayés, dans l'ordre, pour déchiffrer à la lecture. identity signifie « pas de chiffrement » : le mettre en dernier permet de relire les Secrets écrits avant l'activation. Les fournisseurs à clé locale (aescbc, aesgcm, secretbox) gardent la clé dans un fichier du plan de contrôle : un attaquant qui vole le disque vole aussi la clé. Le fournisseur kms délègue le chiffrement des clés à un service externe ; la version 1 de son interface est dépréciée depuis Kubernetes 1.28 et désactivée par défaut depuis la 1.29, la documentation recommande la version 2.

Activer le chiffrement ne rechiffre pas les Secrets existants : il faut les réécrire, par exemple avec kubectl get secrets --all-namespaces -o json | kubectl replace -f -, que donne la documentation.

Sur un Kubernetes managé, tout cela relève du fournisseur. La FAQ de Kapsule indique, au 5 octobre 2026, que les Secrets sont chiffrés au repos dans etcd, que seuls les Secrets le sont (pas les ConfigMaps ni les autres objets), et le modèle de responsabilité partagée de Scaleway place la gestion et le renouvellement de cette clé du côté de Scaleway. Une raison de plus de ne rien mettre de confidentiel dans une ConfigMap.

Pièges courants

Croire qu'un Secret est chiffré parce qu'il est illisible. C'est du base64. base64 -d le lit.

Commiter un manifeste de Secret. Même « pour la préproduction », même dans un dépôt privé. Il reste dans l'historique de tous les clones. Voir En production.

Le saut de ligne final. Un fichier créé avec echo se termine par un saut de ligne, que --from-file embarque dans la valeur. Le mot de passe a alors un caractère de trop, et la connexion à la base échoue avec une erreur d'authentification incompréhensible. printf '%s' ou echo -n l'évitent ; kubectl describe secret montre la taille, qui trahit l'octet de trop.

Attendre qu'une variable d'environnement se mette à jour. Elle ne le fera jamais. kubectl rollout restart après chaque modification, ou des objets immuables à nom versionné.

Un fichier monté avec subPath qui ne bouge plus. Monter la ConfigMap dans un répertoire dédié plutôt qu'un fichier isolé dans un répertoire existant.

Une clé introuvable. Un conteneur dont une variable référence une clé absente, ou un Secret ou une ConfigMap inexistants, ne démarre pas : il reste en CreateContainerConfigError, et kubectl describe pod donne le nom de l'objet ou de la clé manquants. Pour un objet monté en volume, le pod reste en ContainerCreating, et les événements signalent l'échec du montage (FailedMount). Si l'absence est acceptable, la référence peut être marquée optional: true. La leçon 12 reprend ce diagnostic.

Une ConfigMap dans le mauvais namespace. Un pod ne peut référencer que des objets de son namespace. Une ConfigMap créée dans default est invisible pour un pod de signalements.

Sécurité

  • Le namespace est la frontière. Créer un pod, ou un Deployment, dans un namespace permet de lire tous ses Secrets. Séparez les applications qui ne doivent pas partager leurs secrets, et limitez qui peut y déployer.
  • Pas de list ni de watch sur les Secrets pour les humains et les applications qui n'en ont pas besoin : la documentation rappelle que list revient à lire tous les contenus. Le cours Sécurité de Kubernetes détaille le RBAC.
  • Un Secret par consommateur, avec le minimum de clés : secretKeyRef sur une clé précise plutôt que envFrom sur un Secret qui contient tout.
  • Fichier plutôt que variable pour un secret sensible. Une variable d'environnement est héritée par tous les processus fils, apparaît dans /proc/<pid>/environ pour le même utilisateur et pour root (cours Linux : premiers pas, leçon 7), et finit parfois dans un rapport d'erreur qui liste tout l'environnement. Un fichier dans un tmpfs, en mode 0400, se lit seulement quand on en a besoin. kubectl describe pod n'affiche pas la valeur d'une variable tirée d'un Secret, seulement sa référence ; c'est utile, mais insuffisant.
  • Le chiffrement au repos est un minimum sur un cluster que vous administrez ; sur Kapsule, il est fourni pour les Secrets.
  • Les conteneurs privilégiés voient tous les Secrets montés sur leur nœud : un pod privilégié est un risque pour tous les secrets du nœud, pas seulement pour le sien.

En production

  • Les Secrets ne s'écrivent pas à la main ni dans Git. Chez Lyneko, les valeurs vivent dans Scaleway Secret Manager (Scaleway en pratique, leçon 8), et External Secrets Operator les recopie dans des Secrets Kubernetes ; le manifeste versionné, un ExternalSecret, ne contient que la référence. L'autre approche, Sealed Secrets, chiffre le Secret pour qu'il puisse être commité. Le cours GitOps avec Argo CD, leçon 8 compare les deux.
  • La configuration ordinaire, elle, se versionne : les ConfigMaps font partie des manifestes de l'application, relus et déployés comme le reste.
  • Le redémarrage après un changement se décide. Soit des objets immuables à nom versionné, que le Deployment référence (l'historique de déploiement suit l'historique de configuration), soit une annotation de somme de contrôle posée par l'outil de déploiement. Un redémarrage manuel oublié est la cause d'un grand nombre de « la configuration ne marche pas ».
  • La rotation d'un secret (un nouveau mot de passe de base) se fait sans coupure en deux temps : l'application doit accepter l'ancien et le nouveau le temps du remplacement des pods. Le cours Scaleway en pratique, leçon 8 décrit la technique des deux comptes qui alternent.
  • La taille compte : 1 Mio par objet, et chaque ConfigMap ou Secret surveillé coûte au serveur d'API et aux kubelets. Une configuration volumineuse (un modèle, un jeu de données) n'a rien à faire dans une ConfigMap.

Exercices

1. Le mot de passe de trop (niveau 100). Un collègue a créé le Secret de la base avec echo 'postgresql://...' > url.txt puis kubectl create secret generic signalements-db --from-file=DATABASE_URL=url.txt. Les pods démarrent, mais /sante répond une erreur d'authentification. Quelle est la cause probable, et comment la confirmer sans afficher le secret ?

Solution

echo ajoute un saut de ligne final, que --from-file a embarqué : la valeur se termine par \n, et le dernier élément de l'URL (le nom de la base) ou le mot de passe, selon la façon dont le pilote l'analyse, est faux. kubectl describe secret signalements-db affiche la taille de la clé en octets : elle dépasse d'un octet la longueur de l'URL attendue. On peut aussi vérifier le dernier octet sans afficher la valeur : kubectl get secret signalements-db -o jsonpath='{.data.DATABASE_URL}' | base64 -d | tail -c 1 | od -c affiche \n. Correction : recréer le Secret depuis un fichier écrit avec printf '%s', puis kubectl rollout restart.

2. Qui peut lire le secret ? (niveau 200). Dans le namespace signalements, une stagiaire a un rôle qui lui permet seulement de créer et modifier des Deployments, sans aucun droit sur les Secrets. Peut-elle lire signalements-db ? Comment ?

Solution

Oui. Elle peut créer un Deployment dont le pod monte le Secret (en variable ou en volume) et exécute une commande qui l'affiche dans ses journaux, par exemple printenv DATABASE_URL, puis lire les journaux si elle en a le droit, ou envoyer la valeur vers un service extérieur si elle ne l'a pas. La documentation le dit explicitement : quiconque peut créer un pod dans un namespace peut lire tous les Secrets de ce namespace, y compris indirectement par un Deployment. Le cloisonnement passe par le namespace : les secrets de production dans un namespace où seules les personnes et les pipelines habilités peuvent déployer.

3. Mise à jour à chaud (niveau 200). Signalements lit LOG_LEVEL dans son environnement. On voudrait pouvoir changer le niveau de journalisation sans redémarrer les pods. Proposez une modification de l'application et du manifeste, et dites ce qui reste à la charge de l'application.

Solution

Passer le réglage par un fichier dans un volume plutôt que par une variable : une ConfigMap signalements-journalisation avec une clé niveau, montée en volume dans /etc/signalements/journalisation (un répertoire dédié, sans subPath). Le kubelet mettra le fichier à jour après un délai d'environ une minute au plus, sans redémarrer le conteneur. L'application doit relire ce fichier : périodiquement (toutes les trente secondes, par exemple) ou en surveillant le lien ..data avec inotify, puis changer le niveau de son journal. Elle doit aussi supporter une valeur invalide sans tomber. C'est plus de code que rollout restart ; ce n'est justifié que si redémarrer les pods coûte réellement quelque chose.

4. Un Secret sans mot de passe dans Git (niveau 200). Votre dépôt contient les manifestes de Signalements. Écrivez la liste des fichiers qui y figurent pour la configuration, et dites, pour chacun, s'il est versionné et pourquoi.

Solution

Versionnés : la ConfigMap signalements-config (rien de confidentiel), la ConfigMap signalements-gunicorn, le Deployment avec ses références configMapRef, secretKeyRef et imagePullSecrets (des noms, pas des valeurs), et, si l'on utilise External Secrets, les manifestes ExternalSecret qui décrivent où chercher les valeurs dans Secret Manager. Non versionnés : les Secrets signalements-db et registre-scw eux-mêmes, créés dans le cluster par External Secrets (ou à la main lors de l'amorçage), ou bien versionnés uniquement sous forme chiffrée par Sealed Secrets. Le fichier database-url.txt de la leçon est supprimé après usage.

Récapitulatif

  • La configuration se sépare de l'image : une ConfigMap pour l'ordinaire, un Secret pour le confidentiel, consommés en variables d'environnement (envFrom, env.valueFrom) ou en fichiers (volume).
  • Un Secret est encodé en base64, pas chiffré : base64 -d le lit. Ce qui le protège : le chiffrement au repos dans etcd (fourni pour les Secrets sur Kapsule), le RBAC, le cloisonnement par namespace (créer un pod permet de lire tous les Secrets du namespace), et l'application elle-même.
  • Une modification n'atteint pas les variables d'environnement ni les montages subPath ; elle atteint les fichiers montés après un délai, par un échange atomique du lien ..data. Les pods se recréent avec kubectl rollout restart, ou l'on adopte des objets immuables à nom versionné.
  • Un Secret kubernetes.io/dockerconfigjson, référencé par imagePullSecrets, permet au kubelet de tirer une image d'un registre privé.
  • 1 Mio au plus par objet ; une clé ou un objet manquant bloque le conteneur en CreateContainerConfigError (ou le pod en ContainerCreating pour un volume).
  • En production, les valeurs des Secrets viennent d'un gestionnaire de secrets (External Secrets) ou sont chiffrées pour Git (Sealed Secrets), jamais commitées en clair.

Pour aller plus loin

  • La page Good practices for Kubernetes Secrets de la documentation, courte et dense, pour les administrateurs comme pour les développeurs.
  • La tâche Encrypting Confidential Data at Rest, pour voir le chiffrement d'etcd de l'intérieur sur un cluster que vous administrez.
  • Le cours GitOps avec Argo CD, leçon 8, pour les secrets dans une chaîne de déploiement GitOps.
  • La leçon suivante, qui donne à Signalements des ressources dimensionnées et une place sur les nœuds.
Voir ma constellation →

Sources