Compte-Rendu Officiel
Audit Sécurité, Corrections Production
& Mise en Place du Backup Chiffré
Revue complète de la sécurité applicative, corrections de 7 vulnerabilités critiques, hardening VPS, et déploiement du système de backup PostgreSQL chiffré vers IONOS Object Storage.
7 Corrections sécurité
100 % Backup opérationnel
AES-256 Chiffrement données
IONOS Hébergeur UE / CNIL
JWT roté Secrets applicatifs
PG 17 Client dump Neon

1. Contexte & objectifs

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
BackendNode.js + TypeScript + Express 5 + Drizzle ORM
FrontendReact 19 + Vite + Tailwind CSS 4 + shadcn/ui
Base de donnéesPostgreSQL 17 hébergé Neon (cloud)
VPSIONOS — IP 212.227.212.43 — RHEL/CentOS — Nginx + PM2
Backup stockageIONOS Object Storage — bucket transportx-backups-prod
ConformitéDonnées hébergées UE — CNIL — pas d'AWS US

2. Stabilisation de la production

2.1 Cause racine du crash PM2

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.

Correction appliquée

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.

2.2 Modules manquants sur VPS

Les modules user-lookup.ts, field-crypto.ts et la dépendance @sentry/node étaient absents du bundle déployé.

Correction appliquée

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.

2.3 seedSuperAdmin — fail-open

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.

Correction appliquée

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.

3. Audit sécurité — 7 corrections

# 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é

3.1 Détail — Bypass tokenVersion

Avant / Après

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.

3.2 Secrets applicatifs créés / rotés

Variable Action Longueur
JWT_SECRETRoté64 octets hex (openssl rand)
FIELD_ENCRYPTION_KEYCréée32 octets hex
FIELD_HMAC_KEYCréée32 octets hex
BACKUP_ENC_PASSPHRASECréée48 octets base64

4. Système de backup chiffré

4.1 Architecture

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/

4.2 Problèmes résolus pendant le déploiement

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

4.3 Configuration finale VPS

Variable Valeur
AWS_DEFAULT_REGIONeu-central-3
AWS_ENDPOINT_URLhttps://s3.eu-central-3.ionoscloud.com
BACKUP_S3_BUCKETtransportx-backups-prod
PGDATABASEneondb
Cron0 2 * * * — quotidien à 02h00 UTC
Copie locale/var/backups/transportx/ — rétention 48h

4.4 Validations effectuées

Résultats des tests
  • Backup exécuté 3 fois avec succès — [backup] OK confirmé
  • SHA-256 local et distant identiques — intégrité vérifiée
  • Restauration streamée depuis IONOS déchiffrée et décompressée avec succès
  • Entête SQL visible : -- PostgreSQL database dump
  • Fichier de test supprimé du bucket
  • Scripts temporaires nettoyés du VPS et du repo

4.5 Procédure de restauration complète

# 1. Charger les variables de configuration
set -a && source /root/.transportx-backup.env && set +a

# 2. Identifier le fichier à restaurer
KEY="db/localhost/transportx-YYYYMMDDTHHMMSSZ.sql.gz.enc"

# 3. Télécharger, déchiffrer, décompresser, importer
aws s3 cp "s3://${BACKUP_S3_BUCKET}/${KEY}" - \
  --region "${AWS_DEFAULT_REGION}" \
  --endpoint-url "${AWS_ENDPOINT_URL}" \
| openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
  -pass env:BACKUP_ENC_PASSPHRASE \
| gunzip \
| psql "postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT:-5432}/${PGDATABASE}?sslmode=require&options=endpoint%3D${PGHOST%%.*}"

5. Rétention & lifecycle bucket

Action manuelle requise — Console IONOS

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
Buckettransportx-backups-prod
Préfixedb/localhost/
StatutEnabled
ActionExpire current objects
Délai d'expiration30 jours
Uploads incomplets7 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.

6. Chronologie des interventions

7. État final du système

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
TransPortX SAS — Compte-Rendu Technique — Mai 2026 — Confidentiel