12 — Configuration et secrets#

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
import base64
import json
import hashlib
import os
sns.set_theme(style="whitegrid", palette="muted")

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 :

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 6))
ax.set_xlim(0, 13)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Types de Secrets Kubernetes", fontsize=14, fontweight='bold')

types = [
    {
        "type": "Opaque",
        "color": "#8E44AD",
        "desc": "Type par défaut, données arbitraires\n(mots de passe, clés API, tokens)",
        "example": "username, password, api-key",
        "x": 1.8
    },
    {
        "type": "kubernetes.io/tls",
        "color": "#27AE60",
        "desc": "Certificat TLS et clé privée\n(utilisé par Ingress pour HTTPS)",
        "example": "tls.crt, tls.key",
        "x": 5.0
    },
    {
        "type": "kubernetes.io/dockerconfigjson",
        "color": "#E67E22",
        "desc": "Credentials pour registry Docker\n(pull d'images privées)",
        "example": ".dockerconfigjson",
        "x": 8.5
    },
    {
        "type": "kubernetes.io/service-account-token",
        "color": "#4A90D9",
        "desc": "Token JWT pour ServiceAccount\n(auth à l'API server)",
        "example": "token, ca.crt, namespace",
        "x": 11.8
    },
]

for t in types:
    x = t["x"]
    color = t["color"]
    # Boîte principale
    ax.add_patch(FancyBboxPatch((x - 1.6, 2.5), 3.2, 4.1, boxstyle="round,pad=0.15",
                                 facecolor=color, alpha=0.1, edgecolor=color, lw=2))
    # En-tête
    ax.add_patch(FancyBboxPatch((x - 1.6, 5.5), 3.2, 1.0, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.85, edgecolor='none'))
    # Nom du type (police réduite pour les noms longs)
    fname = t["type"]
    fsize = 8 if len(fname) > 20 else 9.5
    ax.text(x, 5.85, fname, ha='center', va='center', fontsize=fsize,
            fontweight='bold', color='white', family='monospace')

    ax.text(x, 4.8, t["desc"], ha='center', va='center', fontsize=8.5,
            color="#333", multialignment='center')

    ax.add_patch(FancyBboxPatch((x - 1.4, 2.7), 2.8, 0.9, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.15, edgecolor=color, lw=0.5))
    ax.text(x, 3.15, "Clés attendues :", ha='center', fontsize=8, color=color,
            fontweight='bold')
    ax.text(x, 2.9, t["example"], ha='center', fontsize=8, color="#333",
            family='monospace')

ax.text(6.5, 1.8,
    "Tous les types partagent le même mécanisme de stockage — seules les clés attendues et les validations diffèrent.",
    ha='center', fontsize=9, color="#666", style='italic')

plt.tight_layout()
plt.savefig("12_types_secrets.png", dpi=120, bbox_inches='tight')
plt.show()
_images/ebc9d7f95c78e11ce1affa8fb75eca6562515483eb8f50af4a64637ee30206b5.png

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#

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(15, 8))
fig.suptitle("Injection de ConfigMap/Secret dans un Pod : Volume vs Variables d'environnement",
             fontsize=13, fontweight='bold')

