Author SHA1 Message Date
Antho 4bc4770e2d fix(securite): supprime le secret JWT de repli présent dans le dépôt
JWT_SECRET retombait sur "data-sentinel-secret-change-in-prod", une valeur
lisible par quiconque a accès au code : un oubli de configuration suffisait
à permettre de forger un jeton d'administrateur.

Sans JWT_SECRET, une clé aléatoire est désormais tirée au démarrage, avec un
avertissement explicite. Le démarrage n'est volontairement pas bloqué : un
oubli de variable d'environnement ne doit pas transformer une configuration
incomplète en indisponibilité totale du service. Contrepartie assumée et
documentée : les sessions ne survivent pas à un redémarrage tant que la
variable n'est pas définie.

Vérifié : la production signe déjà avec un secret propre, ce correctif ne
change donc rien à son fonctionnement.

Ajoute par ailleurs APP_BUILD, injecté par la CI depuis le SHA du commit et
exposé par GET /health (et /version.json côté front). Jusqu'ici, rien ne
permettait de savoir quelle version tournait réellement : un déploiement
non appliqué était indiscernable d'un déploiement réussi.
2026-08-15 14:46:49 +02:00
6 changed files with 74 additions and 6 deletions
+4 -1
View File
@@ -16,7 +16,10 @@ jobs:
uses: actions/checkout@v4
- name: Build image
run: docker build -t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
run: |
docker build \
--build-arg APP_BUILD="${GITHUB_SHA::12}" \
-t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
- name: Tests (pytest dans l'image, BDD simulée)
run: |
+5
View File
@@ -17,5 +17,10 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# SHA du commit construit, injecté par la CI et exposé par GET /health :
# permet de vérifier quelle version tourne réellement après un déploiement.
ARG APP_BUILD=local
ENV APP_BUILD=$APP_BUILD
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+20 -1
View File
@@ -34,7 +34,8 @@ sur `localhost`. Pour une instance nommée, définir `DB_SERVER` (ex.
| `DB_TRUSTED_CONNECTION` | force l'authentification Windows même si `DB_USER` est défini | — |
| `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` |
| `CORS_ORIGINS` | origines autorisées (séparées par `,`) | `localhost:5173,localhost:3000` |
| `JWT_SECRET` | clé de signature JWT | placeholder (à définir en prod) |
| `JWT_SECRET` | clé de signature JWT **obligatoire en déploiement** | clé aléatoire régénérée à chaque démarrage |
| `APP_BUILD` | SHA du commit construit, injecté par la CI et exposé par `/health` | `local` |
| `JWT_ALGORITHM` / `JWT_EXPIRE_MINUTES` | algo / durée du token | `HS256` / `60` |
## Authentification & rôles
@@ -96,8 +97,26 @@ routeurs). Chaque domaine fonctionnel vit dans `routers/` :
Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`).
## Vérifier qu'un déploiement a pris
`GET /health` expose le SHA du commit dont l'image a été construite :
```bash
curl -s https://datasentinel-api.nfteam.ovh/health
# {"api":"ok","database":"ok","version":"1.0.0","build":"cb84e20e1f2a",...}
```
Comparer `build` au dernier commit poussé sur `main`. S'ils diffèrent, le
conteneur tourne encore une ancienne image : `docker compose pull` puis
`docker compose up -d` (un `up -d` seul ne retélécharge rien). Le front expose
la même information sur `/version.json`.
## Sécurité
- `JWT_SECRET` doit être défini en déploiement. À défaut, l'API démarre quand
même mais tire une clé aléatoire à chaque lancement (sessions perdues au
redémarrage) : aucun secret de repli n'est écrit dans le dépôt, un secret
public permettrait de forger un jeton d'administrateur.
- En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`.
- CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials.
- Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif
+36 -2
View File
@@ -3,10 +3,13 @@
# Data Sentinel | COYAUD Anthony | 2026
# ============================================================
import logging
import os
import pyodbc
import secrets
from contextlib import contextmanager
import pyodbc
def _build_connection_string() -> str:
"""
@@ -45,6 +48,33 @@ def _build_connection_string() -> str:
)
def _resolve_jwt_secret() -> str:
"""
Clé de signature des jetons.
Elle doit venir de JWT_SECRET. À défaut, on tire une clé aléatoire au
démarrage plutôt que de retomber sur une valeur écrite dans le dépôt :
un secret public permettrait à quiconque lit le code de forger un jeton
d'administrateur.
Conséquence assumée du repli : la clé change à chaque redémarrage, donc
les sessions en cours sont invalidées. C'est visible et sans gravité en
développement, et le message ci-dessous dit quoi faire en déploiement.
On ne bloque volontairement pas le démarrage, pour ne pas transformer un
oubli de configuration en indisponibilité totale du service.
"""
secret = os.getenv("JWT_SECRET")
if secret:
return secret
logging.getLogger("uvicorn.error").warning(
"JWT_SECRET n'est pas defini : une cle aleatoire est generee pour cette "
"execution. Les sessions seront perdues a chaque redemarrage. "
"Definir JWT_SECRET dans l'environnement (api.env) pour un deploiement."
)
return secrets.token_urlsafe(64)
class Config:
# Chaîne de connexion SQL Server (env en prod, Windows en local)
DB_CONNECTION_STRING = _build_connection_string()
@@ -54,8 +84,12 @@ class Config:
API_VERSION = "1.0.0"
API_DESCRIPTION = "API de monitoring de la qualité des données — XEFI"
# Identifiant de build injecté par la CI : permet de vérifier quelle
# version tourne réellement après un déploiement (voir GET /health).
BUILD = os.getenv("APP_BUILD", "local")
# Sécurité JWT
SECRET_KEY = os.getenv("JWT_SECRET", "data-sentinel-secret-change-in-prod")
SECRET_KEY = _resolve_jwt_secret()
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
TOKEN_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "60"))
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -81,7 +81,13 @@ async def security_headers(request: Request, call_next):
@app.get("/health", tags=["Système"])
def health_check():
"""Ping API + test connexion SQL Server."""
"""
Ping API + test connexion SQL Server.
`build` porte le SHA du commit dont l'image a été construite (injecté par
la CI). C'est le seul moyen fiable de vérifier qu'un déploiement a bien
pris : comparer ce champ au dernier commit poussé.
"""
try:
with get_cursor() as cursor:
cursor.execute("SELECT 1")
@@ -94,6 +100,7 @@ def health_check():
"api" : "ok",
"database" : db_status,
"version" : Config.API_VERSION,
"build" : Config.BUILD,
"nb_monitorings" : len(MONITO_TABLES),
}