Aller au contenu
TAAF-OPS --:-- UTC

TP 5 — Télémétrie satellite (AegisSat)

3h00 Acte 1 · Monitoring Avancé Data · Télémétrie · Spatial · Détection

Auteur : Thibaut Fontaine — Kodetis Institution : Université de la Réunion Date : 2026 Couche traitée : commande / télémétrie des sous-systèmes (complément du Module 2 — Downgrade PQC qui couvre la couche réseau/crypto).



📡 Poste SOC · La Réunion — Acte 1, Voir (et le pont vers Détecter) : vous instrumentez la liaison SATCOM du réseau austral. Liaison nominale… pour l’instant.

🎯 Objectif métier — un satellite ne « tombe » pas comme un serveur : une attaque se lit dans la dérive de sa télémétrie (température, CPU, attitude, comms). Apprendre à la voir, et à la distinguer du bruit opérationnel normal.

Le jour J, un satellite de reconnaissance étatique (YAOGAN-35A) survole la zone (14:23Z) et l’uplink subit un downgrade crypto (10:14Z). Avant de prouver l’intrusion (Acte 3), il faut d’abord voir la télémétrie du segment spatial — sinon une commande injectée (HeaterUp, CommDown…) passe pour une simple fluctuation. Ici, vous posez les yeux sur le satellite.

Ce TP s’appuie sur la taxonomie d’un testbed de cybersécurité satellite reconnu : texydo/satellite_security_testbed (AegisSat, hardware-in-the-loop : OBC, ADCS à bobines de Helmholtz, COSMOS/RubySat).

  • Comprendre l’architecture d’un satellite par sous-systèmes (OBC, ADCS, EPS, TPL, COM, RF, charge utile).
  • Générer une télémétrie réaliste avec des fenêtres d’attaque labellisées.
  • Ingérer dans PostgreSQL base_af et visualiser la santé satellite dans Grafana.
  • Écrire des règles de détection par sous-système et les comparer aux labels (vérité terrain).
  • Mesurer un taux de détection / faux positifs.
graph LR
 subgraph "Génération (modèle AegisSat)"
 GEN[generate_sat_telemetry.py]
 TEL[/telemetry.csv/]
 CMD[/commands.log/]
 LBL[/labels.csv
corrigé/] end subgraph "Ingestion" PG[(PostgreSQL
sat_telemetry)] end subgraph "Visualisation" STAT[Zone 1
Stat panels
triage par sous-système] TS[Zone 2
Time series
métriques continues] STL[Zone 3
State timeline
booléens] end subgraph "Détection" RULES[7 règles par attaque] MATRIX[Matrice détection
vs labels] end GEN --> TEL GEN --> CMD GEN --> LBL TEL --> PG PG --> STAT PG --> TS PG --> STL STAT --> RULES TS --> RULES STL --> RULES RULES --> MATRIX LBL --> MATRIX classDef gen fill:#ffcc99,stroke:#333,stroke-width:2px classDef store fill:#99ccff,stroke:#333,stroke-width:2px classDef viz fill:#99ff99,stroke:#333,stroke-width:2px classDef det fill:#ffccff,stroke:#333,stroke-width:2px class GEN,TEL,CMD,LBL gen class PG store class STAT,TS,STL viz class RULES,MATRIX det

Sous-systèmeRôleMétriques de télémétrie
OBCOrdinateur de bordcpu_pct, mem_pct, tasks
ADCSContrôle d’attitudemag_x/y/z, magnetorquer_on, attitude_err_deg
EPSÉnergiebattery_v, solar_w, bus_current_a
TPLThermiquetemp_c, target_temp_c, heater_on
COMCommunicationsrx_pkts, tx_pkts, link_up, rssi_dbm
WIFI/RFÉmission RFrf_power_dbm, tx_duty
PI_PAYLOADCharge utile (caméra/Pi)cam_state, proc_count, upload_bytes

Catalogue d’attaques (réel) → signature attendue