def draw_pod_injection(ax, title, color, method_desc, pros, cons, code_example, right_label):
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 12)
    ax.axis('off')
    ax.set_title(title, fontsize=11, fontweight='bold', color=color, pad=10)

    # Source (ConfigMap/Secret)
    ax.add_patch(FancyBboxPatch((0.5, 10.0), 4, 1.5, boxstyle="round,pad=0.15",
                                 facecolor="#8E44AD", alpha=0.85, edgecolor='none'))
    ax.text(2.5, 10.75, "Secret / ConfigMap", ha='center', va='center',
            fontsize=10, fontweight='bold', color='white')

    ax.add_patch(FancyBboxPatch((5.5, 10.0), 3.8, 1.5, boxstyle="round,pad=0.15",
                                 facecolor="#7F8C8D", alpha=0.85, edgecolor='none'))
    ax.text(7.4, 10.75, right_label, ha='center', va='center',
            fontsize=9, fontweight='bold', color='white')

    ax.annotate("", xy=(4.8, 8.8), xytext=(2.5, 10.0),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=2.5))
    ax.text(3.3, 9.3, method_desc, ha='center', fontsize=9, color=color, fontweight='bold')

    # Pod
    ax.add_patch(FancyBboxPatch((2.5, 5.8), 5.0, 2.8, boxstyle="round,pad=0.2",
                                 facecolor=color, alpha=0.12, edgecolor=color, lw=2))
    ax.text(5.0, 8.35, "Pod", ha='center', fontsize=10, fontweight='bold', color=color)
    ax.add_patch(FancyBboxPatch((2.9, 6.0), 4.2, 2.2, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.2, edgecolor=color, lw=1))
    ax.text(5.0, 7.05, code_example, ha='center', va='center', fontsize=8,
            color="#333", family='monospace')

    # Avantages
    ax.text(0.5, 5.2, "Avantages :", fontsize=9, fontweight='bold', color="#27AE60")
    for i, pro in enumerate(pros):
        ax.text(0.7, 4.7 - i * 0.55, f"+ {pro}", fontsize=8.5, color="#27AE60")

    # Inconvénients
    ax.text(0.5, 2.9, "Inconvénients :", fontsize=9, fontweight='bold', color="#E74C3C")
    for i, con in enumerate(cons):
        ax.text(0.7, 2.4 - i * 0.55, f"- {con}", fontsize=8.5, color="#E74C3C")

draw_pod_injection(
    axes[0],
    "Méthode 1 : Montage en Volume",
    "#4A90D9",
    "→ Fichiers dans /etc/config",
    pros=["Mise à jour sans redémarrage", "Fichiers lisibles par path", "Idéal pour fichiers de conf"],
    cons=["Chemin à gérer dans l'app", "Latence de propagation ~60s"],
    code_example="/etc/config/\n  ├── database.url\n  ├── app.properties\n  └── tls.crt",
    right_label="Volume\n(tmpfs)"
)

draw_pod_injection(
    axes[1],
    "Méthode 2 : Variables d'environnement",
    "#E67E22",
    "→ Env vars au démarrage",
    pros=["Simple, universel", "Toutes les apps le supportent", "Pas de gestion de path"],
    cons=["Fixées au démarrage du Pod", "Visibles dans /proc/environ", "Pas de mise à jour à chaud"],
    code_example="$DB_HOST = postgres:5432\n$DB_PASSWORD = s3cr3t\n$APP_ENV = production",
    right_label="Env vars\n(processus)"
)

plt.tight_layout()
plt.savefig("12_injection_methodes.png", dpi=120, bbox_inches='tight')
plt.show()
_images/f684ab0bf161b973bf99b7aeae5fdcac1dfd643ad5ff70485f286d88101b5929.png

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.

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 6))
ax.set_xlim(0, 13)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Downward API : métadonnées du Pod accessibles dans le conteneur",
             fontsize=13, fontweight='bold')

# Pod
ax.add_patch(FancyBboxPatch((4.5, 0.5), 8, 6.2, boxstyle="round,pad=0.2",
                             facecolor="#4A90D9", alpha=0.08, edgecolor="#4A90D9", lw=2))
ax.text(8.5, 6.3, "Pod", fontsize=11, fontweight='bold', color="#4A90D9", ha='center')

# Conteneur
ax.add_patch(FancyBboxPatch((5.5, 1.2), 6.2, 4.5, boxstyle="round,pad=0.1",
                             facecolor="#27AE60", alpha=0.08, edgecolor="#27AE60", lw=1.5))
ax.text(8.6, 5.45, "Conteneur", fontsize=10, color="#27AE60", ha='center', fontweight='bold')

