Sécurité Kubernetes#

Kubernetes est un système distribué complexe avec de nombreuses surfaces d’attaque. Une configuration par défaut est rarement sécurisée. Ce chapitre couvre les mécanismes de sécurité essentiels : RBAC, Pod Security, NetworkPolicy, gestion des secrets et sécurité des images.

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 random

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.titlesize": 13,
    "axes.labelsize": 11,
})
random.seed(42)

Le modèle de menaces Kubernetes#

Avant de parler de solutions, il faut comprendre les surfaces d’attaque de Kubernetes.

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Surfaces d'attaque Kubernetes — Modèle de menaces",
             fontsize=13, fontweight="bold", pad=12)

surfaces = [
    # (x, y, titre, risques, couleur)
    (2.5, 6.2, "API Server", "Authentification faible\nRBAC mal configuré\nKubeconfig exposé",
     "#ffcdd2", "#c62828"),
    (7.0, 6.2, "etcd", "Données non chiffrées\nAccès direct sans auth\nSauvegarde exposée",
     "#ffe0b2", "#e65100"),
    (11.5, 6.2, "Images", "CVE dans l'image de base\nLayer avec secrets\nRegistre non sécurisé",
     "#fff9c4", "#f9a825"),
    (2.5, 3.0, "Pods / Conteneurs", "root dans le conteneur\nCapabilities excessives\nFS en écriture",
     "#fce4ec", "#e91e63"),
    (7.0, 3.0, "Réseau", "Pod-to-pod sans restriction\nEgress non filtré\nIngress ouvert",
     "#e8eaf6", "#3949ab"),
    (11.5, 3.0, "Secrets", "Base64 ≠ chiffrement\nSecrets dans les env vars\nEnv vars dans les logs",
     "#e8f5e9", "#2e7d32"),
]

for x, y, titre, risques, fc, ec in surfaces:
    b = FancyBboxPatch((x - 2.1, y - 1.2), 4.2, 2.4, boxstyle="round,pad=0.15",
                        facecolor=fc, edgecolor=ec, linewidth=2)
    ax.add_patch(b)
    ax.text(x, y + 0.8, titre, ha="center", va="center",
            fontsize=10, fontweight="bold", color=ec)
    ax.text(x, y - 0.05, risques, ha="center", va="center",
            fontsize=8, color="#37474f", linespacing=1.5)

# Flèches inter-surfaces (vecteurs d'attaque)
attaques = [
    ((4.6, 6.2), (4.9, 6.2), "#f44336"),
    ((9.1, 6.2), (9.4, 6.2), "#f44336"),
    ((2.5, 5.0), (2.5, 4.2), "#e91e63"),
    ((7.0, 5.0), (7.0, 4.2), "#3949ab"),
    ((11.5, 5.0), (11.5, 4.2), "#f9a825"),
    ((4.6, 3.0), (4.9, 3.0), "#9c27b0"),
    ((9.1, 3.0), (9.4, 3.0), "#9c27b0"),
]
for (x1, y1), (x2, y2), color in attaques:
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.5))

# Mécanismes de défense
defenses = [
    (2.5, 1.0, "→ RBAC + Audit"),
    (5.8, 0.5, "→ Chiffrement etcd\n   Backup sécurisé"),
    (9.5, 1.0, "→ Scanning Trivy\n   Admission controllers"),
]
for x, y, txt in defenses:
    ax.text(x, y, txt, ha="center", va="center", fontsize=8.5, color="#1b5e20",
            bbox=dict(boxstyle="round,pad=0.2", facecolor="#e8f5e9", edgecolor="#2e7d32"))