Section intitulée « Catalogue d’attaques (réel) → signature attendue »
AttaqueCibleSignature télémétrie à détecterSeuil simple
HeaterUpTPLtarget_temp_c poussé à ~50, temp_c qui s’emballetarget_temp_c > 35
CPUHigh / CPUHighTargetOBCcpu_pct saturé sur la fenêtrecpu_pct > 90 pendant > 5 min
CommDownCOMlink_up=0, rx_pkts→0 hors zone d’ombre orbitalelink_up == 0 hors éclipse
RFLeakageRFrf_power_dbm / tx_duty anormalement élevéstx_duty > 0.8
MagUpADCSmagnetorquer_on hors fenêtre, attitude_err_deg qui grimpemagnetorquer_on == 1 sans cmd ADCS
CamUpatkPI_PAYLOADcam_state changé sans commande planifiéecam_state mismatch cmd
malUPPI_PAYLOADproc_count ↑, upload_bytes anormalupload_bytes > baseline×3

Format de commande (repris d’AegisSat commands.yml)

Section intitulée « Format de commande (repris d’AegisSat commands.yml) »
<SOUS-SYSTÈME> <VERBE> <PARAM> <valeur>
ex. TPL SET TPL_SET_TARGET_TEMPERATURE 50
ADCS ON ADCS_MAGNETORQUER_ENABLE ON
PI_PAYLOAD SET PI_PAYLOAD_CAMERA 0

Chaque attaque a une occurrence (minute de simulation) et une durée — exactement le modèle AegisSat (occurrence/duration, valeurs multiples séparées par virgules).


Trois fichiers, bannière fiction en tête, horodatage ISO 8601 UTC :

FichierFormatPour
telemetry.csvts, subsystem, metric, valuevous (à analyser)
commands.log<ts> <commande AegisSat> (nominales + injectées)vous
labels.csvattack, subsystem, start_ts, end_tsformateur (corrigé)
ts,subsystem,metric,value
2026-06-17T08:00:00Z,TPL,temp_c,18.4
2026-06-17T08:00:00Z,TPL,target_temp_c,20.0
2026-06-17T08:00:00Z,TPL,heater_on,0
2026-06-17T08:00:00Z,OBC,cpu_pct,27.3
2026-06-17T08:00:00Z,OBC,mem_pct,42.1
2026-06-17T08:00:00Z,EPS,battery_v,7.45
2026-06-17T08:00:00Z,COM,link_up,1
2026-06-17T08:00:00Z,COM,rssi_dbm,-87
...
2026-06-17T08:35:00Z,TPL,target_temp_c,50.0 <-- début HeaterUp
2026-06-17T08:35:00Z,TPL,heater_on,1
2026-06-17T08:36:00Z,TPL,temp_c,22.8
2026-06-17T08:40:00Z,TPL,temp_c,38.5 <-- pic anormal
2026-06-17T08:59:00Z,TPL,target_temp_c,20.0 <-- fin HeaterUp
2026-06-17T08:00:00Z TPL SET TPL_SET_TARGET_TEMPERATURE 20 # planifiée
2026-06-17T08:35:00Z TPL SET TPL_SET_TARGET_TEMPERATURE 50 # ⚠️ INJECTÉE
2026-06-17T09:20:00Z OBC SET OBC_TASK_LOAD 95 # ⚠️ INJECTÉE (CPUHigh)
2026-06-17T10:00:00Z COM SET COM_LINK_STATE 0 # ⚠️ INJECTÉE (CommDown)
2026-06-17T08:59:00Z TPL SET TPL_SET_TARGET_TEMPERATURE 20 # restauration auto

Exemple — labels.csv (corrigé prof — pas distribué)

Section intitulée « Exemple — labels.csv (corrigé prof — pas distribué) »
attack,subsystem,start_ts,end_ts
HeaterUp,TPL,2026-06-17T08:35:00Z,2026-06-17T09:00:00Z
CPUHigh,OBC,2026-06-17T09:20:00Z,2026-06-17T09:35:00Z
CommDown,COM,2026-06-17T10:00:00Z,2026-06-17T10:20:00Z
MagUp,ADCS,2026-06-17T10:45:00Z,2026-06-17T11:15:00Z
RFLeakage,RF,2026-06-17T11:25:00Z,2026-06-17T11:35:00Z

