0%
ConfigMaps et Secrets

Configuration

Une image, plusieurs environnements — et ce que « Secret » ne veut pas dire.

15-20 min

ConfigMaps et Secrets

Une image de conteneur doit pouvoir être déployée en développement, en préproduction et en production sans être reconstruite. C’est le principe qui justifie toute cette leçon, et il n’est pas théorique : si l’URL de la base de données est inscrite dans l’image, alors l’artefact que vous testez n’est pas celui que vous livrez, et le test ne prouve plus rien.

L’autre moitié du problème est plus désagréable. Un mot de passe placé dans une image y reste définitivement : les couches sont immuables, l’effacer dans une couche ultérieure ne le retire pas de l’historique, et toute personne ayant accès au registre peut le lire. Un secret livré dans une image n’est pas un secret, c’est une fuite avec un délai.

Kubernetes sépare donc deux objets pour deux usages : la ConfigMap porte la configuration non sensible, le Secret porte les données confidentielles. Les deux s’injectent dans les conteneurs de la même façon — variables d’environnement ou fichiers montés — mais leur traitement par le cluster diffère.

Un avertissement à placer immédiatement, avant toute syntaxe, parce que c’est le malentendu le plus répandu de l’écosystème : un Secret Kubernetes n’est pas chiffré par défaut. Il est encodé en base64, ce qui est un format de transport, pas une protection. Nous verrons exactement ce que cela implique, et ce qu’il faut ajouter pour que le mot « secret » soit mérité.

La ConfigMap

Une ConfigMap est un dictionnaire de paires clé-valeur, ou un ensemble de fichiers, stocké dans le cluster.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: atelier
data:
  # Valeurs simples : une clé, une chaîne
  APP_ENV: "production"
  LOG_LEVEL: "info"
  API_TIMEOUT: "30"
  FEATURE_NOUVEAU_PANIER: "true"

  # Fichier entier : la clé devient le nom du fichier si on monte en volume
  application.properties: |
    server.port=8080
    spring.jpa.show-sql=false
    logging.level.root=INFO

  nginx.conf: |
    server {
      listen 80;
      location /api/ {
        proxy_pass http://api:8080/;
      }
    }

Deux détails de syntaxe qui coûtent du temps quand on les ignore. D’abord, toutes les valeurs de data sont des chaînes : API_TIMEOUT: 30 sans guillemets est refusé par le schéma, et true sans guillemets est interprété comme un booléen YAML. Prenez l’habitude de guillemeter systématiquement. Ensuite, le | du YAML conserve les retours à la ligne, ce qui permet d’embarquer un fichier de configuration tel quel.

Les variantes impératives sont pratiques pour de l’exploration :

# Depuis des valeurs littérales
kubectl create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info

# Depuis un fichier : la clé est le nom du fichier
kubectl create configmap nginx-config --from-file=nginx.conf

# Depuis un répertoire entier : une clé par fichier
kubectl create configmap tous-configs --from-file=./config/

# Depuis un fichier .env : une clé par ligne
kubectl create configmap app-config --from-env-file=.env
kubectl get configmaps
kubectl describe configmap app-config     # affiche les valeurs en clair
kubectl get configmap app-config -o yaml

Le Secret

Structurellement identique, avec un champ de plus et un encodage attendu.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: atelier
type: Opaque
stringData:
  # stringData : vous écrivez en clair, Kubernetes encode à l'enregistrement
  DB_USER: "app_user"
  DB_PASSWORD: "S3cr3t-a-changer"
  DATABASE_URL: "postgresql://app_user:S3cr3t-a-changer@postgres:5432/appdb"

stringData est presque toujours le bon choix pour un manifeste écrit à la main : il évite d’encoder soi-même. Le champ data, lui, attend des valeurs déjà en base64 :

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  DB_USER: YXBwX3VzZXI=              # echo -n 'app_user' | base64
  DB_PASSWORD: UzNjcjN0LWEtY2hhbmdlcg==
# Création impérative : l'encodage est fait pour vous
kubectl create secret generic db-credentials \
  --from-literal=DB_USER=app_user \
  --from-literal=DB_PASSWORD='S3cr3t-a-changer'

# Depuis un fichier (certificat, clé privée, fichier de credentials)
kubectl create secret generic tls-keys --from-file=./server.key

# Secret dédié aux registres privés, pour imagePullSecrets
kubectl create secret docker-registry registre-prive \
  --docker-server=registry.example.com \
  --docker-username=deploiement \
  --docker-password='jeton-du-registre'

Les types de Secret les plus utilisés :