ax.text(7, 0.2, "Principe : Défense en profondeur — chaque couche ajoute une protection",
        ha="center", va="center", fontsize=9, color="#546e7a", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/15_threat_model.png", dpi=130, bbox_inches="tight")
plt.show()
_images/2bf0f1045b92068a54190806b6e4279f6dbb9e57c6a733cc3688a01aa8f55ec8.png

RBAC : contrôle d’accès basé sur les rôles#

RBAC (Role-Based Access Control) est le système d’autorisation de Kubernetes. Il répond à la question : qui peut faire quoi sur quels objets ?

Les quatre ressources RBAC#

Ressource

Portée

Description

Role

Namespace

Permissions dans un namespace spécifique

ClusterRole

Cluster entier

Permissions sur toutes les ressources (ou ressources non-namespacées)

RoleBinding

Namespace

Associe un Role (ou ClusterRole) à un sujet

ClusterRoleBinding

Cluster entier

Associe un ClusterRole à un sujet pour tout le cluster

ServiceAccount : l’identité des Pods#

# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: mon-application
  namespace: production
automountServiceAccountToken: false   # Désactiver l'injection automatique du token
# role-minimal.yaml — Principe du moindre privilège
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: lecteur-pods
  namespace: production
rules:
  # Chaque règle spécifie : groupes d'API, ressources, verbes autorisés
  - apiGroups: [""]           # "" = Core API group
    resources: ["pods"]
    verbs: ["get", "list", "watch"]   # Lecture seule
    # NE PAS mettre "create", "delete", "patch" si non nécessaire !

  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]

  # Pas de droit sur les secrets, configmaps, services — non nécessaire
# rolebinding.yaml — Attacher le Role au ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mon-app-lecteur
  namespace: production
subjects:
  - kind: ServiceAccount
    name: mon-application
    namespace: production
roleRef:
  kind: Role
  name: lecteur-pods
  apiGroup: rbac.authorization.k8s.io

ClusterRole vs Role — quand utiliser lequel ?

  • Role : pour les permissions limitées à un namespace (application, CI/CD limité à un namespace)

  • ClusterRole : pour les ressources non-namespacées (Nodes, PersistentVolumes, Namespaces) ou pour les outils qui doivent accéder à tout le cluster (monitoring, backup)

Un ClusterRole peut être utilisé dans un RoleBinding pour limiter sa portée à un namespace — c’est souvent la meilleure pratique : définir le ClusterRole une fois, le lier à des namespaces spécifiques.

Hide code cell source

# Matrice RBAC — qui peut faire quoi
fig, ax = plt.subplots(figsize=(14, 6))
ax.axis("off")
ax.set_title("Matrice RBAC — exemple d'une organisation multi-équipes",
             fontsize=12, fontweight="bold", pad=10)

# Sujets (lignes)
sujets = [
    "SA: mon-application",
    "SA: monitoring-agent",
    "SA: ci-deployer",
    "User: dev-alice",
    "User: ops-bob",
    "User: admin-claire",
]

# Permissions (colonnes)
permissions = [
    "pods\nget/list",
    "pods\ncreate/del",
    "deployments\nget",
    "deployments\nupdate",
    "secrets\nget",
    "nodes\nget/list",
    "namespaces\ncreate",
    "*\n(tout)",
]

# Matrice : True = autorisé, False = refusé, "NS" = limité au namespace
matrice = [
    # mon-application
    [True,  False, False, False, False, False, False, False],
    # monitoring-agent
    [True,  False, True,  False, False, True,  False, False],
    # ci-deployer
    [True,  False, True,  True,  False, False, False, False],
    # dev-alice
    [True,  False, True,  False, False, False, False, False],
    # ops-bob
    [True,  True,  True,  True,  True,  True,  False, False],
    # admin-claire
    [True,  True,  True,  True,  True,  True,  True,  True],
]

n_sujets = len(sujets)
n_perms = len(permissions)
cell_w = 1.5
cell_h = 0.7
x_offset = 3.5
y_offset = 0.5

# En-têtes colonnes
for j, perm in enumerate(permissions):
    ax.text(x_offset + j * cell_w + cell_w/2, y_offset + n_sujets * cell_h + 0.4,
            perm, ha="center", va="center", fontsize=8, fontweight="bold",
            color="#37474f", linespacing=1.3)

# Lignes
for i, (sujet, row) in enumerate(zip(sujets, matrice)):
    y = y_offset + (n_sujets - 1 - i) * cell_h
    # Label sujet
    ax.text(x_offset - 0.15, y + cell_h/2, sujet, ha="right", va="center",
            fontsize=8.5, fontweight="bold" if "admin" in sujet else "normal",
            color="#1a237e" if "SA:" in sujet else "#37474f")

    for j, autorise in enumerate(row):
        x = x_offset + j * cell_w
        if autorise:
            fc = "#c8e6c9" if autorise is True else "#fff9c4"
            symbol = "✓"
            tc = "#2e7d32"
        else:
            fc = "#ffcdd2"
            symbol = "✗"
            tc = "#c62828"

        rect = plt.Rectangle((x + 0.05, y + 0.05), cell_w - 0.1, cell_h - 0.1,
                               facecolor=fc, edgecolor="#e0e0e0", linewidth=0.5)
        ax.add_patch(rect)
        ax.text(x + cell_w/2, y + cell_h/2, symbol, ha="center", va="center",
                fontsize=12, color=tc, fontweight="bold")