Le générateur est fourni, prêt à l’emploi, dans le repo de données : kds-formation/datasatellite/.

satellite/
├── Makefile
├── config.yaml # sous-systèmes + fenêtres d'attaque + seed
├── generate_sat_telemetry.py
├── ingest.sql # crée la table sat_telemetry + COPY
├── README.md
└── out/ # généré (gitignoré)
Fenêtre de terminal
git clone https://gitlab.com/kds-formation/data.git
cd data/satellite
make install # pip install pyyaml
make generate SEED=2026-A # → out/telemetry.csv, commands.log, labels.csv
make check # sanity-check
ls -la out/
# -rw-r--r-- telemetry.csv ~5 MB, ~80 000 lignes
# -rw-r--r-- commands.log ~120 lignes
# -rw-r--r-- labels.csv ~6 lignes

out/ est gitignoré : chaque promo régénère ses données avec son propre SEED (anti-recyclage). Le labels.csv (corrigé) reste côté formateur.

seed_promo: 2026-A
duration_minutes: 240
attacks:
- { name: HeaterUp, occurrence: 35, duration: 25 }
- { name: CPUHigh, occurrence: 80, duration: 15 }
- { name: CommDown, occurrence: 120, duration: 20 }
- { name: MagUp, occurrence: 165, duration: 30 }
- { name: RFLeakage, occurrence: 205, duration: 10 }
# Attaques valides : HeaterUp, CPUHigh, CPUHighTarget, CommDown, MagUp, RFLeakage, CamUpatk, malUP

Fenêtre de terminal
cd ~/data/satellite
make generate SEED=2026-A
# [+] Generated 80123 telemetry rows
# [+] Generated 124 commands (5 injected)
# [+] Wrote labels.csv (5 attacks)
Fenêtre de terminal
make ingest
# Crée la table sat_telemetry(ts, subsystem, metric, value) + index
# COPY de out/telemetry.csv → 80123 lignes

Vérification :

Fenêtre de terminal
docker exec -it postgres-af psql -U taaf_admin -d base_af -c \
"SELECT subsystem, count(*) FROM sat_telemetry GROUP BY subsystem ORDER BY 1;"

Sortie attendue :

subsystem | count
-----------+-------
ADCS | 14400
COM | 12000
EPS | 9600
OBC | 9600
PI_PAYLOAD| 7200
RF | 4800
TPL | 14400

Dans Grafana → ConfigurationData sourcesPostgreSQL base_afSave & test → ✅ green.


🎯 Cible : un dashboard où l’anomalie saute aux yeux sans lire les chiffres. On vise 3 zones.

SELECT ts AS "time", value, metric
FROM sat_telemetry
WHERE subsystem = 'TPL' AND $__timeFilter(ts)
ORDER BY ts, metric;

La colonne metric devient le nom de série dans Grafana.

6.2 — Zone 1 — Triage (rangée de panneaux Stat)

Section intitulée « 6.2 — Zone 1 — Triage (rangée de panneaux Stat) »

Un panneau par sous-système (7 panneaux), en haut du dashboard.

PanelSous-systèmeRequête valueSeuils
TPL HealthTPLmax(value) WHERE metric='temp_c'vert < 30, jaune 30-40, rouge > 40
OBC HealthOBCmax(value) WHERE metric='cpu_pct'vert < 70, jaune 70-90, rouge > 90
ADCS HealthADCSmax(value) WHERE metric='attitude_err_deg'vert < 2, jaune 2-5, rouge > 5
COM HealthCOMmin(value) WHERE metric='link_up'vert = 1, rouge = 0
EPS HealthEPSmin(value) WHERE metric='battery_v'vert > 7, rouge < 6
RF HealthRFmax(value) WHERE metric='tx_duty'vert < 0.5, jaune 0.5-0.8, rouge > 0.8
PI HealthPI_PAYLOADmax(value) WHERE metric='upload_bytes'seuil dynamique vs baseline

