12 — Configuration et secrets#
Le problème de la configuration dans les conteneurs#
Dans le monde des conteneurs, une règle d’or s’applique : l’image est immuable. On ne doit jamais cuire la configuration directement dans l’image Docker — cela forcerait à reconstruire l’image pour changer une URL de base de données ou un niveau de log.
Analogie : le livre de recettes vs les ingrédients
Une image Docker, c’est comme une recette de cuisine écrite dans un livre. La recette ne change pas. En revanche, on peut varier les ingrédients (la configuration) à chaque préparation sans réécrire le livre. C’est exactement ce que font ConfigMap et Secret : ils injectent les « ingrédients » dans le « plat » (le Pod) au moment de la cuisson.
Kubernetes propose deux objets pour cela :
ConfigMap : données de configuration non-sensibles (URLs, paramètres, fichiers de config)
Secret : données sensibles (mots de passe, clés API, certificats)
ConfigMap#
Un ConfigMap stocke des paires clé-valeur ou des fichiers de configuration complets.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# Paires clé-valeur simples
APP_ENV: "production"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
DB_HOST: "postgres.production.svc.cluster.local"
# Fichier de configuration complet
app.properties: |
server.port=8080
server.timeout=30s
cache.ttl=300
feature.new-ui=true
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://backend:8080;
}
}
# Créer un ConfigMap depuis un fichier
kubectl create configmap app-config --from-file=app.properties
# Créer depuis des valeurs littérales
kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info
# Inspecter un ConfigMap
kubectl describe configmap app-config -n production
# Modifier en direct (déconseillé en prod, utiliser GitOps)
kubectl edit configmap app-config -n production
Secret#
Un Secret fonctionne comme un ConfigMap, mais pour les données sensibles. La différence principale : les valeurs sont encodées en base64.
Base64 ≠ chiffrement !
L’encodage base64 n’est PAS du chiffrement. C’est uniquement un encodage binaire-texte qui permet de stocker des données binaires (certificats, clés) dans un format YAML. N’importe qui avec accès à l’objet Secret peut décoder les valeurs en une commande. La vraie sécurité vient du RBAC et du chiffrement au repos (EncryptionConfiguration).
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
data:
# Valeurs encodées en base64 (PAS chiffrées !)
username: cG9zdGdyZXM= # base64("postgres")
password: czNjcjN0UGFzcw== # base64("s3cr3tPass")
api-key: YWJjMTIzZGVmNDU2 # base64("abc123def456")
Les types de Secrets#
Kubernetes définit plusieurs types de Secrets selon leur usage :
Encodage et décodage base64#
import base64
# Encodage : c'est ce qu'on met dans le manifeste YAML
def encode_secret(value: str) -> str:
"""Encode une valeur pour un Secret Kubernetes."""
return base64.b64encode(value.encode('utf-8')).decode('utf-8')
# Décodage : c'est ce que voit l'application dans le Pod
def decode_secret(encoded: str) -> str:
"""Décode une valeur depuis un Secret Kubernetes."""
return base64.b64decode(encoded.encode('utf-8')).decode('utf-8')
# Exemples pratiques
secrets_to_encode = {
"username": "admin",
"password": "P@ssw0rd!Super_Secure",
"api_key": "sk-prod-abc123def456ghi789",
"db_url": "postgresql://admin:P@ssw0rd!@postgres:5432/mydb",
}
print("Encodage des secrets pour le manifeste YAML :")
print("=" * 60)
encoded_secrets = {}
for key, value in secrets_to_encode.items():
encoded = encode_secret(value)
encoded_secrets[key] = encoded
print(f" {key}:")
print(f" valeur originale : {value}")
print(f" base64 encodé : {encoded}")
print()
# Vérification du décodage (ce que fait K8s quand il monte le Secret)
print("\nDécodage (ce que voit l'application dans le Pod) :")
print("=" * 60)
for key, encoded in encoded_secrets.items():
decoded = decode_secret(encoded)
print(f" {key}: {decoded}")
# Démonstration que base64 n'est pas du chiffrement
print("\n⚠️ Démonstration : base64 n'est PAS du chiffrement")
print("=" * 60)
sample = "P@ssw0rd!Super_Secure"
encoded = encode_secret(sample)
print(f" Encodé : {encoded}")
print(f" Décodé : {decode_secret(encoded)}")
print(f" → En une commande shell : echo '{encoded}' | base64 -d")
print(f" → Résultat : {sample}")
print("\n CONCLUSION : n'importe qui avec accès à l'objet Secret peut lire les valeurs.")
print(" La protection vient du RBAC Kubernetes, PAS de l'encodage base64.")
Encodage des secrets pour le manifeste YAML :
============================================================
username:
valeur originale : admin
base64 encodé : YWRtaW4=
password:
valeur originale : P@ssw0rd!Super_Secure
base64 encodé : UEBzc3cwcmQhU3VwZXJfU2VjdXJl
api_key:
valeur originale : sk-prod-abc123def456ghi789
base64 encodé : c2stcHJvZC1hYmMxMjNkZWY0NTZnaGk3ODk=
db_url:
valeur originale : postgresql://admin:P@ssw0rd!@postgres:5432/mydb
base64 encodé : cG9zdGdyZXNxbDovL2FkbWluOlBAc3N3MHJkIUBwb3N0Z3Jlczo1NDMyL215ZGI=
Décodage (ce que voit l'application dans le Pod) :
============================================================
username: admin
password: P@ssw0rd!Super_Secure
api_key: sk-prod-abc123def456ghi789
db_url: postgresql://admin:P@ssw0rd!@postgres:5432/mydb
⚠️ Démonstration : base64 n'est PAS du chiffrement
============================================================
Encodé : UEBzc3cwcmQhU3VwZXJfU2VjdXJl
Décodé : P@ssw0rd!Super_Secure
→ En une commande shell : echo 'UEBzc3cwcmQhU3VwZXJfU2VjdXJl' | base64 -d
→ Résultat : P@ssw0rd!Super_Secure
CONCLUSION : n'importe qui avec accès à l'objet Secret peut lire les valeurs.
La protection vient du RBAC Kubernetes, PAS de l'encodage base64.
Parsing d’un manifeste Secret YAML#
import json
import base64
# Simulation d'un Secret Kubernetes (comme retourné par kubectl get secret -o json)
secret_json = {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "db-credentials",
"namespace": "production",
"creationTimestamp": "2026-03-15T10:30:00Z",
"labels": {"app": "mon-app", "env": "production"},
},
"type": "Opaque",
"data": {
"username": base64.b64encode(b"postgres").decode(),
"password": base64.b64encode(b"s3cr3t_DB_Pass!").decode(),
"host": base64.b64encode(b"postgres.production.svc.cluster.local").decode(),
"port": base64.b64encode(b"5432").decode(),
}
}
def parse_and_decode_secret(secret: dict) -> dict:
"""
Parse un Secret Kubernetes et décode toutes les valeurs.
Utile pour l'audit et le debug (avec les bonnes permissions).
"""
result = {
"name": secret["metadata"]["name"],
"namespace": secret["metadata"]["namespace"],
"type": secret["type"],
"labels": secret["metadata"].get("labels", {}),
"values": {}
}
for key, encoded_value in secret.get("data", {}).items():
try:
decoded = base64.b64decode(encoded_value).decode('utf-8')
# Masquage partiel des valeurs sensibles pour l'affichage
if len(decoded) > 4:
masked = decoded[:2] + "*" * (len(decoded) - 4) + decoded[-2:]
else:
masked = "****"
result["values"][key] = {
"encoded": encoded_value,
"decoded_masked": masked,
"length": len(decoded)
}
except Exception as e:
result["values"][key] = {"error": str(e)}
return result
parsed = parse_and_decode_secret(secret_json)
print("Analyse du Secret 'db-credentials' :")
print("=" * 55)
print(f"Nom : {parsed['name']}")
print(f"Namespace : {parsed['namespace']}")
print(f"Type : {parsed['type']}")
print(f"Labels : {parsed['labels']}")
print("\nValeurs (partiellement masquées) :")
for key, info in parsed["values"].items():
if "error" not in info:
print(f" {key:12s} : {info['decoded_masked']:<35} (longueur: {info['length']})")
Analyse du Secret 'db-credentials' :
=======================================================
Nom : db-credentials
Namespace : production
Type : Opaque
Labels : {'app': 'mon-app', 'env': 'production'}
Valeurs (partiellement masquées) :
username : po****es (longueur: 8)
password : s3***********s! (longueur: 15)
host : po*********************************al (longueur: 37)
port : **** (longueur: 4)
Deux façons d’injecter ConfigMap et Secret dans un Pod#
Manifestes d’injection dans la pratique#
# Méthode 1 : envFrom (import en masse)
apiVersion: v1
kind: Pod
metadata:
name: mon-app
spec:
containers:
- name: app
image: mon-app:1.0
envFrom:
- configMapRef:
name: app-config # Importe TOUTES les clés comme env vars
- secretRef:
name: db-credentials # Idem pour le Secret
---
# Méthode 2 : env individuel (import sélectif)
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: db_url # Une seule clé du Secret
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL # Une seule clé du ConfigMap
---
# Méthode 3 : montage en volume
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
- name: secrets-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
- name: secrets-volume
secret:
secretName: db-credentials
defaultMode: 0400 # Lecture seule pour l'owner uniquement
Projected Volumes : combiner plusieurs sources#
Les Projected Volumes permettent de combiner plusieurs sources de données dans un seul répertoire monté.
volumes:
- name: combined-config
projected:
sources:
- configMap:
name: app-config
- secret:
name: db-credentials
- serviceAccountToken:
path: token
expirationSeconds: 3600 # Token renouvelé toutes les heures
audience: vault # Pour s'authentifier auprès de Vault
- downwardAPI:
items:
- path: pod-name
fieldRef:
fieldPath: metadata.name
- path: pod-namespace
fieldRef:
fieldPath: metadata.namespace
- path: cpu-limit
resourceFieldRef:
containerName: app
resource: limits.cpu
Downward API : les métadonnées du Pod dans le conteneur#
La Downward API permet à un conteneur de connaître ses propres métadonnées sans appeler l’API Kubernetes.
Chiffrement au repos : EncryptionConfiguration#
Par défaut, les Secrets sont stockés en clair dans etcd (la base de données de Kubernetes). kubectl get secret -o yaml montre les valeurs encodées en base64, mais un accès direct à etcd les révèle en clair.
# EncryptionConfiguration : active le chiffrement des Secrets dans etcd
# À configurer sur l'API Server (--encryption-provider-config)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc: # Chiffrement AES-CBC
keys:
- name: key1
secret: <clé-base64-32-octets>
- identity: {} # Fallback : pas de chiffrement
Pour aller plus loin, les KMS providers (Key Management Service) délèguent la gestion des clés à un système externe (AWS KMS, GCP Cloud KMS, HashiCorp Vault).
Gestion des secrets en production#
En production, stocker des secrets dans des objets Kubernetes natifs présente des limites. Trois approches complémentaires existent :
Simulation de rotation de secret#
La rotation de secret consiste à remplacer périodiquement un mot de passe ou une clé pour limiter l’impact d’une compromission.
import hashlib
import base64
import time
import json
from datetime import datetime, timedelta
class SecretRotationSimulator:
"""
Simule un système de rotation automatique de secrets,
comme le ferait HashiCorp Vault ou External Secrets Operator.
"""
def __init__(self, secret_name: str, rotation_period_days: int = 30):
self.secret_name = secret_name
self.rotation_period_days = rotation_period_days
self.history = []
self.current_version = 0
self._generate_initial_secret()
def _generate_secret_value(self, version: int) -> str:
"""Génère un secret pseudo-aléatoire basé sur une version."""
seed = f"{self.secret_name}-v{version}-{os.urandom(8).hex()}"
hash_val = hashlib.sha256(seed.encode()).hexdigest()
# Format : 32 caractères alphanum + spéciaux
chars = hash_val[:24] + "!@#$"[version % 4] + hash_val[24:31].upper()
return chars
def _generate_initial_secret(self):
self.current_version = 1
value = self._generate_secret_value(1)
self.history.append({
"version": 1,
"created_at": datetime.now().isoformat(),
"expires_at": (datetime.now() + timedelta(days=self.rotation_period_days)).isoformat(),
"value_encoded": base64.b64encode(value.encode()).decode(),
"fingerprint": hashlib.sha256(value.encode()).hexdigest()[:12],
"status": "active"
})
def rotate(self) -> dict:
"""Effectue une rotation : crée un nouveau secret, marque l'ancien comme 'deprecated'."""
# Marquer l'ancienne version comme dépréciée
if self.history:
self.history[-1]["status"] = "deprecated"
self.current_version += 1
value = self._generate_secret_value(self.current_version)
new_entry = {
"version": self.current_version,
"created_at": datetime.now().isoformat(),
"expires_at": (datetime.now() + timedelta(days=self.rotation_period_days)).isoformat(),
"value_encoded": base64.b64encode(value.encode()).decode(),
"fingerprint": hashlib.sha256(value.encode()).hexdigest()[:12],
"status": "active"
}
self.history.append(new_entry)
return new_entry
def get_current_secret(self) -> str:
"""Retourne la valeur décodée du secret actuel."""
active = next((e for e in self.history if e["status"] == "active"), None)
if not active:
raise ValueError("Aucun secret actif !")
return base64.b64decode(active["value_encoded"]).decode()
def print_history(self):
print(f"\nHistorique de rotation : Secret '{self.secret_name}'")
print(f"Période de rotation : {self.rotation_period_days} jours")
print("=" * 75)
print(f"{'Version':<10} {'Statut':<15} {'Fingerprint':<15} {'Créé le':<22} {'Expire le'}")
print("-" * 75)
for entry in self.history:
status_color = "→ ACTIF " if entry["status"] == "active" else " ancien "
created = entry["created_at"][:19]
expires = entry["expires_at"][:19]
print(f" v{entry['version']:<8} {status_color:<15} {entry['fingerprint']:<15} {created:<22} {expires}")
# Simulation
sim = SecretRotationSimulator("db-password", rotation_period_days=30)
print("=== Simulation de rotation de secrets ===\n")
print(f"Secret initial : {sim.get_current_secret()}")
# Simulations de rotations
for i in range(3):
print(f"\n--- Rotation #{i+1} déclenchée (expiration, ou audit de sécurité) ---")
new_secret = sim.rotate()
print(f"Nouveau secret (fingerprint: {new_secret['fingerprint']}) : {sim.get_current_secret()}")
sim.print_history()
print(f"\n✓ La valeur actuelle du secret sera injectée dans les nouveaux Pods.")
print(f" Les Pods existants continuent avec l'ancien secret jusqu'à leur prochain redémarrage.")
print(f" (Sauf si montage en volume : mise à jour automatique en ~60s)")
=== Simulation de rotation de secrets ===
Secret initial : ba67ec3d9e8605b3d79725f1@C8B2410
--- Rotation #1 déclenchée (expiration, ou audit de sécurité) ---
Nouveau secret (fingerprint: 42fffe29e060) : 8ef8774dd416314fcb307fdc#870CB95
--- Rotation #2 déclenchée (expiration, ou audit de sécurité) ---
Nouveau secret (fingerprint: 630f955f3e2e) : 81c2eff4dbfcb73f6b038204$D3CCD9F
--- Rotation #3 déclenchée (expiration, ou audit de sécurité) ---
Nouveau secret (fingerprint: 729f8ea3402f) : 3f4bcec2eb1841571dc34c11!6DCFC7A
Historique de rotation : Secret 'db-password'
Période de rotation : 30 jours
===========================================================================
Version Statut Fingerprint Créé le Expire le
---------------------------------------------------------------------------
v1 ancien 63a91586bd87 2026-03-21T19:54:31 2026-04-20T19:54:31
v2 ancien 42fffe29e060 2026-03-21T19:54:31 2026-04-20T19:54:31
v3 ancien 630f955f3e2e 2026-03-21T19:54:31 2026-04-20T19:54:31
v4 → ACTIF 729f8ea3402f 2026-03-21T19:54:31 2026-04-20T19:54:31
✓ La valeur actuelle du secret sera injectée dans les nouveaux Pods.
Les Pods existants continuent avec l'ancien secret jusqu'à leur prochain redémarrage.
(Sauf si montage en volume : mise à jour automatique en ~60s)
Bonnes pratiques#
Récapitulatif#
Dans ce chapitre, nous avons exploré les deux piliers de la configuration dans Kubernetes :
ConfigMap pour les données non-sensibles, injectable en volume ou en variables d’environnement
Secret pour les données sensibles, dont la vraie protection vient du RBAC et du chiffrement au repos
Nous avons vu que base64 n’est pas du chiffrement, simulé la rotation automatique de secrets, et présenté les trois principales solutions de gestion des secrets en production (Vault, Sealed Secrets, External Secrets Operator).
Le prochain chapitre aborde une question tout aussi fondamentale : comment persister les données au-delà du cycle de vie d’un Pod ?