ax.set_xlim(0, x_offset + n_perms * cell_w + 0.5)
ax.set_ylim(0, y_offset + n_sujets * cell_h + 1.5)

ax.text(0.5, 0.05,
        "SA = ServiceAccount (identité d'un Pod)   ✓ = autorisé   ✗ = interdit\n"
        "Principe du moindre privilège : accorder uniquement ce qui est strictement nécessaire",
        transform=ax.transAxes, ha="center", va="bottom", fontsize=8,
        color="#546e7a", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/15_rbac_matrix.png", dpi=130, bbox_inches="tight")
plt.show()
_images/4d7ee3ec020bbb2fa3a0140f9db0fc61f9c738a3803ef930c40c76c92aaa8183.png

Pod Security : sécuriser les conteneurs#

SecurityContext#

Le securityContext définit les paramètres de sécurité d’un Pod ou d’un conteneur.

# pod-securise.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-securise
spec:
  # SecurityContext au niveau du Pod (s'applique à tous les conteneurs)
  securityContext:
    runAsNonRoot: true          # Refuser si l'image utilise root
    runAsUser: 1000             # UID 1000
    runAsGroup: 3000            # GID 3000
    fsGroup: 2000               # GID pour les volumes montés
    seccompProfile:
      type: RuntimeDefault      # Profil seccomp par défaut (filtre les syscalls)

  containers:
    - name: app
      image: mon-app:1.0
      # SecurityContext au niveau du conteneur (surcharge le Pod)
      securityContext:
        allowPrivilegeEscalation: false   # Empêche sudo / setuid
        readOnlyRootFilesystem: true      # FS en lecture seule
        capabilities:
          drop: ["ALL"]                   # Retirer TOUTES les capabilities Linux
          add: ["NET_BIND_SERVICE"]       # Ré-ajouter seulement ce qui est nécessaire
      volumeMounts:
        # Si readOnlyRootFilesystem: true, les répertoires avec écriture nécessaire
        # doivent être montés explicitement
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/cache

  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}

PodSecurityAdmission (PSA)#

Depuis Kubernetes 1.25, PodSecurityAdmission est le mécanisme intégré pour appliquer des profils de sécurité au niveau du namespace.

# Appliquer un profil de sécurité à un namespace entier
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # Mode enforce : rejette les Pods non conformes
    pod-security.kubernetes.io/enforce: restricted
    # Mode warn : avertissement mais le Pod est quand même créé
    pod-security.kubernetes.io/warn: restricted
    # Mode audit : enregistre dans les logs d'audit
    pod-security.kubernetes.io/audit: restricted

Niveaux de sécurité PSA :

  • privileged : aucune restriction (réservé aux namespaces système)

  • baseline : restrictions minimales (empêche les escalades de privilèges connues)

  • restricted : strictement sécurisé (bonne pratique pour les applications de production)

NetworkPolicy : isolation réseau#

Par défaut dans Kubernetes, tous les Pods peuvent communiquer avec tous les autres Pods dans le cluster. C’est pratique pour le développement, mais dangereux en production.

# networkpolicy-deny-all.yaml — Bloquer tout le trafic par défaut
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
  namespace: production
spec:
  podSelector: {}    # S'applique à TOUS les Pods du namespace
  policyTypes:
    - Ingress
    - Egress
  # Pas de règles → tout est bloqué
# networkpolicy-api.yaml — Autoriser seulement le trafic nécessaire
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api            # S'applique aux Pods de l'API
  policyTypes:
    - Ingress
    - Egress

  ingress:
    # Autoriser le trafic entrant depuis l'Ingress Controller (namespace ingress-nginx)
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - port: 8080

    # Autoriser depuis d'autres Pods de l'API (réplication)
    - from:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - port: 8080

  egress:
    # Autoriser vers la base de données (même namespace)
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - port: 5432

    # Autoriser les requêtes DNS (OBLIGATOIRE si on bloque tout)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 7))

# --- Sans NetworkPolicy ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Sans NetworkPolicy\n(tout le monde parle à tout le monde)", fontsize=11,
             fontweight="bold", color="#c62828")

pods_g1 = [(2, 6.5, "api", "#e3f2fd", "#1565c0"),
           (5, 6.5, "web", "#e8f5e9", "#2e7d32"),
           (8, 6.5, "admin", "#fff3e0", "#e65100"),
           (2, 3.5, "db",   "#fce4ec", "#e91e63"),
           (5, 3.5, "redis","#ede7f6", "#7e57c2"),
           (8, 3.5, "batch","#e0f7fa", "#00838f")]

for x, y, nom, fc, ec in pods_g1:
    b = FancyBboxPatch((x - 0.9, y - 0.45), 1.8, 0.9, boxstyle="round,pad=0.08",
                        facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax.add_patch(b)
    ax.text(x, y, nom, ha="center", va="center", fontsize=9, fontweight="bold", color=ec)

# Flèches "tout vers tout" (dangereux)
for i, (x1, y1, n1, _, _) in enumerate(pods_g1):
    for j, (x2, y2, n2, _, _) in enumerate(pods_g1):
        if i != j:
            ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                        arrowprops=dict(arrowstyle="-", color="#ef9a9a", lw=0.8, alpha=0.5))

# Cas dangereux en rouge
ax.annotate("", xy=(8, 4.4), xytext=(2, 5.6),
            arrowprops=dict(arrowstyle="->", color="#f44336", lw=2.5))
ax.text(5, 5.3, "batch peut accéder à db\n→ RISQUE !",
        ha="center", va="center", fontsize=8, color="#c62828", fontweight="bold",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#ffebee", edgecolor="#f44336"))

# --- Avec NetworkPolicy ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Avec NetworkPolicy\n(flux autorisés uniquement)", fontsize=11,
              fontweight="bold", color="#2e7d32")

pods_g2 = [(2, 6.5, "api", "#e3f2fd", "#1565c0"),
           (5, 6.5, "web", "#e8f5e9", "#2e7d32"),
           (8, 6.5, "admin", "#fff3e0", "#e65100"),
           (2, 3.5, "db",   "#fce4ec", "#e91e63"),
           (5, 3.5, "redis","#ede7f6", "#7e57c2"),
           (8, 3.5, "batch","#e0f7fa", "#00838f")]