Color mode = Background → la tuile devient rouge dès qu’un sous-système est attaqué.

6.3 — Zone 2 — Métriques continues (Time series)

Section intitulée « 6.3 — Zone 2 — Métriques continues (Time series) »

Pour chaque sous-système, panneau avec :

  • Toutes les métriques numériques de ce sous-système.
  • Unité par panneau (°C pour TPL, % pour OBC, dBm pour COM RSSI).
  • Threshold en mode Area → la partie anormale de la courbe se remplit en rouge.
  • Légende en tableau avec Last + Max (lire la valeur du pic sans survoler).

6.4 — Zone 3 — États booléens (State timeline)

Section intitulée « 6.4 — Zone 3 — États booléens (State timeline) »
State timelineMétriqueValue mapping
Link statusCOM.link_up0 → DOWN (rouge), 1 → UP (vert)
MagnetorquerADCS.magnetorquer_on0 → OFF (gris), 1 → ON (orange)
HeaterTPL.heater_on0 → OFF (gris), 1 → ON (orange)
CameraPI_PAYLOAD.cam_state0 → OFF, 1 → ON (couleur selon contexte)

→ Des bandes vert/rouge — bien plus lisible qu’une ligne plate noyée parmi d’autres échelles.

📸 Capture obligatoire #1 : vue complète du dashboard avec les 3 zones. 📸 Capture obligatoire #2 : zoom sur la fenêtre d’attaque HeaterUp (Zone 2 TPL avec aire rouge). 📸 Capture obligatoire #3 : Zone 3 montrant link_up qui passe DOWN pendant CommDown.


Créez 7 alertes Grafana, une par attaque. Exemple pour HeaterUp :

-- Alert query (datasource: PostgreSQL base_af)
SELECT MAX(value) AS thermal_target
FROM sat_telemetry
WHERE subsystem = 'TPL' AND metric = 'target_temp_c'
AND ts > NOW() - INTERVAL '5 minutes';

Alert condition : WHEN last() OF query(A) IS ABOVE 35

Notification policy → discord-soc-critical (héritée du TP4 Alerting).

AttaqueMétriqueConditionDurée miniNiveau
HeaterUptarget_temp_c> 352 mincritical
CPUHighcpu_pct> 905 minhigh
CommDownlink_up== 0 hors éclipse3 mincritical
MagUpmagnetorquer_on== 1 sans cmd planifiée1 minhigh
RFLeakagetx_duty> 0.82 minhigh
CamUpatkcam_statemismatch dernière cmd1 minmedium
malUPupload_bytes> baseline × 35 minmedium

Pour MagUp et CamUpatk, le seuil seul ne suffit pas : il faut comparer l’état télémétrie avec la dernière commande planifiée.

-- Détecter magnetorquer_on=1 sans commande ADCS récente
WITH last_cmd AS (
SELECT MAX(ts) AS cmd_ts
FROM sat_commands
WHERE command LIKE 'ADCS%MAGNETORQUER%'
)
SELECT t.ts, t.value
FROM sat_telemetry t, last_cmd
WHERE t.subsystem = 'ADCS' AND t.metric = 'magnetorquer_on'
AND t.value = 1
AND (last_cmd.cmd_ts IS NULL OR t.ts > last_cmd.cmd_ts + INTERVAL '5 minutes');

→ Cette règle attrape MagUp sans déclencher sur les activations légitimes.


8.1 — Récupérer le labels.csv (fourni par le prof à la fin du TP)

