docs: acte la terminaison TLS par l'ADR 0007 et met à jour les vues
L'ADR 0007 tranche le reverse proxy en Compose plutôt que l'ingress k3s, qui supposait un registre et des manifestes inexistants, et referme la première question ouverte de 10-infra.md. Les vues suivent : troisième topologie et ports 80/443 dans 10-infra.md, TLS, HSTS et CSP passent d'« Absent, et assumé » à « En place » dans la vue d'ensemble, la ligne API8 transport rejoint les points couverts de la traçabilité OWASP. Trois affirmations périmées disparaissent au passage : le compose a bien un service frontend, environment.ts ne pointe plus sur localhost:8000, et le Dockerfile du front n'est plus mono-étage sur une branche.
This commit is contained in:
@@ -46,6 +46,7 @@ flowchart TB
|
||||
navigateur["Navigateur"]
|
||||
|
||||
subgraph machine["Machine on-premise"]
|
||||
proxy["Reverse proxy Nginx<br/>:80 et :443"]
|
||||
front["Frontend Angular 22<br/>apps/frontend"]
|
||||
api["API FastAPI<br/>apps/backend"]
|
||||
db[("PostgreSQL 17<br/>TimescaleDB")]
|
||||
@@ -54,7 +55,9 @@ flowchart TB
|
||||
grafana["Grafana"]
|
||||
end
|
||||
|
||||
navigateur --> front
|
||||
navigateur --> proxy
|
||||
proxy --> front
|
||||
proxy --> api
|
||||
front -.-> api
|
||||
api --> db
|
||||
airflow -.-> db
|
||||
@@ -78,7 +81,7 @@ collecteur ne vient le lire.
|
||||
| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, authentification complète (garde de route, intercepteur de jeton), cinq services HTTP, graphiques Chart.js. `stats`/`alerts` sur fixtures, `predictions` branché sur l'API réelle |
|
||||
| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) |
|
||||
| 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`. Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../../ML-START.md). Automatisation (Airflow) et surveillance de dérive (EC06, #44/#45) pas encore construites |
|
||||
| Infra | Terraform, k3s single-node | `infra/terraform` | `En cours` | Module d'installation du cluster. Jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| 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` | `Cible` | Rien |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `Cible` | Rien |
|
||||
@@ -137,6 +140,11 @@ consolidée.
|
||||
jeton facultatif, sonde de disponibilité qui ne publie plus la version de TimescaleDB.
|
||||
- **CI backend bloquante** : format, lint, typage strict et tests avec seuil de couverture.
|
||||
- **Conteneur backend non-root**, déclaré dans `apps/backend/Dockerfile`.
|
||||
- **Terminaison TLS au frontal** : un reverse proxy Nginx est le seul service publié, il redirige
|
||||
80 vers 443, sert le SPA et l'API sous la même origine, pose **HSTS** et **CSP** que
|
||||
l'application refuse délibérément de poser, et ajoute une **limitation de débit au frontal**
|
||||
distincte de celle de l'application. Voir
|
||||
[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md).
|
||||
- **Côté infrastructure** : la clé SSH est marquée `sensitive`, le kubeconfig reste en `600/root`
|
||||
sur la machine cible et n'est lu que par `sudo`, `*.tfvars` est ignoré par git sauf les
|
||||
`.example`.
|
||||
@@ -150,13 +158,10 @@ consolidée.
|
||||
arrêteraient une application compromise. Même raison de report.
|
||||
- **Portée par site** dans l'autorisation : les rôles sont globaux, un opérateur du site A peut
|
||||
agir sur le site B. C'est la limite connue du modèle.
|
||||
- **TLS, HSTS et CSP** : ils appartiennent au terminateur TLS, qui n'existe pas encore.
|
||||
- **Limitation de débit au frontal** : celle de l'application protège les identifiants, pas
|
||||
l'infrastructure.
|
||||
- **Certificat reconnu** : aucun nom de domaine public ne résout vers la machine, donc le défi
|
||||
HTTP-01 de Let's Encrypt ne peut pas aboutir. Le certificat servi est auto-signé, le chemin ACME
|
||||
est livré et documenté mais pas exercé.
|
||||
- **Analyse de dépendances et de conteneurs** dans la CI, qui relève du chantier CI/CD.
|
||||
- **Le fichier `environment.ts` de production** pointe encore sur `http://localhost:8000` en HTTP
|
||||
simple : dans cet état, le cookie `Secure` ne sera pas posé. Voir
|
||||
[31-contrat-authentification.md](31-contrat-authentification.md).
|
||||
|
||||
## Décisions structurantes
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Infrastructure
|
||||
|
||||
Deux topologies coexistent et ne servent pas la même chose. Ce document dit laquelle vaut dans
|
||||
quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre les deux.
|
||||
Trois topologies coexistent et ne servent pas la même chose. Ce document dit laquelle vaut dans
|
||||
quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre elles.
|
||||
|
||||
| Topologie | Sert à | Statut |
|
||||
|---|---|---|
|
||||
| Docker Compose | Développer et recetter sur le poste | `Fait` |
|
||||
| k3s single-node | Déployer sur le serveur on-premise | `En cours` |
|
||||
| Docker Compose plus reverse proxy | Déployer sur la machine on-premise | `Fait` |
|
||||
| k3s single-node | Cible à terme | `En cours` |
|
||||
|
||||
## Poste de développement
|
||||
|
||||
@@ -48,7 +49,44 @@ Deux pièges sont documentés en tête du `docker-compose.yml`, ils ne se devine
|
||||
l'image, dont `timescaledb-tune`. Ajouter un fichier dans `db/init/` impose donc une ligne dans
|
||||
le compose. Voir [`db/README.md`](../../db/README.md).
|
||||
|
||||
## Cible de déploiement
|
||||
## Machine cible, exécution Docker
|
||||
|
||||
Statut : `Fait`. Défini par l'overlay `docker-compose.prod.yml`, appliqué par-dessus le
|
||||
`docker-compose.yml`. Écrit et validé sur le poste, **jamais encore lancé sur le serveur de
|
||||
l'école**. Décision et motifs dans l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
navigateur["Navigateur"]
|
||||
|
||||
subgraph machine["Machine on-premise"]
|
||||
proxy["service proxy<br/>nginx:1.28-alpine<br/>:80 et :443"]
|
||||
front["service frontend<br/>nginx statique :3000"]
|
||||
api["service backend<br/>uvicorn :8000"]
|
||||
db[("service db<br/>:5432")]
|
||||
mail["service mailpit"]
|
||||
end
|
||||
|
||||
navigateur -->|"HTTPS"| proxy
|
||||
proxy -->|"/"| front
|
||||
proxy -->|"/api/"| api
|
||||
api --> db
|
||||
api --> mail
|
||||
```
|
||||
|
||||
Le proxy est **le seul service à publier des ports** sur le réseau. Backend et frontend ne sont
|
||||
plus publiés du tout, la base et l'interface Mailpit sont ramenées sur `127.0.0.1`, donc joignables
|
||||
par tunnel SSH et pas autrement. Le détail du routage, les deux modes d'obtention du certificat et
|
||||
la commande de validation hors exécution sont dans [`infra/proxy/README.md`](../../infra/proxy/README.md).
|
||||
|
||||
Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent pas :
|
||||
|
||||
- Servir le SPA et l'API sous la même origine est ce qui rend le cookie `__Secure-ev_refresh`
|
||||
utilisable. Sans cela, `apiUrl: '/api/v1'` ne mène nulle part une fois en conteneur.
|
||||
- `APP_TRUST_PROXY_HEADERS` passe à vrai en même temps, sinon la limitation de débit par IP
|
||||
compte sur l'IP du proxy et devient globale.
|
||||
|
||||
## Cible à terme, k3s
|
||||
|
||||
Statut : `En cours`. Le module `infra/terraform/modules/k3s/` installe le cluster. Il n'a jamais
|
||||
été appliqué.
|
||||
@@ -104,6 +142,8 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
||||
| `*.tfvars` ignoré, `*.tfvars.example` versionné | Les tfvars portent l'adresse du serveur et le chemin de la clé | `.gitignore` |
|
||||
| Désinstallation gérée au `destroy` | `k3s-uninstall.sh` en `on_failure = continue` : un serveur injoignable ne bloque pas le `destroy` | `modules/k3s/main.tf` |
|
||||
| Deux racines, `dev` et `prod` | Séparation des états et des variables par environnement | `environments/` |
|
||||
| Terminaison TLS par un reverse proxy Nginx en Compose | L'ingress k3s supposait un registre et des manifestes qui n'existent pas, à quatre jours du rendu | `docker-compose.prod.yml`, [ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) |
|
||||
| Certificat auto-signé par défaut, chemin ACME câblé | Aucun domaine public ne résout vers la machine : le défi HTTP-01 ne peut pas aboutir | `scripts/tls-selfsigned.sh`, `infra/proxy/acme-deploy-hook.sh` |
|
||||
|
||||
## Ports et noms
|
||||
|
||||
@@ -112,12 +152,14 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
||||
| PostgreSQL, côté hôte | `5433` | Redirigé vers 5432 dans le conteneur. 5432 est souvent déjà pris |
|
||||
| PostgreSQL, côté réseau Compose | `db:5432` | Nom de service, utilisé par `DATABASE_URL` du service `backend` |
|
||||
| API | `8000` | Identique en conteneur et hors conteneur |
|
||||
| Frontend, `ng serve` | `4200` | Valeur par défaut d'`APP_CORS_ORIGINS`. Le compose n'a aucun service frontend |
|
||||
| Frontend, `ng serve` | `4200` | Boucle de développement. Valeur par défaut d'`APP_CORS_ORIGINS` |
|
||||
| Frontend en conteneur | `3000` | Ce qu'écoute le nginx de l'image, en conteneur comme côté hôte |
|
||||
| Reverse proxy | `80` et `443` | Les seuls ports publiés par `docker-compose.prod.yml`. 80 ne sert que la redirection et le défi ACME |
|
||||
| SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable |
|
||||
| Base applicative | `enervision` | Variable `POSTGRES_DB` |
|
||||
| Base de test | `enervision_test` | Créée par `db/init/110-test-database.sql`, nom attendu en dur par `apps/backend/tests/conftest.py` |
|
||||
|
||||
## Le trou entre les deux topologies
|
||||
## Le trou vers k3s
|
||||
|
||||
Rien ne relie aujourd'hui ce qui est construit par Compose et ce qui tournerait sur k3s. Compose
|
||||
construit une image backend localement ; k3s ne saurait pas où la trouver. C'est la première
|
||||
@@ -125,7 +167,11 @@ question à trancher, avant toute ressource Kubernetes.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
- **Quel ingress** remplace Traefik, et qui termine le TLS.
|
||||
- **Quel ingress** remplace Traefik le jour de la bascule k3s. Qui termine le TLS est tranché par
|
||||
l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md), mais la réponse vaut pour la
|
||||
topologie Compose, pas pour Kubernetes.
|
||||
- **Quel nom de domaine public**, sans lequel Let's Encrypt reste hors d'atteinte et le certificat
|
||||
reste auto-signé.
|
||||
- **Quel registre d'images**, et comment il est alimenté sans CI.
|
||||
- **Quel stockage persistant** côté Kubernetes pour PostgreSQL, et si la base tourne dans le
|
||||
cluster ou à côté.
|
||||
|
||||
@@ -374,9 +374,12 @@ Le reste, par ordre de surface :
|
||||
de secret au logger, la deuxième de ne jamais mettre un jeton dans une URL.
|
||||
- En-têtes posés par l'application : `X-Content-Type-Options`, `X-Frame-Options`,
|
||||
`Referrer-Policy`, plus `Cache-Control: no-store` sur `/auth/*`. HSTS et CSP appartiennent au
|
||||
terminateur TLS, que l'application ne connaît pas.
|
||||
terminateur TLS, que l'application ne connaît pas : le reverse proxy les pose
|
||||
([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)).
|
||||
- Le conteneur tourne en utilisateur non-root, avec un `HEALTHCHECK` sur `/api/v1/health/live`.
|
||||
- Ni limitation de débit au frontal, ni TLS, ni journalisation des accès applicative.
|
||||
- TLS, limitation de débit au frontal et journal d'accès sont portés par le reverse proxy.
|
||||
`APP_TRUST_PROXY_HEADERS` doit alors valoir vrai, sinon le compteur par IP devient global.
|
||||
- Pas de journalisation des accès applicative.
|
||||
|
||||
## Observabilité
|
||||
|
||||
|
||||
@@ -97,11 +97,11 @@ En développement, `proxy.conf.json` redirige tout `/api` vers `http://localhost
|
||||
qui évite le CORS sur le poste, et c'est pourquoi `environment.development.ts` se contente d'un
|
||||
`apiUrl` relatif, `/api/v1`.
|
||||
|
||||
En production, il n'y a pas de proxy, mais `environment.ts` porte lui aussi un `apiUrl` relatif
|
||||
(`/api/v1`) plutôt qu'une URL absolue : la dette qui pointait en dur sur
|
||||
`http://localhost:8000/api/v1` a été corrigée. Un build de production sert donc l'appel `/api/v1/...`
|
||||
sur son propre origin, ce qui suppose qu'un ingress ou un reverse proxy route `/api` vers le
|
||||
backend une fois déployé — question toujours ouverte dans [10-infra.md](10-infra.md).
|
||||
En production, `environment.ts` porte lui aussi un `apiUrl` relatif (`/api/v1`) plutôt qu'une URL
|
||||
absolue : la dette qui pointait en dur sur `http://localhost:8000/api/v1` a été corrigée. Un build
|
||||
de production sert donc l'appel `/api/v1/...` sur son propre origin, et c'est le **reverse proxy**
|
||||
qui route `/api` vers le backend : `location /api/` dans `infra/proxy/conf.d/enervision.conf`, voir
|
||||
[10-infra.md](10-infra.md) et l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md).
|
||||
|
||||
## Exécution
|
||||
|
||||
@@ -118,13 +118,14 @@ le message d'erreur arrive avant toute compilation. Un poste en 22.21 ou en 24.1
|
||||
tester ni construire le frontend.
|
||||
|
||||
Le frontend a ses cibles dans le `Makefile` racine (`install-frontend`, `dev-frontend`,
|
||||
englobées par `install` et `dev`), mais **aucun service dans `docker-compose.yml`** : en
|
||||
développement il tourne toujours directement via `npm`, depuis `apps/frontend`. Le port 4200
|
||||
n'apparaît dans le compose que comme valeur par défaut d'`APP_CORS_ORIGINS`, côté backend.
|
||||
englobées par `install` et `dev`). En développement il tourne directement via `npm`, depuis
|
||||
`apps/frontend` : le port 4200 n'apparaît dans le compose que comme valeur par défaut
|
||||
d'`APP_CORS_ORIGINS`, côté backend.
|
||||
|
||||
Un `Dockerfile` frontend existe sur la branche `feat/pipeline-cd`, mais il est mono-étage et sans
|
||||
`CMD` : il construit sans rien servir. Le `README.md` de l'application demande un multi-étage
|
||||
avec un service statique, il reste à écrire.
|
||||
Le service `frontend` du `docker-compose.yml` sert le build statique par le nginx de
|
||||
`apps/frontend/Dockerfile`, multi-étage, qui **écoute sur 3000**. En déploiement il n'est plus
|
||||
publié du tout : le reverse proxy est seul à sortir sur le réseau, et l'atteint par le réseau
|
||||
Compose.
|
||||
|
||||
## Sécurité
|
||||
|
||||
|
||||
@@ -129,18 +129,18 @@ n'est pas envoyé et le rafraîchissement échoue toujours.
|
||||
En développement, `proxy.conf.json` fait passer `/api` par `localhost:4200`, donc tout est
|
||||
**même origine** et le cookie marche sans rien configurer.
|
||||
|
||||
En production, `src/environments/environment.ts` contient encore le gabarit
|
||||
`http://localhost:8000/api/v1`, en HTTP simple et sur une autre origine. **Dans cet état, aucun
|
||||
cookie `Secure` ne sera posé et l'authentification ne fonctionnera pas.**
|
||||
En déploiement, les deux conditions sont désormais remplies par le reverse proxy
|
||||
([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)) : `environment.ts` porte un
|
||||
`apiUrl` relatif, `/api/v1`, et le proxy sert le SPA sur `/` et l'API sur `/api/` **sous la même
|
||||
origine, en HTTPS**. C'est cela, et rien d'autre, qui rend le cookie `__Secure-ev_refresh`
|
||||
utilisable : servi en HTTP simple ou depuis une autre origine, il n'est jamais posé et
|
||||
l'authentification ne survit pas à un rechargement de page.
|
||||
|
||||
Deux corrections, à faire avant la démonstration :
|
||||
|
||||
1. passer `apiUrl` à `/api/v1` et servir le SPA et l'API sous la même origine, via un
|
||||
`location /api` dans le `nginx.conf` du conteneur frontend ou via l'ingress ;
|
||||
2. servir en HTTPS.
|
||||
Ce qui reste à surveiller : le certificat est auto-signé tant qu'aucun domaine public ne résout
|
||||
vers la machine. Un navigateur qui refuse l'exception refusera aussi le cookie.
|
||||
|
||||
Et au moins une fois avant la soutenance, lancer le front **sans le proxy**, en cross-origin
|
||||
réel : c'est le seul moyen d'exercer le préflight CORS et `SameSite`, que le proxy masque.
|
||||
réel : c'est le seul moyen d'exercer le préflight CORS et `SameSite`, que la même origine masque.
|
||||
|
||||
## Origines autorisées
|
||||
|
||||
|
||||
@@ -42,17 +42,21 @@ lecture seule ; plusieurs lignes resteront à compléter une fois les endpoints
|
||||
| Refus de rétrograder ou désactiver le dernier administrateur actif | `app/services/user.py` | A04 Insecure Design |
|
||||
| Amorçage du premier administrateur hors dépôt, mot de passe jamais dans `argv` ni dans Git | `app/cli.py` | A02, A05 |
|
||||
| CI bloquante : format, lint avec règles Bandit, typage strict, tests avec seuil de couverture | `.github/workflows/backend.yml` | A06 Vulnerable and Outdated Components |
|
||||
| Terminaison TLS au frontal, redirection 80 vers 443, HSTS et CSP posés par le proxy, limitation de débit au frontal | `infra/proxy/conf.d/enervision.conf`, ADR 0007 | API8 Security Misconfiguration, A05 |
|
||||
|
||||
Note sur A06 : le jeu de règles `S` de ruff, déjà actif dans `pyproject.toml`, est le portage des
|
||||
règles Bandit. Ajouter Bandit à la CI serait redondant, contrairement à ce qu'annonce l'EC01.
|
||||
|
||||
Note sur API8 : le transport est couvert, le certificat ne l'est qu'à moitié. Tant qu'aucun nom de
|
||||
domaine public ne résout vers la machine, le défi HTTP-01 de Let's Encrypt ne peut pas aboutir et
|
||||
le certificat servi reste auto-signé. Le chemin ACME est livré et documenté, pas exercé.
|
||||
|
||||
## Non couvert, et pourquoi
|
||||
|
||||
| Item | État | Raison |
|
||||
|---|---|---|
|
||||
| **API1 Broken Object Level Authorization** | **ouvert** | Les rôles sont globaux, il n'y a pas de portée par site : `GET /sites/{site_id}` et `GET /recommendations/{recommendation_id}` répondent à tout compte `lecteur` pour n'importe quel site ou recommandation, sans vérifier une affectation compte-site qui n'existe pas encore. Un opérateur du site A pourra agir sur le site B dès que les endpoints d'écriture métier existeront. Correctif prévu : table d'affectation compte-site, contrôle d'appartenance dans la même dépendance que le contrôle de rôle. |
|
||||
| **API4, lectures de séries temporelles** | **partiel** | `GET /readings` plafonne la fenêtre temporelle (90 jours) et la pagination (`limit` ≤ 2000), voir plus haut. Reste ouvert : pagination en `limit`/`offset` simple plutôt qu'en curseur (un `offset` élevé sur une fenêtre dense reste coûteux), et aucun `statement_timeout` au niveau de la connexion pour borner une requête individuelle si les plafonds au-dessus s'avéraient insuffisants. |
|
||||
| **API8 Security Misconfiguration, transport** | **ouvert** | Pas de TLS, donc ni HSTS, ni cookie `Secure` réellement posé en production. Ils appartiennent au terminateur TLS, qui n'existe pas. |
|
||||
| **API10 Unsafe Consumption of APIs** | **ouvert, 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 doit être traitée comme une entrée hostile : bornes physiques, taille de tableau plafonnée, timeout, et frontière d'anti-corruption. 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. |
|
||||
|
||||
Reference in New Issue
Block a user