typeUsage
Opaquedonnées arbitraires : le cas général
kubernetes.io/dockerconfigjsonauthentification à un registre d’images privé
kubernetes.io/tlscertificat et clé privée, consommés par les Ingress
kubernetes.io/service-account-tokenjeton d’un compte de service

Ce que « Secret » protège réellement

Voici la démonstration à faire une fois pour ne plus jamais l’oublier :

# Le contenu « protégé »
kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# S3cr3t-a-changer

Aucun mot de passe, aucune clé, aucun déchiffrement. Le base64 n’a jamais été un mécanisme de sécurité — il sert à transporter des octets binaires dans un champ texte. Ce que cela implique concrètement :

  • Toute personne autorisée à lire les Secrets d’un namespace lit tous les mots de passe de ce namespace en clair.
  • Un Secret validé dans Git est un secret compromis, même encodé en base64.
  • Par défaut, les Secrets sont stockés en clair dans etcd. Une sauvegarde d’etcd mal protégée expose tout.

Ce qui rend un Secret réellement confidentiel, ce sont trois mesures ajoutées :

  1. Le RBAC. C’est la seule protection qui fonctionne par défaut : restreindre qui peut lire les Secrets. Un Secret n’est confidentiel que si les droits de lecture le sont.
  2. Le chiffrement au repos. Il s’active côté serveur d’API par une EncryptionConfiguration, avec une clé locale ou un service de gestion de clés (KMS). Ce n’est pas activé d’office sur un cluster autogéré ; les offres managées le proposent souvent, à vérifier au cas par cas.
  3. Un gestionnaire externe. HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, avec un opérateur qui synchronise vers le cluster. La rotation devient possible et l’audit existe, ce que Kubernetes seul ne fournit pas.

Par rapport à une variable d’environnement écrite dans un manifeste de Deployment, le Secret apporte tout de même deux gains réels : la valeur n’apparaît pas dans kubectl describe deployment, et elle est administrable séparément du déploiement. C’est un progrès, pas une garantie.

Injecter par variables d’environnement

La méthode la plus directe, clé par clé :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: mon-api:1.0.0
          ports:
            - containerPort: 8080
          env:
            # Une clé de ConfigMap vers une variable, avec renommage possible
            - name: ENVIRONNEMENT
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: APP_ENV

            # Une clé de Secret vers une variable
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: DB_PASSWORD

            # Une valeur littérale, pour ce qui n'est ni partagé ni sensible
            - name: PORT
              value: "8080"

Pour tout importer d’un coup, sans énumérer :

          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: db-credentials

envFrom est concis, mais il a une contrepartie qu’il faut mesurer : les noms de variables deviennent implicites. En lisant le Deployment, on ne sait plus quelles variables l’application reçoit. Pour une application dont vous ne maîtrisez pas le code, l’énumération explicite reste préférable.

Ajoutez enfin optional: true quand une clé peut légitimement manquer, faute de quoi le Pod reste bloqué en CreateContainerConfigError :

            - name: FONCTION_EXPERIMENTALE
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: FEATURE_NOUVEAU_PANIER
                  optional: true

La limite majeure des variables d’environnement

Elle est structurelle : les variables d’environnement sont fixées au démarrage du processus. Modifier une ConfigMap ne change rien dans les Pods qui tournent déjà, et ne déclenche aucun redéploiement. Vous obtenez alors un cluster où la ConfigMap dit une chose et l’application en fait une autre — une incohérence silencieuse, très difficile à repérer.

Deux réponses possibles, selon le besoin.

Forcer un redéploiement à chaque changement, en faisant dépendre le template d’une empreinte de la configuration :

    metadata:
      labels:
        app: api
      annotations:
        # À recalculer à chaque livraison : toute modification du template
        # déclenche une mise à jour progressive des Pods.
        checksum/config: "9f2b7c1e4a"

Ou, plus simplement en exploitation courante :

# Redémarrage progressif du Deployment, sans changer l'image
kubectl rollout restart deployment/api

Injecter par volume

Monter une ConfigMap comme un répertoire donne un fichier par clé. C’est le mode obligatoire pour un fichier de configuration entier, et le seul qui permette une mise à jour sans redémarrage.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
          volumeMounts:
            # Toutes les clés de la ConfigMap deviennent des fichiers ici
            - name: config-volume
              mountPath: /etc/app/config
              readOnly: true
            # Un Secret monté : permissions restrictives, jamais dans un log
            - name: secrets-volume
              mountPath: /etc/app/secrets
              readOnly: true
      volumes:
        - name: config-volume
          configMap:
            name: app-config
            items:
              # Sans « items », toutes les clés sont montées.
              # Avec, on choisit précisément, et on renomme.
              - key: nginx.conf
                path: default.conf
        - name: secrets-volume
          secret:
            secretName: db-credentials
            defaultMode: 0400     # lecture seule pour le propriétaire