Section intitulée « 8.1 — Récupérer le labels.csv (fourni par le prof à la fin du TP) »
Fenêtre de terminal
# Le formateur vous fournit labels.csv via Moodle après votre rendu
docker cp labels.csv postgres-af:/tmp/
docker exec -it postgres-af psql -U taaf_admin -d base_af -c \
"COPY sat_labels FROM '/tmp/labels.csv' CSV HEADER;"
WITH detections AS (
SELECT 'HeaterUp' AS attack, MIN(ts) AS det_ts FROM sat_telemetry
WHERE subsystem='TPL' AND metric='target_temp_c' AND value > 35
UNION ALL
SELECT 'CPUHigh', MIN(ts) FROM sat_telemetry
WHERE subsystem='OBC' AND metric='cpu_pct' AND value > 90
-- ... (autres règles)
)
SELECT l.attack, l.start_ts, l.end_ts,
d.det_ts,
CASE
WHEN d.det_ts BETWEEN l.start_ts AND l.end_ts THEN 'DETECTED'
WHEN d.det_ts > l.end_ts THEN 'LATE'
WHEN d.det_ts IS NULL THEN 'MISSED'
END AS status
FROM sat_labels l
LEFT JOIN detections d ON l.attack = d.attack;

Résultat attendu (en TP réussi) :

attack | start_ts | end_ts | det_ts | status
-----------+----------------------+----------------------+----------------------+----------
HeaterUp | 2026-06-17 08:35:00 | 2026-06-17 09:00:00 | 2026-06-17 08:36:00 | DETECTED
CPUHigh | 2026-06-17 09:20:00 | 2026-06-17 09:35:00 | 2026-06-17 09:24:00 | DETECTED
CommDown | 2026-06-17 10:00:00 | 2026-06-17 10:20:00 | 2026-06-17 10:03:00 | DETECTED
MagUp | 2026-06-17 10:45:00 | 2026-06-17 11:15:00 | 2026-06-17 10:46:00 | DETECTED
RFLeakage | 2026-06-17 11:25:00 | 2026-06-17 11:35:00 | 2026-06-17 11:27:00 | DETECTED

Taux de détection = 5/5 = 100 %.

-- Détections en dehors des fenêtres attendues
SELECT d.attack, d.det_ts
FROM detections d
LEFT JOIN sat_labels l ON d.attack = l.attack
AND d.det_ts BETWEEN l.start_ts AND l.end_ts
WHERE l.attack IS NULL;

→ Cible : 0 faux positif. Si vous en avez, ajustez les seuils (durée mini, hystérésis).

📸 Capture obligatoire #4 : table comparant détections vs labels.


  1. Dataset généré (SEED=2026-A) — preuve : sortie de make check.
  2. Dump SQL de la table sat_telemetry (taille + count par sous-système).
  3. Dashboard Grafana exporté en JSON (File → Share → Export) + 3 captures (cf. § 6).
  4. 7 règles d’alerte Grafana provisionnées YAML dans le repo monitoring/grafana/provisioning/alerting/.
  5. Matrice de détection (markdown ou PDF) avec :
    • Taux de détection par sous-système
    • Taux de faux positifs
    • Détections précoces (détection avant fin de fenêtre)
  6. Paragraphe d’analyse : 3 attaques les plus discrètes, comment les seuils devraient évoluer pour ne plus les rater.
CritèrePondération
Génération + ingestion réussies (table peuplée)10 %
Dashboard 3 zones — Zone 1 triage fonctionnelle15 %
Dashboard 3 zones — Zone 2 time series avec seuils area15 %
Dashboard 3 zones — Zone 3 state timelines avec value mappings10 %
7 règles d’alerte provisionnées15 %
Matrice de détection vs labels (>= 4/5 attaques détectées)20 %
Paragraphe d’analyse + propositions d’amélioration15 %