for x, y, nom, fc, ec in pods_g2:
    b = FancyBboxPatch((x - 0.9, y - 0.45), 1.8, 0.9, boxstyle="round,pad=0.08",
                        facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax2.add_patch(b)
    ax2.text(x, y, nom, ha="center", va="center", fontsize=9, fontweight="bold", color=ec)

# Flux autorisés uniquement
flux_ok = [
    ((2, 6.05), (2, 3.95), "api → db"),
    ((2, 6.05), (5, 3.95), "api → redis"),
    ((5, 6.05), (2, 3.95), "web → db"),
    ((8, 3.95), (5, 3.95), "batch → redis"),
]
for (x1, y1), (x2, y2), label in flux_ok:
    ax2.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#43a047", lw=2))
    ax2.text((x1+x2)/2 + 0.2, (y1+y2)/2, label, fontsize=7, color="#2e7d32")

# Flux bloqués
ax2.annotate("", xy=(8, 4.4), xytext=(2, 5.6),
            arrowprops=dict(arrowstyle="-|>", color="#bdbdbd", lw=1.5, linestyle="dashed"))
ax2.text(5, 5.2, "✗ batch → db\nBLOQUÉ",
        ha="center", va="center", fontsize=8, color="#9e9e9e", fontweight="bold")

ax2.text(5, 0.5, "NetworkPolicy CNI requis : Calico, Cilium, Weave...",
         ha="center", va="center", fontsize=8, color="#546e7a", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/15_network_policy.png", dpi=130, bbox_inches="tight")
plt.show()
_images/dac7667e51037b10e243ee1c25e22c9ca6dac190e8a10a66cd49acd3234f4c6d.png

Gestion des secrets : au-delà du base64#

La fausse sécurité du base64#

# Un Secret Kubernetes est par défaut stocké en base64 — ce N'EST PAS du chiffrement !
kubectl get secret mon-secret -o yaml
# data:
#   password: dGVzdDEyMzQ=

# Décoder est trivial :
echo "dGVzdDEyMzQ=" | base64 -d
# test1234

Base64 ≠ Chiffrement

Le base64 est un encodage, pas un chiffrement. N’importe qui ayant accès à etcd ou au fichier YAML peut lire vos secrets. Pour une vraie sécurité, il faut :

  1. Chiffrement de etcd au repos (--encryption-provider-config)

  2. RBAC strict sur les Secrets (peu de ServiceAccounts y ont accès)

  3. Outils de gestion de secrets externes (Vault, External Secrets Operator)

HashiCorp Vault + External Secrets Operator#

# External Secrets Operator : synchronise les secrets d'un vault externe vers K8s
# Installation
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
# secretstore.yaml — Connexion à HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.exemple.com:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "mon-application"
# externalsecret.yaml — Récupérer un secret depuis Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h              # Synchroniser toutes les heures
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-credentials-k8s      # Nom du Secret Kubernetes créé
    creationPolicy: Owner
  data:
    - secretKey: username          # Clé dans le Secret K8s
      remoteRef:
        key: production/database   # Chemin dans Vault
        property: username         # Propriété dans le secret Vault
    - secretKey: password
      remoteRef:
        key: production/database
        property: password

Sécurité des images#

Scanning avec Trivy#

# Scanner une image avec Trivy (outil gratuit, très complet)
trivy image nginx:latest

# Résultat :
# nginx:latest (debian 12.4)
# =============================
# Total: 23 (HIGH: 5, CRITICAL: 2)
#
# CVE-2024-XXXX  CRITICAL  curl  7.88.1  → mettre à jour vers 8.x

# Scanner dans un Dockerfile (avant de pousser)
trivy image --exit-code 1 --severity CRITICAL mon-app:latest
# --exit-code 1 : fait échouer le build si des CVE CRITICAL sont trouvées

Kyverno : admission controller de validation#

Kyverno est un admission controller qui valide, mute et génère des ressources Kubernetes selon des politiques déclaratives.

# Installer Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
# kyverno-policy-nonroot.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-non-root
spec:
  validationFailureAction: Enforce    # Reject si non conforme
  rules:
    - name: check-runAsNonRoot
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["production", "staging"]
      validate:
        message: "Les Pods doivent s'exécuter en tant qu'utilisateur non-root"
        pattern:
          spec:
            securityContext:
              runAsNonRoot: "true"

Simulation Python : audit de conformité RBAC et vérification de manifestes#

from dataclasses import dataclass, field
from typing import List, Dict, Set, Optional

@dataclass
class VerificateurManifeste:
    """
    Vérifie la conformité sécurité d'un manifeste Pod Kubernetes.
    Simule un admission controller ou un outil comme Kyverno/OPA.
    """

    def verifier(self, manifeste: dict) -> dict:
        """Analyse un manifeste et retourne les résultats."""
        resultats = {"ok": [], "avertissements": [], "critiques": []}
        spec = manifeste.get("spec", {})
        containers = spec.get("containers", [])
        pod_security = spec.get("securityContext", {})

        # --- Vérifications au niveau du Pod ---
        if pod_security.get("runAsNonRoot"):
            resultats["ok"].append("Pod.securityContext.runAsNonRoot = true")
        else:
            resultats["critiques"].append(
                "Pod.securityContext.runAsNonRoot manquant — le pod peut tourner en root")

        if spec.get("automountServiceAccountToken") is False:
            resultats["ok"].append("automountServiceAccountToken = false")
        else:
            resultats["avertissements"].append(
                "automountServiceAccountToken non désactivé — le token SA est monté par défaut")

        # --- Vérifications par conteneur ---
        for container in containers:
            nom = container.get("name", "?")
            ctx = container.get("securityContext", {})
            resources = container.get("resources", {})

            # Capabilities
            if ctx.get("capabilities", {}).get("drop") == ["ALL"]:
                resultats["ok"].append(f"{nom}: capabilities.drop = ALL")
            else:
                resultats["avertissements"].append(
                    f"{nom}: capabilities.drop ALL non configuré")

            # allowPrivilegeEscalation
            if ctx.get("allowPrivilegeEscalation") is False:
                resultats["ok"].append(f"{nom}: allowPrivilegeEscalation = false")
            else:
                resultats["critiques"].append(
                    f"{nom}: allowPrivilegeEscalation non désactivé → risque d'escalade")

            # readOnlyRootFilesystem
            if ctx.get("readOnlyRootFilesystem"):
                resultats["ok"].append(f"{nom}: readOnlyRootFilesystem = true")
            else:
                resultats["avertissements"].append(
                    f"{nom}: readOnlyRootFilesystem = false — l'app peut modifier son FS")

            # Resources
            if resources.get("requests") and resources.get("limits"):
                resultats["ok"].append(f"{nom}: resources requests et limits définis")
            elif resources.get("requests"):
                resultats["avertissements"].append(
                    f"{nom}: limits manquants — risque de consommation excessive")
            else:
                resultats["critiques"].append(
                    f"{nom}: aucun resources défini — BestEffort QoS, planification non garantie")

            # Secrets dans les env vars
            for env in container.get("env", []):
                if "password" in env.get("name", "").lower() or \
                   "secret" in env.get("name", "").lower() or \
                   "key" in env.get("name", "").lower():
                    if "value" in env:  # valeur en clair !
                        resultats["critiques"].append(
                            f"{nom}: secret '{env['name']}' en clair dans les env vars !")
                    elif "valueFrom" in env and "secretKeyRef" in env["valueFrom"]:
                        resultats["ok"].append(
                            f"{nom}: secret '{env['name']}' lu depuis un Secret K8s")

        return resultats

    def rapport(self, nom_manifeste: str, resultats: dict):
        total = sum(len(v) for v in resultats.values())
        score = len(resultats["ok"]) / total * 100 if total > 0 else 0

        print(f"\n{'='*60}")
        print(f"Audit de sécurité : {nom_manifeste}")
        print(f"Score de conformité : {score:.0f}% ({len(resultats['ok'])}/{total} contrôles réussis)")
        print(f"{'='*60}")

        if resultats["critiques"]:
            print(f"\n❌ CRITIQUE ({len(resultats['critiques'])}) :")
            for r in resultats["critiques"]:
                print(f"   ✗ {r}")

        if resultats["avertissements"]:
            print(f"\n⚠  AVERTISSEMENTS ({len(resultats['avertissements'])}) :")
            for r in resultats["avertissements"]:
                print(f"   ! {r}")

        if resultats["ok"]:
            print(f"\n✅ RÉUSSIS ({len(resultats['ok'])}) :")
            for r in resultats["ok"]:
                print(f"   ✓ {r}")


verificateur = VerificateurManifeste()

# Manifeste non sécurisé
manifeste_mauvais = {
    "spec": {
        "containers": [{
            "name": "webapp",
            "image": "mon-app:latest",
            "env": [
                {"name": "DATABASE_PASSWORD", "value": "MonMotDePasse123"},  # DANGER !
                {"name": "APP_ENV", "value": "production"},
            ],
            "resources": {
                "requests": {"cpu": "100m"}
                # limits manquantes !
            }
            # securityContext manquant
        }]
        # pas de securityContext au niveau pod
    }
}

# Manifeste sécurisé
manifeste_bon = {
    "spec": {
        "automountServiceAccountToken": False,
        "securityContext": {
            "runAsNonRoot": True,
            "runAsUser": 1000,
            "fsGroup": 2000,
        },
        "containers": [{
            "name": "webapp",
            "image": "mon-app:1.2.3",
            "env": [
                {
                    "name": "DATABASE_PASSWORD",
                    "valueFrom": {"secretKeyRef": {"name": "db-secret", "key": "password"}}
                },
            ],
            "resources": {
                "requests": {"cpu": "100m", "memory": "128Mi"},
                "limits":   {"cpu": "500m", "memory": "256Mi"},
            },
            "securityContext": {
                "allowPrivilegeEscalation": False,
                "readOnlyRootFilesystem": True,
                "capabilities": {"drop": ["ALL"]},
            }
        }]
    }
}

res_mauvais = verificateur.verifier(manifeste_mauvais)
verificateur.rapport("pod-non-securise.yaml", res_mauvais)

res_bon = verificateur.verifier(manifeste_bon)
verificateur.rapport("pod-securise.yaml", res_bon)
============================================================
Audit de sécurité : pod-non-securise.yaml
Score de conformité : 0% (0/7 contrôles réussis)
============================================================

❌ CRITIQUE (3) :
   ✗ Pod.securityContext.runAsNonRoot manquant — le pod peut tourner en root
   ✗ webapp: allowPrivilegeEscalation non désactivé → risque d'escalade
   ✗ webapp: secret 'DATABASE_PASSWORD' en clair dans les env vars !

⚠  AVERTISSEMENTS (4) :
   ! automountServiceAccountToken non désactivé — le token SA est monté par défaut
   ! webapp: capabilities.drop ALL non configuré
   ! webapp: readOnlyRootFilesystem = false — l'app peut modifier son FS
   ! webapp: limits manquants — risque de consommation excessive

============================================================
Audit de sécurité : pod-securise.yaml
Score de conformité : 100% (7/7 contrôles réussis)
============================================================

✅ RÉUSSIS (7) :
   ✓ Pod.securityContext.runAsNonRoot = true
   ✓ automountServiceAccountToken = false
   ✓ webapp: capabilities.drop = ALL
   ✓ webapp: allowPrivilegeEscalation = false
   ✓ webapp: readOnlyRootFilesystem = true
   ✓ webapp: resources requests et limits définis
   ✓ webapp: secret 'DATABASE_PASSWORD' lu depuis un Secret K8s

Hide code cell source

# Visualisation : scores de conformité
fig, ax = plt.subplots(figsize=(10, 4))

scenarios = ["Pod non sécurisé", "Pod sécurisé"]
critiques = [len(res_mauvais["critiques"]), len(res_bon["critiques"])]
avertissements = [len(res_mauvais["avertissements"]), len(res_bon["avertissements"])]
ok = [len(res_mauvais["ok"]), len(res_bon["ok"])]

x = np.arange(len(scenarios))
w = 0.5

p1 = ax.bar(x, critiques,      w, label="Critique",      color="#f44336", alpha=0.85)
p2 = ax.bar(x, avertissements, w, label="Avertissement",  color="#ffa726", alpha=0.85, bottom=critiques)
p3 = ax.bar(x, ok,             w, label="Réussi",         color="#66bb6a", alpha=0.85,
             bottom=[c + a for c, a in zip(critiques, avertissements)])

ax.set_xticks(x)
ax.set_xticklabels(scenarios, fontsize=12)
ax.set_ylabel("Nombre de contrôles")
ax.set_title("Résultats de l'audit de sécurité des manifestes Pods", fontweight="bold")
ax.legend(fontsize=10)

totaux = [sum(x) for x in zip(critiques, avertissements, ok)]
for i, (c, a, o, t) in enumerate(zip(critiques, avertissements, ok, totaux)):
    score = o / t * 100
    ax.text(i, t + 0.1, f"{score:.0f}%", ha="center", va="bottom",
            fontsize=13, fontweight="bold",
            color="#2e7d32" if score >= 70 else "#c62828")

sns.despine(ax=ax)
plt.tight_layout()
plt.savefig("_static/15_audit_securite.png", dpi=130, bbox_inches="tight")
plt.show()
_images/0749963d770b952ea8be9a1262cda6989d22ee573cb39a78ee8aca90382125eb.png

Points clés à retenir#

  • RBAC est le mécanisme d’autorisation de Kubernetes : Role/ClusterRole définissent les permissions, RoleBinding/ClusterRoleBinding les associent à des sujets (users, ServiceAccounts)

  • Principe du moindre privilège : accorder uniquement les verbes et ressources strictement nécessaires — ne jamais utiliser verbs: ["*"] en production

  • Le securityContext permet de configurer runAsNonRoot, readOnlyRootFilesystem, capabilities.drop: ALL et allowPrivilegeEscalation: false

  • NetworkPolicy isole le trafic réseau entre Pods — commencer par un deny-all puis ouvrir sélectivement

  • Le base64 des Secrets Kubernetes n’est pas du chiffrement — utiliser le chiffrement d’etcd au repos et/ou un gestionnaire de secrets externe (Vault, External Secrets Operator)

  • Kyverno ou OPA/Gatekeeper permettent d’appliquer des politiques de sécurité automatiquement à l’admission des Pods

  • Scanner régulièrement les images avec Trivy et intégrer ce scan dans la CI/CD