Merge remote-tracking branch 'origin/dev' into docs/livrables-ec03-ec06
# Conflicts: # docs/architecture/00-vue-ensemble.md # docs/architecture/README.md
This commit is contained in:
@@ -14,3 +14,4 @@
|
||||
| [0005](adr/0005-modele-prediction-lightgbm.md) | LightGBM pour la prédiction de consommation, un modèle global |
|
||||
| [0006](adr/0006-moteur-de-regles-dans-le-backend.md) | Le moteur de règles de recommandation vit dans le backend, pas dans `ml/` |
|
||||
| [0007](adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | Terminaison TLS par un reverse proxy Nginx, en Docker Compose |
|
||||
| [0008](adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement |
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# 0008 - Airflow exécute le code du backend en sous-processus
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-21
|
||||
|
||||
## Contexte
|
||||
|
||||
L'issue #116 demande un DAG d'alertes. Ce qu'il a à ordonnancer existe déjà et n'est pas à
|
||||
réécrire : `AlertService.detect()` et ses cinq règles (#104), puis le moteur de recommandations
|
||||
(#38). Les deux vivent dans `apps/backend/app/`, et
|
||||
l'[ADR 0006](0006-moteur-de-regles-dans-le-backend.md) a précisément décidé qu'ils y restent parce
|
||||
qu'ils s'appuient sur les repositories ORM de l'API plutôt que sur du SQL brut. Les deux
|
||||
sont décrits par la documentation comme « lancés à la main ».
|
||||
|
||||
L'image Airflow livrée par #115 ne porte que `ml/`, dans un environnement `uv` distinct
|
||||
(`/opt/ml/.venv`, Python 3.14) de celui d'Airflow lui-même (Python 3.12, contraint par
|
||||
apache-airflow 2.10). Les DAGs `ml_train` et `ml_score` shellent vers cet environnement. Rien
|
||||
d'équivalent n'existe pour `apps/backend` : un `BashOperator` sur
|
||||
`python -m app.detection.internal_alerts` échouerait en `ModuleNotFoundError`.
|
||||
|
||||
## Décision
|
||||
|
||||
**L'image Airflow porte un troisième environnement, `/opt/backend/.venv`**, construit depuis le
|
||||
`pyproject.toml`, le `uv.lock` et le paquet `app/` du backend. Le DAG `alertes` shelle vers lui
|
||||
exactement comme `ml_score` shelle vers `/opt/ml/.venv`.
|
||||
|
||||
Trois raisons :
|
||||
|
||||
- **Le patron existe et vient d'être revu.** #115 a posé `BashOperator` + `uv run --no-sync` +
|
||||
`env -u VIRTUAL_ENV`, avec les tests d'intégrité qui le verrouillent. Introduire une seconde
|
||||
forme d'appel dans le même dossier `dags/` coûterait plus cher à lire qu'un second environnement
|
||||
dans le même `Dockerfile`.
|
||||
- **Aucune surface réseau n'est ajoutée.** La détection n'a pas de route HTTP, contrairement à la
|
||||
génération de recommandations (`POST /recommendations/generate`, rôle `admin`). En créer une pour
|
||||
qu'Airflow l'appelle donnerait à l'ordonnanceur un compte administrateur de l'API, en plus des
|
||||
identifiants PostgreSQL complets qu'il détient déjà, et ferait dépendre la production d'alertes
|
||||
de la disponibilité du conteneur `backend`.
|
||||
- **La logique reste où l'ADR 0006 l'a mise.** Le DAG n'apprend rien du domaine : ni les seuils, ni
|
||||
les cinq règles, ni les clés d'idempotence. Il ne sait que l'heure à laquelle appeler.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- **Airflow reçoit une `APP_SECRET_KEY` délibérément distincte de celle de l'API.** La
|
||||
configuration du backend refuse de se construire sans elle (`app/core/config.py`), et
|
||||
`internal_alerts.main()` appelle `get_settings()` avant toute requête pour échouer tôt. Mais la
|
||||
détection ne signe ni ne vérifie aucun jeton, et Airflow permet d'exécuter du code arbitraire
|
||||
depuis son interface : un Airflow compromis ne doit pas livrer la clé de signature des JWT. D'où
|
||||
`AIRFLOW_APP_SECRET_KEY`, avec sa propre garde dans `airflow-init`.
|
||||
- **`DATABASE_URL`, en dialecte asyncpg, rejoint `ML_DATABASE_URL`** dans l'environnement du
|
||||
conteneur. Le cantonnement des rôles PostgreSQL reste la dette de
|
||||
l'[ADR 0003](0003-autorisation-rbac-a-trois-roles.md), et cette décision l'alourdit d'un
|
||||
consommateur de plus.
|
||||
- **La CI Airflow se déclenche sur les changements du backend.** L'image le `COPY` : sans
|
||||
`apps/backend/app/**`, `pyproject.toml` et `uv.lock` dans les déclencheurs du workflow, une
|
||||
dépendance modifiée casserait la construction sans que rien ne le signale avant le déploiement.
|
||||
En contrepartie, l'image grossit de ce que pèsent SQLAlchemy, asyncpg et pandas.
|
||||
- **Aucune variable ne départage les deux environnements, et c'est voulu.** `uv` place par défaut
|
||||
le venv d'un projet dans `<projet>/.venv` : `cd /opt/ml` ou `cd /opt/backend` suffit à choisir le
|
||||
bon. L'image ne pose donc plus de `UV_PROJECT_ENVIRONMENT` global, hérité de #115 : il vaudrait
|
||||
pour les deux projets, et `uv run` dans l'un résoudrait le venv de l'autre. Le symptôme n'est pas
|
||||
une construction ratée mais un `ModuleNotFoundError` à la première tâche, d'où la vérification
|
||||
d'import sans réseau que la CI fait maintenant sur chacun des deux.
|
||||
- Airflow lui-même reste étranger au domaine : ni LightGBM, ni SQLAlchemy, ni FastAPI n'entrent
|
||||
dans son interpréteur. C'est la propriété que #115 avait établie, et elle tient toujours.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Route HTTP `POST /alerts/detect` réservée `admin`, appelée par le DAG.** L'image ne bougeait
|
||||
pas, mais Airflow détenait alors un compte administrateur de l'API, la détection devenait
|
||||
tributaire du conteneur `backend`, et l'API gagnait une route d'écriture dont aucun client
|
||||
humain n'a l'usage. À rouvrir si un jour un tiers doit déclencher la détection.
|
||||
- **`DockerOperator` lançant l'image du backend.** Demande la socket Docker de l'hôte dans le
|
||||
conteneur Airflow, c'est-à-dire un équivalent root sur la machine, pour un service qui permet
|
||||
déjà d'exécuter du code depuis son interface. Le provider n'est d'ailleurs pas installé.
|
||||
- **Réécrire les cinq règles en SQL dans le DAG.** Contredit frontalement l'ADR 0006, duplique le
|
||||
domaine, et fait diverger les deux copies au premier changement de seuil.
|
||||
- **Monter `apps/backend` en volume plutôt que le copier.** L'environnement ne serait plus figé à
|
||||
la construction, `uv` resynchroniserait au premier lancement, et la CI ne prouverait plus rien
|
||||
de ce qui tourne réellement.
|
||||
@@ -70,9 +70,10 @@ Le lien `front -.-> api` reste en pointillé : le frontend appelle bien une API,
|
||||
intercepteur répond à sa place tant que les endpoints n'existent pas. Voir
|
||||
[30-frontend.md](30-frontend.md).
|
||||
|
||||
Le lien `airflow --> db` est maintenant en trait plein : deux DAGs orchestrent l'entraînement et
|
||||
le scoring du modèle ML (issue #115), cf. plus bas et [20-backend.md](20-backend.md). Le reste du
|
||||
périmètre Airflow envisagé (ingestion, issues #15/#16) reste en pointillé, non construit.
|
||||
Le lien `airflow --> db` est maintenant en trait plein : trois DAGs tournent, deux pour
|
||||
l'entraînement et le scoring du modèle ML (issue #115), un pour la détection d'alertes et la
|
||||
génération des recommandations (issue #116), cf. plus bas et [20-backend.md](20-backend.md). Le
|
||||
reste du périmètre Airflow envisagé (ingestion, issues #15/#16) reste en pointillé, non construit.
|
||||
|
||||
Le lien `prom -.-> api` de même : l'API expose bien `/metrics` au format Prometheus, mais aucun
|
||||
collecteur ne vient le lire.
|
||||
@@ -87,15 +88,15 @@ collecteur ne vient le lire.
|
||||
| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`, orchestré par Airflow (`ml_train`/`ml_score`). Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Surveillance de dérive (EC06, #44/#45) pas encore construite |
|
||||
| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Deux DAGs (`ml_train` manuel, `ml_score` `@hourly`) orchestrent le pipeline ML existant en sous-processus `uv run` (issue #115). L'ingestion (issues #15/#16) n'a pas encore de DAG |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Trois DAGs en sous-processus `uv run` : `ml_train` manuel et `ml_score` `@hourly` pour le pipeline ML (issue #115), `alertes` à `15 * * * *` pour la détection et les recommandations (issue #116, [ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)). L'ingestion (issues #15/#16) n'a pas encore de DAG |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 5 workflows, 16 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow. Détail dans [50-cicd.md](50-cicd.md). **Aucun job de déploiement** (#21) |
|
||||
|
||||
## Flux bout en bout
|
||||
|
||||
Statut : `En cours`. **Le chemin de lecture tourne** : base, API et frontend. **Le chemin
|
||||
d'ingestion dessiné ci-dessous n'existe pas** : les deux DAGs livrés (`ml_train`, `ml_score`,
|
||||
issue #115) orchestrent le pipeline ML, pas l'ingestion, qui reste lancée à la main par les
|
||||
scripts d'import (issues #15 et #16).
|
||||
d'ingestion dessiné ci-dessous n'existe pas** : les trois DAGs livrés (`ml_train`, `ml_score`,
|
||||
issue #115 ; `alertes`, issue #116) orchestrent le pipeline ML et la détection d'alertes, pas
|
||||
l'ingestion, qui reste lancée à la main par les scripts d'import (issues #15 et #16).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
||||
@@ -51,7 +51,7 @@ Trois pièges sont documentés en tête du `docker-compose.yml`, ils ne se devin
|
||||
- `LocalExecutor` exécute les tâches comme sous-processus du **scheduler**, jamais du webserver :
|
||||
c'est le scheduler qui a besoin du volume `airflow_ml_state` (modèle, magasin MLflow).
|
||||
|
||||
### Airflow (`ml_train`/`ml_score`, issue #115)
|
||||
### Airflow (issues #115 et #116)
|
||||
|
||||
Trois services, `docker compose profiles` non utilisés (démarrage explicite via `make
|
||||
airflow-up`, pas dans `make dev`) :
|
||||
@@ -60,13 +60,37 @@ airflow-up`, pas dans `make dev`) :
|
||||
|---|---|---|
|
||||
| `airflow-init` | Migre la base de métadonnées, crée le compte admin | Conteneur jetable (`restart: "no"`), ne redémarre jamais. `webserver`/`scheduler` attendent qu'il se termine avec succès |
|
||||
| `airflow-webserver` | UI, port `8080` | `LocalExecutor` : n'exécute aucune tâche lui-même |
|
||||
| `airflow-scheduler` | Planifie et **exécute** les tâches (`LocalExecutor`) | Les DAGs y tournent en sous-processus (`uv run --frozen --no-dev python -m enervision_ml...`), c'est lui qui a besoin du volume `airflow_ml_state` |
|
||||
| `airflow-scheduler` | Planifie et **exécute** les tâches (`LocalExecutor`) | Les DAGs y tournent en sous-processus (`uv run --no-sync python -m ...`), c'est lui qui a besoin du volume `airflow_ml_state` |
|
||||
|
||||
Construits depuis `etl/airflow/Dockerfile`, contexte `.` (racine du repo, pas `etl/airflow/`) :
|
||||
l'image doit pouvoir `COPY` `ml/pyproject.toml`/`ml/uv.lock`/`ml/enervision_ml` pour se
|
||||
synchroniser un second environnement Python **3.14** (`/opt/ml/.venv`, `uv sync --locked` à la
|
||||
construction), distinct du Python 3.12 qui fait tourner Airflow lui-même. Les DAGs shellent vers
|
||||
ce venv plutôt que d'importer LightGBM/MLflow dans le process Airflow.
|
||||
l'image doit pouvoir `COPY` les sources de `ml/` **et** de `apps/backend/` pour se synchroniser
|
||||
deux environnements Python **3.14** (`/opt/ml/.venv` et `/opt/backend/.venv`, `uv sync --locked` à
|
||||
la construction), distincts du Python 3.12 qui fait tourner Airflow lui-même. Les DAGs shellent
|
||||
vers ces venvs plutôt que d'importer LightGBM, MLflow ou SQLAlchemy dans le process Airflow.
|
||||
Le choix et ses contreparties sont dans
|
||||
l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md).
|
||||
|
||||
| DAG | Planification | Ce qu'il lance, et où |
|
||||
|---|---|---|
|
||||
| `ml_train` | manuelle | `enervision_ml.train`, dans `/opt/ml/.venv` |
|
||||
| `ml_score` | `0 * * * *` | `enervision_ml.score`, dans `/opt/ml/.venv` |
|
||||
| `alertes` | `15 * * * *` | `app.detection.internal_alerts` puis `app.cli generate-recommendations`, dans `/opt/backend/.venv` |
|
||||
|
||||
**Pourquoi `alertes` tourne à la quinzième minute.** Sa règle `anomaly` compare une lecture à la
|
||||
`prediction` du même instant, que `ml_score` écrit à l'heure pile. Le décalage laisse le scoring
|
||||
finir. Aucune dépendance n'est déclarée entre les deux DAGs pour autant, ni `ExternalTaskSensor` ni
|
||||
tâche greffée : quatre règles de détection sur cinq ne touchent pas au modèle, et un modèle jamais
|
||||
entraîné ne doit pas priver le parc de ses alertes. Le décalage est donc une convention et non une
|
||||
garantie : le plafond de `ml_score` est de 30 minutes, et un scoring qui déborde de `:15` prive
|
||||
`anomaly` de la `prediction` de l'heure, qu'elle ne retrouvera au passage suivant que si sa fenêtre
|
||||
la couvre encore. Les quatre autres règles ne s'en aperçoivent pas.
|
||||
|
||||
Ses deux tâches s'enchaînent en revanche (`recommendation.alert_id` est une clé étrangère `NOT
|
||||
NULL`), et toutes deux sont rejouables sans risque : l'idempotence est portée par la base,
|
||||
`uq_alert_source_reference` et `uq_recommendation_alert_rule`. Chacune a 2 tentatives, 2 minutes
|
||||
d'attente entre elles et un plafond de 5 minutes **par tentative** : au pire, reprises comprises,
|
||||
l'enchaînement occupe 38 minutes, ce qui le garde sous le pas horaire qu'un `max_active_runs=1`
|
||||
rend contraignant.
|
||||
|
||||
`airflow-init` s'appuie sur l'entrypoint de l'image (`_AIRFLOW_DB_MIGRATE`,
|
||||
`_AIRFLOW_WWW_USER_*`) plutôt que sur un script maison : l'entrypoint porte le code de sortie, une
|
||||
@@ -77,7 +101,15 @@ passe par l'environnement, jamais par `argv` (ni `ps`, ni `docker compose config
|
||||
Les variables `AIRFLOW_*` ne sont volontairement pas en `${VAR:?}` : Compose interpole le fichier
|
||||
entier avant de filtrer les services, une variable requise manquante casserait `make db-up`,
|
||||
`make dev`... pour tout poste dont le `.env` est antérieur. Elles valent `${VAR:-}` et c'est
|
||||
`airflow-init` qui refuse de démarrer (clé Fernet, clé Flask ou mot de passe vides).
|
||||
`airflow-init` qui refuse de démarrer (clé Fernet, clé Flask, mot de passe ou
|
||||
`AIRFLOW_APP_SECRET_KEY` vides).
|
||||
|
||||
Le conteneur reçoit deux variables du backend en plus de `ML_DATABASE_URL` : `DATABASE_URL`, en
|
||||
dialecte asyncpg, et `APP_SECRET_KEY`, alimentée par `AIRFLOW_APP_SECRET_KEY`. Cette dernière est
|
||||
**délibérément différente** de celle de l'API. La configuration du backend refuse de se construire
|
||||
sans clé, mais la détection ne signe ni ne vérifie aucun jeton : un Airflow compromis, qui permet
|
||||
déjà d'exécuter du code depuis son interface, ne doit pas livrer par-dessus la clé de signature
|
||||
des JWT.
|
||||
|
||||
**Pourquoi `ml_train` est manuel.** Réentraîner est coûteux et sa cadence n'est pas une décision
|
||||
prise. Surtout, `train.py` écrase le modèle sans comparer ses métriques à celles de l'ancien : un
|
||||
@@ -86,8 +118,9 @@ déclenchement reste humain. `ml_score`, lui, est planifié à l'heure, avec `ma
|
||||
(pas deux scorings simultanés dans `prediction`), 2 tentatives et un plafond de 30 minutes.
|
||||
|
||||
CI : `.github/workflows/airflow.yml` (Python 3.12 via `etl/airflow/.python-version`) lance lint et
|
||||
tests d'intégrité des DAGs, et construit l'image (elle `COPY` `ml/`, une modification de `ml/`
|
||||
peut donc la casser) avant de vérifier que le pipeline s'y importe sans réseau.
|
||||
tests d'intégrité des DAGs, et construit l'image (elle `COPY` `ml/` et `apps/backend/`, une
|
||||
modification de l'un ou de l'autre peut donc la casser, d'où leurs chemins dans les déclencheurs)
|
||||
avant de vérifier que les deux environnements s'y importent sans réseau.
|
||||
|
||||
Piège à connaître : sur un volume `pgdata` déjà peuplé (poste de dev existant plutôt que premier
|
||||
`make db-up`), `db/init/120-airflow-database.sql` ne se rejoue pas (PostgreSQL n'exécute
|
||||
|
||||
@@ -256,12 +256,19 @@ auraient pu comparer des lectures/choisir une prévision au hasard. `_detect_spi
|
||||
explicitement les paires de lectures qui partagent le même horodatage (deux `source` pour un seul
|
||||
instant réel, pas une variation).
|
||||
|
||||
La détection est un script lancé à la main, pas encore ordonnancé par Airflow (contrairement à
|
||||
`enervision_ml.score`, orchestré par le DAG `ml_score` depuis l'issue #115) : `uv run python -m app.detection.internal_alerts [--site-id ...] [--now ...]`, dans
|
||||
`apps/backend` puisque les règles s'appuient sur les repositories ORM de l'API plutôt que sur une
|
||||
connexion SQL directe (contrairement à `app/etl/historical_import.py`). Cette issue (#104)
|
||||
débloquait #38 (moteur de règles pour recommandations), dont la FK `alert_id` `NOT NULL` n'avait
|
||||
jusqu'ici rien à référencer côté `source="enervision"`.
|
||||
La détection s'exécute dans `apps/backend`, puisque les règles s'appuient sur les repositories ORM
|
||||
de l'API plutôt que sur une connexion SQL directe (contrairement à
|
||||
`app/etl/historical_import.py`) : `uv run python -m app.detection.internal_alerts [--site-id ...]
|
||||
[--now ...]`, ou `make detect-alerts`. Cette issue (#104) débloquait #38 (moteur de règles pour
|
||||
recommandations), dont la FK `alert_id` `NOT NULL` n'avait jusqu'ici rien à référencer côté
|
||||
`source="enervision"`.
|
||||
|
||||
Depuis l'issue #116, le lancement n'est plus manuel : le DAG Airflow `alertes` enchaîne cette
|
||||
détection et la génération des recommandations, toutes les heures à la quinzième minute. Airflow
|
||||
exécute le code du backend en sous-processus, dans son propre environnement, ce que décide
|
||||
l'[ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md) ; le détail de l'ordonnancement est
|
||||
dans [10-infra.md](10-infra.md). La ligne de commande reste le moyen de rejouer une fenêtre
|
||||
passée, ce que `--now` permet et que le DAG ne fait pas.
|
||||
|
||||
### `/health/ready`
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ décrivent les éléments prévus mais pas encore réalisés.
|
||||
|
||||
L'ingestion des **mesures** est implémentée pour les deux sources du MVP, le dataset CSV/JSON et
|
||||
l'API Mock. Celle des **alertes** de l'API Mock, `/alerts`, reste à faire : voir
|
||||
l'[ADR 0006](../adr/0006-moteur-de-regles-dans-le-backend.md). L'orchestration Airflow, les
|
||||
agrégats continus, la compression et la rétention restent des cibles.
|
||||
l'[ADR 0006](../adr/0006-moteur-de-regles-dans-le-backend.md). Les alertes `source='enervision'`,
|
||||
elles, sont produites par la détection interne, désormais ordonnancée par le DAG Airflow `alertes`
|
||||
(issue #116). L'orchestration de l'ingestion, les agrégats continus, la compression et la
|
||||
rétention restent des cibles.
|
||||
|
||||
## Trois emplacements, trois rôles
|
||||
|
||||
@@ -290,10 +292,10 @@ Les anomalies historiques décrites dans les JSON sont conservées dans `dataset
|
||||
Elles servent à l'analyse des données et ne sont pas considérées comme des alertes actuelles.
|
||||
|
||||
Les lignes de `recommendation` sont écrites par le moteur de règles du backend
|
||||
(`app/services/recommendation_rules.py`), déclenché par `POST /api/v1/recommendations/generate`
|
||||
ou par `make recommendations`, à partir des alertes déjà en base. Le couple
|
||||
`(alert_id, rule_reference)` est unique : rejouer le moteur sur les mêmes alertes n'ajoute aucune
|
||||
ligne.
|
||||
(`app/services/recommendation_rules.py`), déclenché par `POST /api/v1/recommendations/generate`,
|
||||
par `make recommendations`, ou par la seconde tâche du DAG `alertes`, à partir des alertes déjà en
|
||||
base. Le couple `(alert_id, rule_reference)` est unique : rejouer le moteur sur les mêmes alertes
|
||||
n'ajoute aucune ligne.
|
||||
|
||||
### Relations entre les tables
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ qu'une section de plus dans une autre vue devienne illisible. L'observabilité,
|
||||
toujours pas : `monitoring/` ne contient que des `.gitkeep`. Elle en sortira le jour où elle aura
|
||||
de la matière. Un fichier vide de plus n'aide personne.
|
||||
|
||||
L'orchestration Airflow, elle, en a depuis les issues #115 et #116 : trois DAGs, leur image et
|
||||
leurs contraintes sont décrits dans [10-infra.md](10-infra.md).
|
||||
|
||||
La sécurité applicative, elle, a désormais de la matière : la vue consolidée reste dans
|
||||
[00-vue-ensemble.md](00-vue-ensemble.md), le détail dans [20-backend.md](20-backend.md), la
|
||||
traçabilité OWASP dans [owasp-traceabilite.md](owasp-traceabilite.md), et les décisions dans les
|
||||
|
||||
@@ -61,7 +61,7 @@ le certificat servi reste auto-signé. Le chemin ACME est livré et documenté,
|
||||
| **API10 Unsafe Consumption of APIs** | **partiel, et spécifique à ce projet** | L'API Mock de l'école n'a aucune authentification, tourne en HTTP clair sur le réseau de l'école, et expose un endpoint mutatif à quiconque. Sa réponse est traitée comme une entrée hostile par `app/etl/mock_api_import.py`, son seul consommateur à ce jour : les quatre garde-fous attendus sont en place, voir la ligne correspondante plus haut. Reste ouvert : le plafond de taille s'applique après désérialisation de la réponse, borner le corps HTTP lui-même demanderait une lecture en flux ; et `APP_MOCK_API_BASE_URL` n'impose pas `https`, donc les identifiants Basic partiraient en clair sur une URL en `http`. La conséquence la plus sérieuse n'est pas la fausse alerte, c'est l'empoisonnement du jeu d'entraînement du modèle de prédiction. |
|
||||
| **A08 Software and Data Integrity Failures** | **partiel** | La CI vérifie le code mais n'analyse ni les dépendances ni les images. `.terraform.lock.hcl` reste ignoré par git, ce qui contredit une chaîne d'approvisionnement maîtrisée. |
|
||||
| **A10 Server-Side Request Forgery** | **sans objet aujourd'hui** | Aucune URL sortante n'est pilotée par une donnée utilisateur. Le jour où l'adresse d'une source devient un champ de configuration, il faudra une liste blanche de schémas et d'hôtes, sans suivi de redirection. |
|
||||
| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. Plus coûteuse depuis Airflow (#115) : ce service publie le port 8080, détient les identifiants Postgres complets (`ML_DATABASE_URL`, mêmes que le backend) et permet de déclencher l'exécution de code depuis son interface. Un compte Airflow compromis atteint donc toute la base, pas seulement `reading`/`site`. Le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv`. |
|
||||
| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. Plus coûteuse depuis Airflow (#115) : ce service publie le port 8080, détient les identifiants Postgres complets (`ML_DATABASE_URL`, mêmes que le backend) et permet de déclencher l'exécution de code depuis son interface. Un compte Airflow compromis atteint donc toute la base, pas seulement `reading`/`site`. Aggravée par #116 : le conteneur reçoit aussi `DATABASE_URL` et exécute le code du backend en sous-processus (ADR 0008). Atténuations en place : le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv` ; et l'`APP_SECRET_KEY` donnée à Airflow est distincte de celle de l'API, pour qu'une compromission ne livre pas la clé de signature des JWT. |
|
||||
| **Non-répudiation de l'audit** | **dette assumée** | Les déclencheurs arrêtent les accidents, pas un compte détenant `ALTER TABLE`. Voir ADR 0004. |
|
||||
|
||||
## Ce qu'il faut répondre, et ne pas répondre
|
||||
|
||||
Reference in New Issue
Block a user