---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{code-cell} python
:tags: [hide-input]
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.

```{code-cell} python
:tags: [hide-input]
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()
```

## 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

```yaml
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: mon-application
  namespace: production
automountServiceAccountToken: false   # Désactiver l'injection automatique du token
```

```yaml
# 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
```

```yaml
# 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
```

```{admonition} ClusterRole vs Role — quand utiliser lequel ?
:class: tip
- **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.
```

```{code-cell} python
:tags: [hide-input]
# 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()
```

## Pod Security : sécuriser les conteneurs

### SecurityContext

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

```yaml
# 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.

```yaml
# 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.

```yaml
# 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é
```

```yaml
# 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
```

```{code-cell} python
:tags: [hide-input]
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()
```

## Gestion des secrets : au-delà du base64

### La fausse sécurité du base64

```bash
# 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
```

```{admonition} Base64 ≠ Chiffrement
:class: danger
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

```bash
# 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
```

```yaml
# 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"
```

```yaml
# 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

```bash
# 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.

```bash
# Installer Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
```

```yaml
# 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

```{code-cell} python
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)
```

```{code-cell} python
:tags: [hide-input]
# 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()
```

## 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
