Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker. Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Security Fixes Applied — Phase 2
Date: 2026-05-06
SEC-FIX-001 — Bootstrap admin et rattrapage des bases existantes
Problème résiduel (après phase 1)
La migration _migrate_users_must_change_password() ajoutait la colonne avec DEFAULT 0. Un compte admin existant avec le mot de passe par défaut "admin" n'était donc pas forcé à changer.
Corrections appliquées
backend/main.py
- Nouvelle migration
_migrate_users_token_version()— ajout detoken_version INTEGER NOT NULL DEFAULT 1si absente. - Nouvelle migration
_migrate_force_admin_password_change(): vérifie si l'admin amust_change_password=0et si son hash correspond au mot de passe "admin". Si oui, posemust_change_password=1. Idempotent, s'exécute à chaque démarrage. _migrate_users(): mise à jour pour incluretoken_versiondans le CREATE TABLE initial.- Ordre de démarrage mis à jour :
_migrate_users_token_version()puis_migrate_force_admin_password_change()avant_migrate_users().
backend/tests/conftest.py (nouveau)
- Configure
DATABASE_URLvers une base temporaire avant tout import applicatif. SECRET_KEYfixé pour les tests.
backend/tests/test_auth.py (nouveau)
- Tests SEC-FIX-001 : base vide (must_change=1), CRUD bloqué avant changement, CRUD autorisé après, rattrapage admin existant, admin avec mot de passe personnalisé non touché,
INITIAL_ADMIN_PASSWORD. - Tests SEC-FIX-004 : ancien token rejeté après changement, nouveau token valide, backward compat token sans
ver, token avec mauvaise version. - Tests SEC-FIX-003 : validation mot de passe (trop court, sans chiffre, sans lettre, valide), username (invalide, trop long, valide), mauvais mot de passe courant.
- Tests SEC-FIX-002 : rate limit IP (429), rate limit username (429), reset après login réussi.
backend/requirements-test.txt (nouveau)
pytest>=7.4,httpx>=0.25.
SEC-FIX-004 — Invalidation de session après changement de mot de passe
Problème
JWT valable 7 jours, pas d'invalidation après changement de mot de passe.
Corrections appliquées
backend/models.py
- Colonne
token_version = Column(Integer, nullable=False, default=1, server_default="1")ajoutée àUser.
backend/routers/auth.py
TOKEN_EXPIRE_HOURS = 24(réduit de 7 jours à 24 heures).create_token(username, version): payload inclut{ ver: version }.get_current_user: vérifiepayload["ver"] == user.token_version. Rejette avec 401 si différent. Rétrocompatibilité :verabsent dans le payload est traité commever=1.update_account: incrémentecurrent_user.token_versionlors d'un changement de mot de passe.login: transmetuser.token_versionaucreate_token.
SEC-FIX-005 — CORS configurable
Problème
allow_origins=["*"] hardcodé.
Corrections appliquées
backend/main.py
- Lecture de
ALLOWED_ORIGINSenv var (défaut"*"pour rétrocompatibilité). ""désactive le middleware CORS (même origine via proxy).- Valeur CSV : liste d'origines explicites.
.env.example (nouveau)
- Documentation de
ALLOWED_ORIGINSavec exemples.
SEC-FIX-010 — Sécurisation du secret JWT
Problème
secret_key.txt créé sans permissions restrictives. Pas de .gitignore.
Corrections appliquées
backend/routers/auth.py
- Création du fichier secret via
os.open(..., os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)pour imposer des permissions 0600 dès la création (owner read/write uniquement).
.gitignore (nouveau)
- Ignore
db_data/,*.db,.env,__pycache__/,node_modules/, etc.
SEC-FIX-011 — Durcissement des conteneurs
Problème
Conteneurs lancés en root sans contraintes de capabilities ni healthchecks.
Corrections appliquées (Phase 2 initiale)
frontend/Dockerfile
npm install→npm ci(build reproductible, respecte le lockfile).- Image de base finale :
nginxinc/nginx-unprivileged:alpine— nginx s'exécute entièrement en UID 101 sans nécessiterCAP_CHOWNniCAP_SETUID. Compatible aveccap_drop: ALL. EXPOSE 8080(port non-privilégié, correspondant à la config nginx).
frontend/nginx.conf
listen 80→listen 8080(port non-privilégié, ne nécessite pas CAP_NET_BIND_SERVICE).
docker-compose.yml
frontend:cap_drop: ALL,security_opt: no-new-privileges:true, port8080:8080, healthcheck wget,depends_on: backend: condition: service_healthy.- Suppression de la section
volumes: db_data:(bind mount, pas de volume nommé).
Approches abandonnées (Phase 2 initiale)
gosu+appuserpour le backend :cap_drop: ALLsupprimeCAP_SETUID→gosune peut pas changer d'utilisateur depuis le processus uvicorn après démarrage.
Correction complémentaire — backend non-root via user: Compose
Problème résiduel : le backend tournait en UID 0 (root) faute de mécanisme interne pour changer d'utilisateur. La capability DAC_OVERRIDE avait été ajoutée comme contournement (accès en écriture au bind-mount), mais les fichiers db_data/ étaient créés root:root sur l'hôte.
Solution : la directive user: de Docker Compose démarre le processus directement sous l'UID/GID cible, sans nécessiter CAP_SETUID ni gosu.
docker-compose.yml
backend: ajout deuser: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}".cap_add: suppression deDAC_OVERRIDE(inutile — le processus est propriétaire du bind-mount).cap_addconserveNET_RAW(ICMP ping).no-new-privilegesreste omis : ping utilise les file capabilities (cap_net_raw=ep) ;no-new-privilegessupprime le bit effective du fichier, empêchant le sous-processus d'acquérirCAP_NET_RAWdans son ensemble effectif même si le parent le détient dans son ensemble permis.
.env.example
DOCKER_UID/DOCKER_GIDdocumentés avec procédureid -u && id -get instructionmkdir -p db_data.
README.md
- Quick start :
docker compose --env-file .env up --build -d. - Configuration :
APP_UID/APP_GIDremplacés parDOCKER_UID/DOCKER_GID. - Container hardening : table mise à jour (
DOCKER_UID:DOCKER_GID), noteno-new-privilegesprécisée.
SEC-FIX-018 — Documentation
Corrections appliquées
README.md (nouveau)
- Quick start, configuration, sécurité (HTTPS, rotation de clé, durcissement conteneurs), persistance des données, développement.
.env.example (nouveau)
SECRET_KEY,INITIAL_ADMIN_PASSWORD,ALLOWED_ORIGINS,APP_UID/APP_GIDdocumentés avec exemples.
docs/backend.md
- Routeurs protégés :
get_current_user→require_password_changed. - Contrat login : ajout
must_change_password. - Contrat account : codes d'erreur normalisés.
- Modèle
User: ajoutmust_change_password,token_version. - Section Auth : mise à jour expiry, payload, invalidation, rate limiting.
- Section Migrations : ordre complet, description
_migrate_force_admin_password_change.
docs/architecture.md
- Section Auth : schéma mis à jour (
mustChangePassword,AccountModal :forced). - Section Docker Compose : non-root, cap_drop, suppression volume nommé.
CLAUDE.md
- Section Authentification : 24h,
token_version,must_change_password, guard App.vue,INITIAL_ADMIN_PASSWORD. - Section Migrations : liste complète des 7 migrations.
AGENTS.md
- Section Authentication : réécriture complète.
- Data Model :
Usermis à jour.
Fichiers modifiés
| Fichier | Fixes |
|---|---|
backend/database.py |
Support DATABASE_URL pour tests |
backend/models.py |
SEC-FIX-004 (token_version) |
backend/main.py |
SEC-FIX-001 (rattrapage), SEC-FIX-004 (migration), SEC-FIX-005 (CORS) |
backend/routers/auth.py |
SEC-FIX-004 (versioning), SEC-FIX-010 (0600), expiry 24h |
backend/Dockerfile |
SEC-FIX-011 (cap_drop, DAC_OVERRIDE) |
backend/requirements-test.txt |
Tests (nouveau) |
backend/tests/__init__.py |
Tests (nouveau) |
backend/tests/conftest.py |
Tests (nouveau) |
backend/tests/test_auth.py |
Tests SEC-FIX-001, 002, 003, 004 (nouveau) |
frontend/Dockerfile |
SEC-FIX-011 (nginxinc/nginx-unprivileged, npm ci) |
frontend/nginx.conf |
SEC-FIX-011 (port 8080) |
docker-compose.yml |
SEC-FIX-011 (cap_drop, healthchecks) |
.gitignore |
SEC-FIX-010 (nouveau) |
.env.example |
SEC-FIX-005, SEC-FIX-010, SEC-FIX-018 (nouveau) |
README.md |
SEC-FIX-018 (nouveau) |
docs/backend.md |
SEC-FIX-018 |
docs/architecture.md |
SEC-FIX-018 |
CLAUDE.md |
SEC-FIX-018 |
AGENTS.md |
SEC-FIX-018 |
Risques couverts
| Risque | Avant | Après |
|---|---|---|
| Bootstrap admin/admin sur instance existante | Non corrigé | _migrate_force_admin_password_change() force le changement au prochain démarrage |
| JWT valable 7 jours après compromission | Oui | 24h + invalidation immédiate par token_version |
| CORS permissif | Hardcodé * |
Configurable via ALLOWED_ORIGINS, défaut * rétrocompatible |
| Secret JWT lisible (permissions) | Mode par défaut (0644 ou plus) | 0600 dès la création |
| Secrets dans le dépôt | Pas de .gitignore |
db_data/, .env, *.db ignorés |
| Process frontend root | Oui (nginx master + workers en root) | nginxinc/nginx-unprivileged — tout le process tree en UID 101 |
| Process backend root | Oui | user: DOCKER_UID:DOCKER_GID — process = utilisateur hôte, fichiers db_data/ non root-owned |
| Capabilities superflues | Aucun cap_drop | cap_drop: ALL sur les deux conteneurs ; backend ajoute NET_RAW uniquement |
DAC_OVERRIDE exposée |
Oui (contournement root) | Supprimée — inutile avec non-root propriétaire du bind-mount |
| Pas de healthchecks | Aucun | Ajoutés sur les deux services |
| Build frontend non reproductible | npm install |
npm ci |
Limitations restantes
- JWT stocké en
localStorage(pas de cookie HttpOnly) — mitigation : expiry 24h + token versioning. - Rate limiting backend en mémoire (perdu au redémarrage, ne se partage pas entre workers) — mitigation : Nginx
limit_reqcouvre le cas multi-worker. no-new-privilegesabsent du backend :no-new-privilegessupprime le bit effective des file capabilities ; le sous-processus ping ne peut pas acquérirCAP_NET_RAWdans son ensemble effectif même si le parent le détient en permis. Mitigation : le processus tourne désormais sous un UID non-privilégié.- Politique de mot de passe minimale (
password1passe) — pas de liste noire de mots de passe communs. - Pas de logs structurés d'audit (SEC-FIX-012 non traité dans cette phase).