# Fichiers montés
items = [
    ("/etc/podinfo/pod-name", "metadata.name", "mon-app-5d4f7b-x9j2k"),
    ("/etc/podinfo/namespace", "metadata.namespace", "production"),
    ("/etc/podinfo/labels", "metadata.labels", "app=mon-app\\nenv=prod"),
    ("/etc/podinfo/cpu-request", "requests.cpu", "250m"),
    ("/etc/podinfo/mem-limit", "limits.memory", "512Mi"),
    ("/etc/podinfo/node-name", "spec.nodeName", "worker-node-3"),
]

for i, (path, field, value) in enumerate(items):
    y = 4.8 - i * 0.55
    ax.text(5.7, y, path, fontsize=7.5, color="#27AE60", family='monospace')
    ax.text(9.0, y, f"← {field}", fontsize=7.5, color="#888")
    ax.text(11.2, y, f'"{value}"', fontsize=7.5, color="#E67E22", family='monospace')

# API Server
ax.add_patch(FancyBboxPatch((0.3, 2.5), 3.2, 2, boxstyle="round,pad=0.15",
                             facecolor="#E74C3C", alpha=0.85, edgecolor='none'))
ax.text(1.9, 3.5, "API Server\nKubernetes", ha='center', va='center',
        fontsize=10, fontweight='bold', color='white')

ax.annotate("", xy=(4.5, 4.5), xytext=(3.5, 3.8),
            arrowprops=dict(arrowstyle="-|>", color="#4A90D9", lw=2))
ax.text(3.8, 4.4, "Downward\nAPI", fontsize=8.5, color="#4A90D9", ha='center')

ax.text(1.9, 1.8, "Kubelet injecte les\nmétadonnées directement\n(pas d'appel API requis)",
        ha='center', fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#EBF5FB", edgecolor="#4A90D9", lw=1))

plt.tight_layout()
plt.savefig("12_downward_api.png", dpi=120, bbox_inches='tight')
plt.show()
_images/b63d35892cd40675172e84a660699c2082af52f46eba5b1f66967f38210886c6.png

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 :

Hide code cell source

fig, axes = plt.subplots(1, 3, figsize=(15, 8))
fig.suptitle("Gestion des secrets en production", fontsize=14, fontweight='bold')

solutions = [
    {
        "name": "HashiCorp Vault",
        "color": "#E67E22",
        "icon": "🔐",
        "steps": [
            "1. Le Pod démarre avec un\nServiceAccount",
            "2. Le sidecar Vault Agent\ndemande un token à Vault",
            "3. Vault vérifie l'identité K8s\nvia l'API server",
            "4. Vault renvoie les secrets\n(renouvelés automatiquement)",
            "5. Le sidecar écrit les secrets\ndans un volume tmpfs partagé",
        ],
        "avantage": "Rotation automatique,\naudit complet, multi-cloud",
        "complexite": "Haute"
    },
    {
        "name": "Sealed Secrets\n(Bitnami)",
        "color": "#4A90D9",
        "icon": "🔒",
        "steps": [
            "1. Le dev chiffre le secret\navec kubeseal (clé publique)",
            "2. Le SealedSecret chiffré\nest commitable dans Git",
            "3. Le controller Sealed Secrets\ntourne dans le cluster",
            "4. Il déchiffre avec la clé\nprivée stockée dans le cluster",
            "5. Crée l'objet Secret K8s\nnormal dans le namespace",
        ],
        "avantage": "GitOps compatible,\nsimple à adopter",
        "complexite": "Faible"
    },
    {
        "name": "External Secrets\nOperator",
        "color": "#27AE60",
        "icon": "🌐",
        "steps": [
            "1. Les secrets sont stockés\ndans AWS SM / GCP SM / Vault",
            "2. Un ExternalSecret K8s\ndécrit ce qu'on veut",
            "3. L'opérateur ESO se connecte\nau provider externe",
            "4. Il synchronise les secrets\nvers des objets K8s natifs",
            "5. Les Pods consomment des\nSecrets K8s normaux",
        ],
        "avantage": "Intégration cloud native,\nrotation automatique",
        "complexite": "Moyenne"
    }
]

for ax, sol in zip(axes, solutions):
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 12)
    ax.axis('off')
    color = sol["color"]

    # En-tête
    ax.add_patch(FancyBboxPatch((0.3, 10.5), 9.4, 1.3, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.9, edgecolor='none'))
    ax.text(5, 11.15, sol["name"], ha='center', va='center', fontsize=12,
            fontweight='bold', color='white')

    # Étapes
    for i, step in enumerate(sol["steps"]):
        y = 9.5 - i * 1.5
        # Numéro de l'étape
        circ = plt.Circle((1.1, y), 0.35, color=color, alpha=0.8)
        ax.add_patch(circ)
        ax.text(1.1, y, str(i+1), ha='center', va='center', fontsize=10,
                fontweight='bold', color='white')
        ax.text(2.0, y, step, fontsize=8.5, va='center', color="#333")
        if i < len(sol["steps"]) - 1:
            ax.annotate("", xy=(1.1, y - 0.6), xytext=(1.1, y - 0.35),
                        arrowprops=dict(arrowstyle="-|>", color=color, lw=1.5, alpha=0.5))

    # Avantage et complexité
    ax.add_patch(FancyBboxPatch((0.3, 0.3), 9.4, 1.4, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.1, edgecolor=color, lw=1))
    ax.text(5, 1.35, f"Avantage : {sol['avantage']}", ha='center', fontsize=8.5, color=color)
    ax.text(5, 0.75, f"Complexité : {sol['complexite']}", ha='center', fontsize=9,
            fontweight='bold', color="#555")

