Initial commit — Stupid Simple Network Inventory
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>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# 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 de `token_version INTEGER NOT NULL DEFAULT 1` si absente.
|
||||
- Nouvelle migration `_migrate_force_admin_password_change()` : vérifie si l'admin a `must_change_password=0` et si son hash correspond au mot de passe "admin". Si oui, pose `must_change_password=1`. Idempotent, s'exécute à chaque démarrage.
|
||||
- `_migrate_users()` : mise à jour pour inclure `token_version` dans 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_URL` vers une base temporaire avant tout import applicatif.
|
||||
- `SECRET_KEY` fixé 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érifie `payload["ver"] == user.token_version`. Rejette avec 401 si différent. Rétrocompatibilité : `ver` absent dans le payload est traité comme `ver=1`.
|
||||
- `update_account` : incrémente `current_user.token_version` lors d'un changement de mot de passe.
|
||||
- `login` : transmet `user.token_version` au `create_token`.
|
||||
|
||||
---
|
||||
|
||||
## SEC-FIX-005 — CORS configurable
|
||||
|
||||
### Problème
|
||||
`allow_origins=["*"]` hardcodé.
|
||||
|
||||
### Corrections appliquées
|
||||
|
||||
**`backend/main.py`**
|
||||
- Lecture de `ALLOWED_ORIGINS` env 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_ORIGINS` avec 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écessiter `CAP_CHOWN` ni `CAP_SETUID`. Compatible avec `cap_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`, port `8080: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` + `appuser` pour le backend : `cap_drop: ALL` supprime `CAP_SETUID` → `gosu` ne 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 de `user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"`.
|
||||
- `cap_add` : suppression de `DAC_OVERRIDE` (inutile — le processus est propriétaire du bind-mount).
|
||||
- `cap_add` conserve `NET_RAW` (ICMP ping).
|
||||
- `no-new-privileges` reste omis : ping utilise les file capabilities (`cap_net_raw=ep`) ; `no-new-privileges` supprime le bit effective du fichier, empêchant le sous-processus d'acquérir `CAP_NET_RAW` dans son ensemble effectif même si le parent le détient dans son ensemble permis.
|
||||
|
||||
**`.env.example`**
|
||||
- `DOCKER_UID` / `DOCKER_GID` documentés avec procédure `id -u && id -g` et instruction `mkdir -p db_data`.
|
||||
|
||||
**`README.md`**
|
||||
- Quick start : `docker compose --env-file .env up --build -d`.
|
||||
- Configuration : `APP_UID`/`APP_GID` remplacés par `DOCKER_UID`/`DOCKER_GID`.
|
||||
- Container hardening : table mise à jour (`DOCKER_UID:DOCKER_GID`), note `no-new-privileges` pré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_GID` documenté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` : ajout `must_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 : `User` mis à 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_req` couvre le cas multi-worker.
|
||||
- `no-new-privileges` absent du backend : `no-new-privileges` supprime le bit effective des file capabilities ; le sous-processus ping ne peut pas acquérir `CAP_NET_RAW` dans 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 (`password1` passe) — pas de liste noire de mots de passe communs.
|
||||
- Pas de logs structurés d'audit (SEC-FIX-012 non traité dans cette phase).
|
||||
Reference in New Issue
Block a user