Suite à une instabilité de l'environnement de production (VPS IONOS — PM2 en boucle de redémarrage), une revue complète a été engagée couvrant : stabilisation de la production, audit de sécurité applicative, hardening des secrets, et déploiement d'un système de sauvegarde chiffré conforme CNIL.
| Périmètre | Détail |
|---|---|
| Backend | Node.js + TypeScript + Express 5 + Drizzle ORM |
| Frontend | React 19 + Vite + Tailwind CSS 4 + shadcn/ui |
| Base de données | PostgreSQL 17 hébergé Neon (cloud) |
| VPS | IONOS — IP 212.227.212.43 — RHEL/CentOS — Nginx + PM2 |
| Backup stockage | IONOS Object Storage — bucket transportx-backups-prod |
| Conformité | Données hébergées UE — CNIL — pas d'AWS US |
La migration SQL 026_field_encryption.sql effectuait un ALTER TABLE fret_demande_stops
sur une table inexistante en base. Le runner de migration échouait silencieusement,
laissant le schéma dans un état incohérent et provoquant 129 redémarrages en boucle.
Ajout d'un CREATE TABLE IF NOT EXISTS fret_demande_stops avant l'ALTER TABLE.
Ajout du type fret_stop_type avec gestion EXCEPTION WHEN duplicate_object.
Migration redéployée sur VPS — PM2 stable.
Les modules user-lookup.ts, field-crypto.ts et la dépendance
@sentry/node étaient absents du bundle déployé.
Synchronisation complète de node-backend/src/lib/ par SCP.
Installation de @sentry/node via pnpm. Health check GET /api/health → 200 OK rétabli.
La variable SUPER_ADMIN_PASSWORD était définie au niveau module avec une valeur hardcodée
de fallback. Un module lançant une exception au chargement causait l'échec du démarrage.
Variable déplacée à l'intérieur de la branche insert uniquement.
Rejet explicite avec message clair si SUPER_ADMIN_PASSWORD est absent de l'environnement.
| # | Vulnérabilité | Gravité | Fichier | Statut |
|---|---|---|---|---|
| 1 | Bypass tokenVersion — refresh token révoqué accepté | Critique | auth.service.ts |
Corrigé |
| 2 | Home.tsx — erreurs fetch silencieuses (catch vide) | Moyen | pages/Home.tsx |
Corrigé |
| 3 | CI — --no-frozen-lockfile (dépendances non verrouillées) |
Moyen | .github/workflows/deploy.yml |
Corrigé |
| 4 | Enum fluvial absent de la validation Zod fret |
Moyen | routes/fret.ts |
Corrigé |
| 5 | Mot de passe superadmin hardcodé en fallback | Critique | lib/seedSuperAdmin.ts |
Corrigé |
| 6 | JWT_SECRET faible / inchangé depuis le développement | Critique | .env VPS |
Roté |
| 7 | Champs sensibles non chiffrés en base | Moyen | migration 026 |
Corrigé |
Avant : la condition vérifiait tokenVersion !== undefined && ... — un token sans ce champ était accepté.
Après : rejet systématique si le champ est absent ou différent de la version courante en base.
| Variable | Action | Longueur |
|---|---|---|
JWT_SECRET | Roté | 64 octets hex (openssl rand) |
FIELD_ENCRYPTION_KEY | Créée | 32 octets hex |
FIELD_HMAC_KEY | Créée | 32 octets hex |
BACKUP_ENC_PASSPHRASE | Créée | 48 octets base64 |
Pipeline streaming sans fichier temporaire en clair. Les données ne transitent jamais en clair sur le disque du VPS ni sur le réseau.
pg_dump (Neon PG17) → gzip -9 → openssl enc -aes-256-cbc -pbkdf2 -iter 200000 → tee /var/backups/transportx/[fichier local 48h] → aws s3 cp → s3://transportx-backups-prod/db/localhost/
| Problème | Cause | Solution |
|---|---|---|
| Neon SNI — Endpoint ID is not specified | libpq v13 sur VPS — PGOPTIONS non interprété | DSN complet sslmode=require&options=endpoint%3D<id> via --dbname |
| Version client incompatible | pg_dumpall v13 vs Neon PostgreSQL 17 | Installation de postgresql17 (PGDG) — binaire /usr/pgsql-17/bin/pg_dump |
| pg_dumpall — auth sous-processus échoue | pg_dump spawné sans accès au PGPASSWORD du parent | Migration vers pg_dump (base unique Neon) |
| InvalidAccessKeyId AWS | Credentials S3 placeholders __A_COMPLETER__ |
Injection credentials IONOS Object Storage réels |
| Upload IONOS — NoneType is not iterable | Options avancées --sse / --storage-class incompatibles IONOS |
Mode upload minimal — suppression des options avancées pour les endpoints S3-compatibles |
| Restauration — bad decrypt | Commande déchiffrement sans -iter 200000 |
Ajout de -iter 200000 dans toutes les commandes de déchiffrement |
| Variable | Valeur |
|---|---|
AWS_DEFAULT_REGION | eu-central-3 |
AWS_ENDPOINT_URL | https://s3.eu-central-3.ionoscloud.com |
BACKUP_S3_BUCKET | transportx-backups-prod |
PGDATABASE | neondb |
| Cron | 0 2 * * * — quotidien à 02h00 UTC |
| Copie locale | /var/backups/transportx/ — rétention 48h |
[backup] OK confirmé-- PostgreSQL database dump
La lecture de la lifecycle via aws s3api get-bucket-lifecycle-configuration
retourne une erreur d'incompatibilité sur IONOS Object Storage. Cette opération doit être
réalisée directement dans la console IONOS.
Règle de rétention à configurer :
| Paramètre | Valeur |
|---|---|
| Bucket | transportx-backups-prod |
| Préfixe | db/localhost/ |
| Statut | Enabled |
| Action | Expire current objects |
| Délai d'expiration | 30 jours |
| Uploads incomplets | 7 jours (si disponible) |
Une règle lifecycle doit être configurée dans IONOS Object Storage sur le bucket
transportx-backups-prod, ciblant le préfixe db/localhost/,
avec expiration automatique des objets au bout de 30 jours et suppression des uploads
multipart incomplets au bout de 7 jours si l'option est disponible.
| Composant | État | Remarque |
|---|---|---|
| Production VPS | Stable | PM2 actif, health check /api/health → 200 |
| Migrations SQL | OK | 026 corrigée et déployée |
| Sécurité applicative | Durcie | 7 corrections, secrets rotés |
| Backup chiffré | Opérationnel | Cron 02h00 UTC, IONOS eu-central-3 |
| Restauration | Validée | Test streamé réussi, SHA-256 OK |
| Lifecycle bucket | À configurer | À faire dans la console IONOS |