plt.tight_layout()
plt.savefig("12_secrets_production.png", dpi=120, bbox_inches='tight')
plt.show()
_images/daed388e17865fce3bd3d65558d4a1e49cb79e65238c7bd68b54b521ca538557.png

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#

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title("Bonnes pratiques : ConfigMap et Secrets", fontsize=14, fontweight='bold')

practices = [
    {
        "category": "Ne JAMAIS faire",
        "color": "#E74C3C",
        "items": [
            "Committer des secrets en clair dans Git",
            "Passer des secrets en argument de commande (visibles dans ps)",
            "Stocker des secrets dans des variables d'environnement de l'image",
            "Donner des droits 'get secrets' trop larges via RBAC",
            "Utiliser le namespace 'default' pour les apps de production",
        ]
    },
    {
        "category": "Toujours faire",
        "color": "#27AE60",
        "items": [
            "RBAC minimal sur les Secrets (qui peut lire quoi ?)",
            "Activer EncryptionConfiguration pour chiffrer etcd",
            "Rotation régulière des secrets (30-90 jours selon criticité)",
            "Auditer les accès aux Secrets (audit logging API server)",
            "Utiliser Vault, Sealed Secrets ou ESO en production",
        ]
    }
]

for col_idx, practice in enumerate(practices):
    x_start = 0.3 + col_idx * 6.5
    color = practice["color"]

    ax.add_patch(FancyBboxPatch((x_start, 5.8), 6.0, 1.8, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.85, edgecolor='none'))
    ax.text(x_start + 3.0, 6.7, practice["category"], ha='center', va='center',
            fontsize=12, fontweight='bold', color='white')

    for i, item in enumerate(practice["items"]):
        y = 5.1 - i * 0.85
        symbol = "✗" if "JAMAIS" in practice["category"] else "✓"
        ax.text(x_start + 0.3, y, symbol, fontsize=12, color=color, va='center')
        ax.text(x_start + 0.8, y, item, fontsize=9, color="#333", va='center')

ax.text(6.5, 0.5,
    "Règle d'or : un secret mal géré est pire qu'un secret inexistant — il donne une fausse impression de sécurité.",
    ha='center', fontsize=9.5, color="#555", style='italic',
    bbox=dict(boxstyle="round,pad=0.4", facecolor="#FEF9E7", edgecolor="#E67E22", lw=1.5))

plt.tight_layout()
plt.savefig("12_bonnes_pratiques.png", dpi=120, bbox_inches='tight')
plt.show()
_images/9d03229ecc5a541f92c9067bfabedfd0dd86784aec2221a32467f23330645d4a.png

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 ?