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

# 14. Kubernetes RBAC et sécurité de l'orchestration

Kubernetes est devenu le standard de facto pour l'orchestration de conteneurs, mais sa richesse fonctionnelle introduit une surface d'attaque considérable. Ce chapitre se concentre sur la sécurité : contrôle d'accès basé sur les rôles (RBAC), politiques réseau, standards de sécurité des pods et audit logging.

## Surface d'attaque Kubernetes

### API Server

L'API Server est le point d'entrée unique de tout cluster Kubernetes. Sa compromission équivaut à un accès total au cluster.

**Authentification** : Kubernetes supporte plusieurs méthodes — certificats X.509, tokens de service, OIDC, webhooks. La méthode `--anonymous-auth=true` (désactivée par défaut depuis K8s 1.6) permet des requêtes non authentifiées.

**TLS** : toutes les communications vers l'API Server doivent être chiffrées. Les configurations exposant l'API Server en HTTP (port 8080, interface `--insecure-bind-address`) sont critiques.

### etcd : chiffrement au repos

etcd stocke l'état complet du cluster, y compris les Secrets Kubernetes. Sans chiffrement au repos, un accès au disque etcd expose tous les secrets :

```yaml
# kube-apiserver — activer le chiffrement etcd
--encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
```

```yaml
# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}
```

### Kubelet : port 10250

Le kubelet expose une API sur le port 10250. Sans authentification, un attaquant peut exécuter des commandes dans n'importe quel pod du nœud :

```bash
# Attaque sans authentification (mauvaise configuration)
curl -sk https://node-ip:10250/run/default/mypod/mycontainer \
  -d "cmd=id"
```

La configuration sécurisée impose `--anonymous-auth=false` et `--authorization-mode=Webhook` sur le kubelet.

### Évasion de conteneur vers le nœud

Depuis un pod compromis, un attaquant peut tenter d'atteindre le nœud hôte via :
- Montage du filesystem hôte (`hostPath`)
- Partage du namespace PID hôte (`hostPID: true`)
- Accès au socket Docker/containerd du nœud
- Exploitation de vulnérabilités du noyau

```{admonition} Règle des 4C de la sécurité cloud-native
:class: important
La sécurité Kubernetes s'articule en couches concentriques : **Cloud** (infra), **Cluster** (API server, etcd), **Container** (runtime), **Code** (application). Une faille dans une couche externe peut contourner les contrôles des couches internes.
```

## RBAC Kubernetes : principes et objets

### Modèle RBAC

Kubernetes implémente un RBAC basé sur quatre objets :

| Objet | Scope | Description |
|-------|-------|-------------|
| `ServiceAccount` | Namespace | Identité d'un pod |
| `Role` | Namespace | Permissions sur des ressources namespaced |
| `ClusterRole` | Cluster | Permissions sur des ressources cluster-wide |
| `RoleBinding` | Namespace | Lie un sujet à un Role (ou ClusterRole) |
| `ClusterRoleBinding` | Cluster | Lie un sujet à un ClusterRole globalement |

### Définition d'un Role restrictif

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]
```

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: monitoring-agent
    namespace: production
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
```

### Verbes dangereux

Certains verbes Kubernetes accordent des permissions d'escalade de privilèges :

| Verbe | Ressource | Risque |
|-------|-----------|--------|
| `*` | `*` | Accès total — équivaut à cluster-admin |
| `bind` | `clusterrolebindings` | Peut s'attribuer n'importe quel rôle |
| `escalate` | `roles/clusterroles` | Peut modifier un rôle pour s'accorder des droits |
| `impersonate` | `users/groups/serviceaccounts` | Peut agir en tant qu'un autre sujet |
| `exec` | `pods` | Exécution de commandes dans n'importe quel pod |
| `create` | `pods` | Peut lancer un pod `--privileged` |

### Anti-patterns RBAC

```{admonition} Anti-patterns RBAC courants
:class: warning
- **`cluster-admin` généralisé** : attribuer `cluster-admin` à des opérateurs ou des applications de CI/CD. Un seul token compromis donne le contrôle total du cluster.
- **ServiceAccount par défaut avec token automontage** : tous les pods utilisent par défaut le ServiceAccount `default` avec un token. Désactiver avec `automountServiceAccountToken: false`.
- **Wildcards dans les resources** : `resources: ["*"]` accorde l'accès à des ressources futures non anticipées.
- **Bindings cross-namespace** : utiliser un `ClusterRoleBinding` quand un `RoleBinding` suffit élargit inutilement le scope.
```

## Network Policies : isolation L3/L4

### Principe des Network Policies

Par défaut, Kubernetes autorise tout le trafic entre pods (modèle flat network). Les Network Policies permettent de définir des règles d'ingress et d'egress au niveau IP/port.

**Prérequis** : le CNI plugin doit supporter les Network Policies (Calico, Cilium, Weave Net, mais **pas** Flannel seul).

### Default-deny : politique de base

```yaml
# Bloquer tout le trafic entrant dans le namespace production
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}      # Sélectionne tous les pods
  policyTypes:
    - Ingress
    - Egress
```

```yaml
# Autoriser seulement le frontend vers le backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
```

## Pod Security Standards

Les Pod Security Standards (PSS) remplacent les PodSecurityPolicies (dépréciées en 1.21, supprimées en 1.25). Trois niveaux sont définis :

| Niveau | Description | Usage recommandé |
|--------|-------------|-----------------|
| `Privileged` | Aucune restriction | Nœuds système, CNI plugins |
| `Baseline` | Prévient les escalades connues | Applications générales |
| `Restricted` | Durcissement complet | Applications sensibles |

### Activation via labels de namespace

```bash
# Appliquer le niveau Restricted avec mode enforce
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted
```

## securityContext hardened

Le `securityContext` configure les paramètres de sécurité au niveau pod et conteneur :

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: gcr.io/distroless/java21:nonroot
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}
  automountServiceAccountToken: false
```

## Admission controllers de sécurité

### OPA/Gatekeeper

Gatekeeper est un webhook d'admission Kubernetes qui évalue des politiques OPA (Rego) :

```yaml
# ConstraintTemplate — définit le type de contrainte
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg}] {
          not input.review.object.metadata.labels["app"]
          msg := "Label 'app' obligatoire"
        }
```

### Kyverno

Kyverno utilise des politiques YAML nativement Kubernetes, sans langage Rego :

```yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-privileged
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Les conteneurs privilégiés sont interdits"
        pattern:
          spec:
            containers:
              - =(securityContext):
                  =(privileged): "false"
    - name: require-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "runAsNonRoot doit être true"
        pattern:
          spec:
            securityContext:
              runAsNonRoot: true
```

## Audit logging Kubernetes

### Politique d'audit

L'audit logging enregistre les requêtes vers l'API Server. Quatre niveaux sont disponibles :

| Niveau | Contenu enregistré |
|--------|-------------------|
| `None` | Rien |
| `Metadata` | Métadonnées de la requête (qui, quoi, quand) |
| `Request` | Métadonnées + corps de la requête |
| `RequestResponse` | Métadonnées + corps requête + corps réponse |

```yaml
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Ne pas loguer les health checks
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
      - group: ""
        resources: ["endpoints", "services"]

  # Logger les secrets en RequestResponse
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["secrets"]

  # Logger les execs de pods
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["pods/exec", "pods/attach"]

  # Niveau par défaut
  - level: Metadata
```

## Visualisations

```{code-cell} python3
:tags: [hide-input]
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import pandas as pd
import numpy as np
import networkx as nx
```

### Matrice RBAC Kubernetes

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

service_accounts = ["ci-pipeline\n(cluster-admin)", "monitoring\n(pod-reader)", "app-backend\n(minimal)", "ingress-ctrl\n(networking)", "db-operator\n(secret-rw)"]
ressources_verbes = ["Secrets\n(get/list)", "Pods\n(exec)", "Deployments\n(create)", "ClusterRoles\n(bind)", "Nodes\n(get)", "ConfigMaps\n(read/write)"]

# 0=aucun accès, 1=accès limité/sûr, 2=accès large/risqué, 3=accès dangereux
matrix = np.array([
    [3, 3, 3, 3, 2, 3],   # ci-pipeline cluster-admin
    [0, 1, 0, 0, 1, 1],   # monitoring pod-reader
    [0, 0, 0, 0, 0, 1],   # app-backend minimal
    [0, 0, 1, 0, 1, 0],   # ingress-ctrl
    [2, 0, 0, 0, 0, 2],   # db-operator
])

cmap = sns.color_palette(["#2ecc71", "#f1c40f", "#e67e22", "#e74c3c"], as_cmap=False)
colors_mapped = [[cmap[v] for v in row] for row in matrix]

fig, ax = plt.subplots(figsize=(11, 5))

labels_text = {0: "Aucun", 1: "Limité", 2: "Large", 3: "Critique"}
annot = np.array([[labels_text[v] for v in row] for row in matrix])

for i, row in enumerate(matrix):
    for j, val in enumerate(row):
        color = cmap[val]
        ax.add_patch(plt.Rectangle([j - 0.5, i - 0.5], 1, 1, color=color, alpha=0.85))
        text_color = "white" if val >= 2 else "#2c3e50"
        ax.text(j, i, labels_text[val], ha="center", va="center",
                fontsize=9, fontweight="bold", color=text_color)

ax.set_xticks(range(len(ressources_verbes)))
ax.set_yticks(range(len(service_accounts)))
ax.set_xticklabels(ressources_verbes, fontsize=9)
ax.set_yticklabels(service_accounts, fontsize=9)
ax.set_xlim(-0.5, len(ressources_verbes) - 0.5)
ax.set_ylim(-0.5, len(service_accounts) - 0.5)
ax.set_title("Matrice RBAC Kubernetes — ServiceAccounts × Ressources", fontsize=13, pad=15)
ax.invert_yaxis()

legend_patches = [mpatches.Patch(color=cmap[i], label=l)
                  for i, l in enumerate(["Aucun accès", "Accès limité (sûr)", "Accès large (risqué)", "Accès critique (dangereux)"])]
ax.legend(handles=legend_patches, loc="upper right", bbox_to_anchor=(1.42, 1), fontsize=9)

sns.despine(left=True, bottom=True)
plt.savefig("k8s_rbac_matrix.png", dpi=100, bbox_inches="tight")
plt.show()
```

### Graphe des Network Policies

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

G = nx.DiGraph()

pods = {
    "Internet": "#95a5a6",
    "Ingress\nController": "#3498db",
    "Frontend": "#2ecc71",
    "Backend": "#27ae60",
    "Database": "#e74c3c",
    "Monitoring\n(Prometheus)": "#9b59b6",
    "Redis\n(Cache)": "#e67e22",
}

for pod in pods:
    G.add_node(pod)

# Flux autorisés (allowed=True) et bloqués (allowed=False)
edges = [
    ("Internet", "Ingress\nController", True),
    ("Ingress\nController", "Frontend", True),
    ("Ingress\nController", "Backend", True),
    ("Frontend", "Backend", True),
    ("Backend", "Database", True),
    ("Backend", "Redis\n(Cache)", True),
    ("Monitoring\n(Prometheus)", "Frontend", True),
    ("Monitoring\n(Prometheus)", "Backend", True),
    ("Monitoring\n(Prometheus)", "Database", True),
    # Flux bloqués par Network Policies
    ("Frontend", "Database", False),
    ("Frontend", "Redis\n(Cache)", False),
    ("Internet", "Backend", False),
    ("Internet", "Database", False),
]

pos = {
    "Internet": (0, 2),
    "Ingress\nController": (2, 2),
    "Frontend": (4, 3),
    "Backend": (4, 1),
    "Database": (6.5, 1),
    "Redis\n(Cache)": (6.5, 2.5),
    "Monitoring\n(Prometheus)": (1, 0),
}

fig, ax = plt.subplots(figsize=(13, 6))
ax.set_facecolor("#f8f9fa")

node_colors = [pods[n] for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=2200,
                       ax=ax, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=8, font_color="white",
                        font_weight="bold", ax=ax)

allowed_edges = [(u, v) for u, v, a in edges if a]
blocked_edges  = [(u, v) for u, v, a in edges if not a]

nx.draw_networkx_edges(G, pos, edgelist=allowed_edges,
                       edge_color="#2ecc71", arrows=True,
                       arrowsize=20, width=2.5, ax=ax,
                       connectionstyle="arc3,rad=0.05")
nx.draw_networkx_edges(G, pos, edgelist=blocked_edges,
                       edge_color="#e74c3c", arrows=True,
                       arrowsize=20, width=2, ax=ax,
                       style="dashed", connectionstyle="arc3,rad=0.1")

green_patch = mpatches.Patch(color="#2ecc71", label="Flux autorisé (NetworkPolicy)")
red_patch   = mpatches.Patch(color="#e74c3c", label="Flux bloqué (default-deny)")
ax.legend(handles=[green_patch, red_patch], loc="lower right", fontsize=10)

ax.set_title("Topologie réseau Kubernetes avec Network Policies", fontsize=13, pad=15)
ax.axis("off")

plt.savefig("k8s_network_policies.png", dpi=100, bbox_inches="tight")
plt.show()
```

### Timeline d'une séquence d'attaque Kubernetes

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

evenements = [
    (0,  "Énumération API",          "GET /api/v1/namespaces",                    "Reconnaissance",   "#3498db"),
    (1,  "Listing ServiceAccounts",  "GET /api/v1/serviceaccounts",               "Reconnaissance",   "#3498db"),
    (2,  "Accès token SA default",   "GET /secrets (token automontage)",          "Escalade",         "#e67e22"),
    (3,  "Listing pods système",     "GET /api/v1/pods (kube-system)",            "Reconnaissance",   "#3498db"),
    (4,  "Exec dans pod",            "POST /api/v1/pods/etcd/exec",               "Intrusion",        "#e74c3c"),
    (5,  "Lecture etcd",             "etcdctl get / --prefix (secrets cluster)",  "Exfiltration",     "#c0392b"),
    (6,  "Création pod privilégié",  "POST /api/v1/pods (hostPID + hostPath /)",  "Évasion conteneur","#8e44ad"),
    (7,  "Accès nœud hôte",         "chroot /host (filesystem nœud)",            "Compromission nœud","#6c3483"),
]

fig, ax = plt.subplots(figsize=(13, 6))
ax.set_facecolor("#1a1a2e")

phases_colors = {
    "Reconnaissance": "#3498db",
    "Escalade":       "#e67e22",
    "Intrusion":      "#e74c3c",
    "Exfiltration":   "#c0392b",
    "Évasion conteneur": "#8e44ad",
    "Compromission nœud": "#6c3483",
}

y_positions = {"Reconnaissance": 3, "Escalade": 2, "Intrusion": 1,
               "Exfiltration": 0.5, "Évasion conteneur": -0.5, "Compromission nœud": -1.5}

for t, nom, detail, phase, couleur in evenements:
    y = y_positions[phase]
    ax.scatter(t, y, s=200, color=couleur, zorder=5, edgecolors="white", linewidths=1.5)
    offset_y = 0.25 if t % 2 == 0 else -0.35
    ax.annotate(f"t+{t}m\n{nom}", (t, y),
                xytext=(t, y + offset_y),
                fontsize=7.5, color="white", ha="center", va="center",
                fontweight="bold")
    ax.annotate(detail, (t, y),
                xytext=(t, y + offset_y - 0.28),
                fontsize=6.5, color="#bdc3c7", ha="center", va="top", style="italic")

# Ligne de temps
ax.axhline(y=3,    color="#3498db",  alpha=0.3, linewidth=1.5, linestyle="--")
ax.axhline(y=2,    color="#e67e22",  alpha=0.3, linewidth=1.5, linestyle="--")
ax.axhline(y=1,    color="#e74c3c",  alpha=0.3, linewidth=1.5, linestyle="--")
ax.axhline(y=0.5,  color="#c0392b",  alpha=0.3, linewidth=1.5, linestyle="--")
ax.axhline(y=-0.5, color="#8e44ad",  alpha=0.3, linewidth=1.5, linestyle="--")
ax.axhline(y=-1.5, color="#6c3483",  alpha=0.3, linewidth=1.5, linestyle="--")

# Labels des phases
for phase, y in y_positions.items():
    ax.text(-0.6, y, phase, fontsize=8, color=phases_colors[phase],
            ha="right", va="center", fontweight="bold")

ax.set_xlim(-1, 8)
ax.set_ylim(-2.5, 4)
ax.set_xlabel("Temps (minutes depuis la compromission initiale)", color="white", fontsize=10)
ax.set_title("Timeline d'une attaque Kubernetes : énumération → évasion de conteneur",
             fontsize=12, color="white", pad=15)
ax.tick_params(colors="white")
ax.spines["bottom"].set_color("#4a4a6a")
for spine in ["top", "left", "right"]:
    ax.spines[spine].set_visible(False)
ax.set_yticks([])

plt.savefig("k8s_attack_timeline.png", dpi=100, bbox_inches="tight", facecolor="#1a1a2e")
plt.show()
```

## Résumé

1. **Surface d'attaque multi-composants** : l'API Server, etcd (chiffrement au repos obligatoire), le kubelet (port 10250 non authentifié) et les nœuds constituent autant de vecteurs d'entrée distincts.

2. **RBAC granulaire** : ServiceAccounts dédiés par application, rôles au scope minimum, désactivation du token automontage par défaut, bannissement de `cluster-admin` hors administrateurs humains.

3. **Verbes dangereux** : `bind`, `escalate`, `impersonate` et `exec` permettent une escalade de privilèges directe — les surveiller avec des outils comme `kubectl-who-can` ou rbac-police.

4. **Network Policies** : adopter un modèle default-deny puis ouvrir explicitement les flux nécessaires. Exige un CNI compatible (Calico, Cilium).

5. **Pod Security Standards** : le niveau `restricted` impose runAsNonRoot, readOnlyRootFilesystem, drop capabilities et seccomp. Activé via labels de namespace, facile à auditer.

6. **Admission controllers** : OPA/Gatekeeper (Rego) et Kyverno (YAML natif) permettent de définir des politiques guardrails au niveau cluster, bloquant les déploiements non conformes avant création.

7. **Audit logging** : enregistrer systématiquement les opérations sur les secrets, les execs de pods et les mutations de RBAC. Analyser avec des outils SIEM (Falco, Elastic) pour détecter les séquences d'attaque.
