En plein après-midi, le SAV de Heartbeat se fait submerger par les appels des patients inquiets, incapables de prendre ou de modifier leurs rendez-vous. Les équipes soignantes, elles aussi, sont dans l'incertitude, sans accès aux dossiers médicaux ni aux outils de communication internes. Mais que se passe-t-il ?
Tous les services Kubernetes se sont arrêtés soudainement, sans avertissement et sans raison apparente. Vous vous penchez donc sur les logs d’audit du cluster.
Pour la réalisation de cet exercice, un fichier de journaux d'audit de Kubernetes a été fourni. Il contient toutes les informations nécessaires pour identifier les actions effectuées par les utilisateurs et les applications sur le cluster.
L'objectif de cet exercice est d'analyser les journaux d'audit pour identifier les actions malveillantes effectuées par un utilisateur spécifique.
Le scénario décrit une entreprise ayant récemment subi un déni de service. Nous nous dirigeons donc vers des actions de suppression de ressources Kubernetes, telles que les pods, les services et les déploiements. La technique que nous avons utilisée était d’analyser le nombre de delete effectués par les 4 utilisateurs par fenêtre glissante. Ensuite, il suffit d’observer quel utilisateur a un nombre de delete le plus élevé et à quel moment. Pour ce faire, étant donné que le fichier de log est extrêmement lourd, nous avons décidé de faire un script en Python. Je vous mets le script à la fin du fichier markdown.
Une fois le script lancé, nous obtenons ce résultat qu’il nous suffit d’analyser :
>>> Détection de pics DELETE par utilisateur <<<
Fenêtre: 5m | Top par utilisateur: 3
====================================================================================================
Utilisateur: REDACTED
----------------------------------------------------------------------------------------------------
[#1] REDACTED.399676+00:00 → 2025-01-20T11:57:02.527382+00:00 | deletes=104 | ~1511.7/min
Top minutes : 2025-01-20T11:57:00+00:00 (58), 2025-01-20T11:56:00+00:00 (46)
Top namespaces : kube-system (32), redis (18), mongodb (14), vault (14), intranet (12)
Top resources : pods (50), services (24), deployments (18), statefulsets (8), jobs (4)
[#2] 2025-02-23T13:14:30.602858+00:00 → 2025-02-23T13:18:42.721336+00:00 | deletes=102 | ~24.3/min
Top minutes : 2025-02-23T13:18:00+00:00 (30), 2025-02-23T13:15:00+00:00 (28), 2025-02-23T13:14:00+00:00 (20), 2025-02-23T13:17:00+00:00 (20), 2025-02-23T13:16:00+00:00 (4)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
[#3] 2025-02-21T07:14:26.372314+00:00 → 2025-02-21T07:16:34.014435+00:00 | deletes=100 | ~47.0/min
Top minutes : 2025-02-21T07:15:00+00:00 (46), 2025-02-21T07:14:00+00:00 (28), 2025-02-21T07:16:00+00:00 (26)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (100)
Utilisateur: k3s-cloud-controller-manager
----------------------------------------------------------------------------------------------------
[#1] 2025-01-20T11:57:01.238264+00:00 → 2025-01-20T11:57:01.251828+00:00 | deletes=2 | ~120.0/min
Top minutes : 2025-01-20T11:57:00+00:00 (2)
Top namespaces : kube-system (2)
Top resources : daemonsets (2)
Utilisateur: martin.matin
----------------------------------------------------------------------------------------------------
[#1] 2025-02-28T23:51:47.095207+00:00 → 2025-02-28T23:54:20.469307+00:00 | deletes=104 | ~40.7/min
Top minutes : 2025-02-28T23:53:00+00:00 (42), 2025-02-28T23:52:00+00:00 (38), 2025-02-28T23:54:00+00:00 (16), 2025-02-28T23:51:00+00:00 (8)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (104)
[#2] 2025-02-26T23:43:27.882982+00:00 → 2025-02-26T23:45:50.495289+00:00 | deletes=102 | ~42.9/min
Top minutes : 2025-02-26T23:44:00+00:00 (43), 2025-02-26T23:45:00+00:00 (34), 2025-02-26T23:43:00+00:00 (25)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
[#3] 2025-02-23T12:07:31.660359+00:00 → 2025-02-23T12:09:52.748810+00:00 | deletes=100 | ~42.5/min
Top minutes : 2025-02-23T12:09:00+00:00 (42), 2025-02-23T12:08:00+00:00 (38), 2025-02-23T12:07:00+00:00 (20)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (100)
Utilisateur: myreille.daniel
----------------------------------------------------------------------------------------------------
[#1] 2025-02-22T06:59:06.275031+00:00 → 2025-02-22T07:04:04.203386+00:00 | deletes=110 | ~22.2/min
Top minutes : 2025-02-22T07:03:00+00:00 (38), 2025-02-22T07:00:00+00:00 (34), 2025-02-22T06:59:00+00:00 (32), 2025-02-22T07:04:00+00:00 (4), 2025-02-22T07:02:00+00:00 (2)
Top namespaces : test-myreille-daniel-5317 (8), test-daniel-daniel-1994 (6), test-martin-matin-1623 (6), test-patrick-pinson-8938 (6), test-daniel-daniel-8921 (4)
Top resources : deployments (110)
[#2] 2025-02-23T15:44:19.269702+00:00 → 2025-02-23T15:46:32.023144+00:00 | deletes=102 | ~46.1/min
Top minutes : 2025-02-23T15:45:00+00:00 (46), 2025-02-23T15:44:00+00:00 (34), 2025-02-23T15:46:00+00:00 (22)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
[#3] 2025-02-03T17:22:18.960045+00:00 → 2025-02-03T17:24:52.468232+00:00 | deletes=102 | ~39.9/min
Top minutes : 2025-02-03T17:23:00+00:00 (40), 2025-02-03T17:24:00+00:00 (32), 2025-02-03T17:22:00+00:00 (30)
Top namespaces : test-patrick-pinson-8938 (6), test-myreille-daniel-8419 (6), test-daniel-daniel-8792 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4)
Top resources : deployments (102)
Utilisateur: patrick.pinson
----------------------------------------------------------------------------------------------------
[#1] 2025-02-27T01:08:53.476960+00:00 → 2025-02-27T01:11:20.463412+00:00 | deletes=102 | ~41.6/min
Top minutes : 2025-02-27T01:10:00+00:00 (43), 2025-02-27T01:09:00+00:00 (38), 2025-02-27T01:11:00+00:00 (15), 2025-02-27T01:08:00+00:00 (6)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
[#2] 2025-02-22T11:13:28.856561+00:00 → 2025-02-22T11:16:52.816945+00:00 | deletes=102 | ~30.0/min
Top minutes : 2025-02-22T11:15:00+00:00 (36), 2025-02-22T11:16:00+00:00 (36), 2025-02-22T11:13:00+00:00 (19), 2025-02-22T11:14:00+00:00 (11)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
[#3] 2025-02-26T21:38:20.600073+00:00 → 2025-02-26T21:42:02.090110+00:00 | deletes=102 | ~27.6/min
Top minutes : 2025-02-26T21:39:00+00:00 (30), 2025-02-26T21:40:00+00:00 (26), 2025-02-26T21:41:00+00:00 (26), 2025-02-26T21:38:00+00:00 (18), 2025-02-26T21:42:00+00:00 (2)
Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
Top resources : deployments (102)
Nous avons décidé d'afficher un peu plus de détails nous permettant d'analyser au mieux les actions des différents utilisateurs.
En en observant l'output du script nous nous rendons compte d'une massive quantité d'actions de suppression (~1511.7/min) effectuées par l'utilisateur Daniel le 20 janvier 2025 entre 11:56:58 et 11:57:02. Cela correspond à un kubectl delete all d'où la réponse à la première question de l'exercice.
Grâce aux journaux récupérés, les réponses aux autres questions sont triviales.
import os
import sys
import json
from datetime import datetime, timedelta, timezone
from collections import defaultdict, Counter
# --- Paramètres fixes ---
WINDOW = timedelta(minutes=5) # fenêtre glissante
TOP = 3 # top pics par utilisateur
def is_system_user(username):
if not username:
return True
return (
username.startswith("system:")
or ":serviceaccount:" in username
or username == "kube-apiserver"
)
def parse_ts(ts):
if ts.endswith("Z"):
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
if len(ts) == 10 and ts[4] == "-" and ts[7] == "-":
return datetime.fromisoformat(ts + "T00:00:00+00:00")
dt = datetime.fromisoformat(ts)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def floor_minute(dt):
return dt.replace(second=0, microsecond=0)
def sliding_windows_max(events, window):
res = []
i = 0
for j, t in enumerate(events):
while i <= j and (t - events[i]) > window:
i += 1
res.append((i, j, j - i + 1))
return res
def select_top_nonoverlapping(candidates, topk):
selected = []
used_ranges = []
for i, j, c in candidates:
ok = True
for ui, uj in used_ranges:
ov = max(0, min(j, uj) - max(i, ui) + 1) # overlap en nb d'index
if ov > 0:
if ov >= 0.5 * (j - i + 1) or ov >= 0.5 * (uj - ui + 1):
ok = False
break
if ok:
selected.append((i, j, c))
used_ranges.append((i, j))
if len(selected) >= topk:
break
return selected
def main():
if len(sys.argv) != 2:
print(f"Usage: {os.path.basename(sys.argv[0])} /chemin/vers/audit_logs.jsonl", file=sys.stderr)
sys.exit(1)
input_path = sys.argv[1]
if not os.path.exists(input_path):
print(f"[Erreur] Fichier introuvable: {input_path}", file=sys.stderr)
sys.exit(1)
per_user_events = defaultdict(list)
with open(input_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
if ev.get("verb") != "delete":
continue
user = (ev.get("user") or {}).get("username") or "<unknown>"
if is_system_user(user):
continue
ts_str = ev.get("requestReceivedTimestamp") or ev.get("stageTimestamp")
if not ts_str:
continue
try:
ts = parse_ts(ts_str)
except Exception:
continue
obj = ev.get("objectRef") or {}
ns = obj.get("namespace") or "<cluster-scope>"
res = obj.get("resource") or obj.get("apiGroup") or "<unknown-res>"
name = obj.get("name") or ""
per_user_events[user].append((ts, ns, res, name))
if not per_user_events:
print("Aucun évènement DELETE (hors comptes système).")
return
for u in per_user_events:
per_user_events[u].sort(key=lambda x: x[0])
print("\n>>> Détection de pics DELETE par utilisateur <<<")
print(f"Fenêtre: {int(WINDOW.total_seconds()//60)}m | Top par utilisateur: {TOP}")
print("=" * 100)
for user in sorted(per_user_events.keys()):
events = per_user_events[user]
times = [e[0] for e in events]
if not times:
continue
# calcul des fenêtres glissantes
windows = sliding_windows_max(times, WINDOW)
windows_sorted = sorted(
windows,
key=lambda t: (t[2], -(times[t[1]] - times[t[0]]).total_seconds(), times[t[1]]),
reverse=True,
)
top_sel = select_top_nonoverlapping(windows_sorted, TOP)
if not top_sel:
continue
print(f"\nUtilisateur: {user}")
print("-" * 100)
for rank, (i, j, c) in enumerate(top_sel, start=1):
t0, t1 = times[i], times[j]
duration = max(timedelta(seconds=1), t1 - t0) # éviter /0
per_min = c / (duration.total_seconds() / 60.0)
slice_ev = events[i : j + 1]
by_min = Counter(floor_minute(ts) for ts, _, _, _ in slice_ev).most_common(5)
top_ns = Counter(ns for _, ns, _, _ in slice_ev).most_common(5)
top_res = Counter(res for _, _, res, _ in slice_ev).most_common(5)
print(f"[#{rank}] {t0.isoformat()} → {t1.isoformat()} | deletes={c} | ~{per_min:.1f}/min")
print(" Top minutes : " + ", ".join(f"{m.isoformat()} ({n})" for m, n in by_min))
print(" Top namespaces : " + ", ".join(f"{ns} ({n})" for ns, n in top_ns))
print(" Top resources : " + ", ".join(f"{res} ({n})" for res, n in top_res))
if __name__ == "__main__":
main()