Format de rendu : PDF nom-prenom-promo.pdf sur Moodle + JSON dashboard.


  1. Triage : selon SEED=2026-A, TPL passe rouge en premier à 08:35Z (HeaterUp), puis OBC à 09:20Z (CPUHigh), puis COM à 10:00Z (CommDown), puis ADCS à 10:45Z (MagUp), puis RF à 11:25Z (RFLeakage).

  2. Panneau TPL : zone rouge à partir de 08:36Z (1 min après injection), temp_c.max ≈ 38-42 °C selon le bruit du générateur.

  3. CommDown vs éclipse : croiser avec commands.log — si une commande COM SET COM_LINK_STATE 0 apparaît dans la même minute, c’est une attaque ; sinon, et si c’est dans une fenêtre d’éclipse calculée (typiquement passages au-dessus de la zone d’ombre Terre), c’est légitime. La règle robuste consulte un calendrier d’éclipses orbitales.

  4. State timeline lisibilité : link_up est un booléen — sur une courbe normalisée 0-1 mélangée à rssi_dbm (-100 dB) et rx_pkts (100+), la ligne 0/1 disparaît. Le state timeline lui dédie une bande horizontale colorée indépendante des autres axes. Autre booléen à traiter pareil : magnetorquer_on, heater_on, cam_state.

  5. MagUp vs CPUHigh : MagUp est plus discret. magnetorquer_on=1 est une valeur légitime dans certaines fenêtres orbitales (correction d’attitude). Pour ne pas avoir de FP, il faut corréler avec commands.log (cf. § 7.3). CPUHigh est plus brutal : cpu_pct > 90 n’est presque jamais légitime sur ce satellite.

  6. Durcir seuil thermique : (a) durée mini (> 2 min) pour ignorer les pics transitoires ; (b) hystérésis : alerte si > 35 pendant 2 min, désactive si < 30 pendant 1 min ; (c) delta : dT/dt > 5°C/min (vitesse anormale, pas niveau seul).

  7. Corrélation pic ↔ commande : si pic temp_c à 08:36Z corrélé avec TPL SET TPL_SET_TARGET_TEMPERATURE 50 à 08:35Z, on a la preuve que c’est une commande injectée. Sans corrélation, le seuil seul ne dit pas qui a déclenché → impossibilité d’attribuer.

  8. Complémentarité Module 2 : la couche réseau/crypto (Module 2 — downgrade PQC) protège la confidentialité du transport des commandes. La couche commande/télémétrie (ce TP) protège l’intégrité opérationnelle : même si le transport est intercepté, une commande qui contredit le plan thermique doit lever une alarme. Les deux sont nécessaires : sans crypto, l’attaquant lit toutes les commandes ; sans détection métier, il peut en injecter sans être vu.

  9. Règle multi-signaux : exemple — MagUp + attitude_err_deg > 5° + commands.log sans cmd ADCS récente. Un seul des 3 ne suffit pas, les 3 ensemble = quasi-certitude. Robustesse aux FP.

  10. Taux de détection 5/5 = 100 %. FP attendus : 0 à 2 selon le tuning. À ajuster : la règle RFLeakage avec seuil 0.8 peut tirer sur des pointes légitimes — passer à durée mini 2 min ou moyenne mobile.

  • Tout en courbes (link_up mélangé aux autres métriques) → -10 % (manque de pédagogie visuelle).
  • Seuils sans durée mini → FP explosent dans la matrice → -10 %.
  • MagUp détecté juste sur magnetorquer_on=1 sans corrélation cmd → 2-3 FP attendus → -5 % et discuter.
  • Matrice de détection incomplète (pas de status DETECTED/LATE/MISSED) → -10 %.
  • Pas de paragraphe d’analyse → -10 %.

La cohérence est claire : à 10:14:30Z, l’attaquant downgrade le KEM (Module 2). À 14:23Z, YAOGAN-35A survole. Si vous complétez votre dashboard avec un panel “Survol satellite” (zone shading sur la timeline pendant les passages SIGINT), vous voyez l’opportunité d’attaque, l’attaque réseau (Module 2), et la conséquence sur la télémétrie (ce TP) — d’un seul coup d’œil.

Vos 7 règles d’alerte peuvent être converties en règles Sigma (logsource aegissat, custom) et intégrées au repo taaf-detections du TP1 SIEM. Bénéfice : la couverture ATT&CK s’enrichit de techniques spatiales (T1600 Weaken Encryption, T1565 Data Manipulation).


⚠️ À ne pas reproduire : ce testbed ne contient pas de datasets/*.csv, de PCAP, ni de simulate_downgrade_attack.py. C’est un framework HIL. On en reprend le modèle, on génère nos données.