Avec ce manifeste, le conteneur voit /etc/app/config/default.conf contenant la valeur de la clé nginx.conf. Le renommage par items est ce qui permet de placer un fichier là où l’application l’attend, sans contorsion.

Pour remplacer un fichier unique dans un répertoire existant sans masquer le reste de son contenu, subPath est la clé :

          volumeMounts:
            - name: config-volume
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf     # remplace UN fichier, pas le répertoire

Sans subPath, monter sur /etc/nginx/conf.d/ masquerait tout le contenu d’origine du répertoire. C’est une cause classique de conteneur qui refuse de démarrer après ajout d’une configuration.

La mise à jour à chaud, avec ses conditions

Les ConfigMaps montées en volume sont mises à jour dans le conteneur qui tourne, sans redémarrage. Trois réserves à connaître :

  • La propagation prend jusqu’à une minute environ, selon la période de synchronisation du kubelet.
  • L’application doit savoir recharger son fichier. nginx a besoin d’un nginx -s reload, une application Spring Boot ne relit pas ses propriétés spontanément. Le fichier change, le comportement pas.
  • Un montage avec subPath n’est jamais mis à jour. C’est le compromis exact de cette option : précision du montage contre perte du rechargement.
# Modifier une ConfigMap en place puis observer le fichier dans le Pod
kubectl edit configmap app-config
kubectl exec -it deploy/web -- cat /etc/app/config/default.conf

Variables d’environnement ou volume : lequel choisir

CritèreVariables d’environnementVolume
Valeurs simples et courtesadaptéverbeux
Fichier de configuration completinadaptéseule option
Mise à jour sans redémarrageimpossiblepossible, avec réserves
Visibilité dans /proc et les logs de crashexposéelimitée aux lecteurs du fichier
Sortie de kubectl describe podnoms des variables visibleschemin du montage seulement

Pour les secrets, le volume est plus sûr, et pour une raison concrète que l’on peut constater soi-même :

# L'environnement du processus principal est lisible dans /proc.
kubectl exec -it deploy/api -- sh -c 'tr "\0" "\n" < /proc/1/environ | grep DB_'
# DB_PASSWORD=S3cr3t-a-changer

Les variables d’environnement se retrouvent aussi dans les traces d’exception, dans les rapports de plantage, et sont héritées par tout processus enfant. Un fichier monté en mode 0400 a une surface d’exposition nettement plus étroite.

Les ConfigMaps immuables

Marquer une ConfigMap ou un Secret comme immuable interdit toute modification ultérieure, et allège au passage la charge de surveillance sur le serveur d’API dans les grands clusters.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v3
immutable: true
data:
  APP_ENV: "production"
  LOG_LEVEL: "warn"

Cela impose une discipline utile : plutôt que de modifier app-config, vous créez app-config-v4 et vous faites pointer le Deployment dessus. Le changement de configuration devient une modification versionnée du Deployment, donc visible dans kubectl rollout history et annulable par kubectl rollout undo. Une ConfigMap modifiée en place, elle, ne laisse aucune trace exploitable.

À retenir

  • Une image doit être indépendante de son environnement. La ConfigMap porte la configuration, le Secret les données sensibles, et les deux s’injectent par variables d’environnement ou par volume.
  • Un Secret n’est pas chiffré : il est encodé en base64, lisible d’un base64 -d, et stocké en clair dans etcd par défaut. Ce qui le protège vraiment, c’est le RBAC, le chiffrement au repos configuré côté serveur d’API, et un gestionnaire externe.
  • Les variables d’environnement sont figées au démarrage : modifier une ConfigMap ne change rien aux Pods en cours. Forcez la bascule par une annotation de checksum ou un kubectl rollout restart.
  • Les volumes sont propagés à chaud (à une minute près), à condition que l’application recharge son fichier — et jamais avec subPath, qui gèle le montage.
  • Préférez le volume pour les secrets : les variables d’environnement se retrouvent dans /proc, dans les traces d’erreur et chez tous les processus enfants.

Conseil : versionnez vos ConfigMaps (app-config-v3) et rendez-les immuables. Un changement de configuration devient alors une révision du Deployment, donc traçable et réversible comme n’importe quel déploiement.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !