Express : sécurité et production
Passage en production d’une application Express derrière un reverse proxy. Noms génériques : application monapi, utilisateur monapi, dossier /srv/monapi, port local 3000, domaine exemple.com.
Sécurité de l’application
Section intitulée « Sécurité de l’application »npm install helmet express-rate-limitimport express from 'express';import helmet from 'helmet';import { rateLimit } from 'express-rate-limit';
const app = express();
app.disable('x-powered-by'); // Helmet le fait aussiapp.set('trust proxy', 1); // un proxy devant l'application
app.use(helmet());app.use( rateLimit({ windowMs: 15 * 60 * 1000, limit: 300, standardHeaders: 'draft-7', legacyHeaders: false, }),);app.use(express.json({ limit: '100kb' }));| Mesure | Rôle |
|---|---|
helmet() |
En-têtes de sécurité (Content-Security-Policy, X-Content-Type-Options, …) |
rateLimit |
Limite le nombre de requêtes par adresse (force brute, abus) |
trust proxy |
Fait lire l’adresse du client dans X-Forwarded-For (indispensable derrière un proxy pour req.ip et la limitation de débit) |
express.json({ limit }) |
Limite la taille des corps |
app.disable('x-powered-by') |
Ne pas annoncer Express |
Limiter plus strictement les routes sensibles :
const limiteConnexion = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10 });app.post('/api/connexion', limiteConnexion, (req, res) => { /* ... */});Bonnes pratiques
Section intitulée « Bonnes pratiques »- Valider toutes les entrées (voir API JSON).
- Requêtes SQL paramétrées uniquement.
- Secrets dans l’environnement, jamais dans le code ni le dépôt.
npm auditrégulier,npm cien production, dépendances à jour.- Ne pas renvoyer les traces d’erreurs au client.
- CORS limité aux origines nécessaires.
- Exécuter sous un utilisateur sans privilèges, en HTTPS derrière le proxy.
npm auditnpm audit --omit=devnpm outdatedVariables d’environnement
Section intitulée « Variables d’environnement »/etc/monapi.env :
NODE_ENV=productionPORT=3000DATABASE_URL=postgres://app@127.0.0.1:5432/boutiqueSESSION_SECRET=<valeur-aleatoire-longue>sudo chown root:monapi /etc/monapi.envsudo chmod 640 /etc/monapi.envopenssl rand -base64 48 # générer un secretNODE_ENV=production active des optimisations d’Express (cache des vues, messages d’erreur réduits).
Déployer
Section intitulée « Déployer »sudo adduser --system --group --home /srv/monapi monapisudo -u monapi git clone https://git.exemple.com/monapi.git /srv/monapi/appcd /srv/monapi/appsudo -u monapi npm ci --omit=devService systemd
Section intitulée « Service systemd »/etc/systemd/system/monapi.service :
[Unit]Description=API monapi (Express)After=network.target
[Service]User=monapiGroup=monapiWorkingDirectory=/srv/monapi/appEnvironmentFile=/etc/monapi.envExecStart=/usr/bin/node src/serveur.jsRestart=on-failureRestartSec=3NoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=trueReadWritePaths=/srv/monapi
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now monapicurl -I http://127.0.0.1:3000/santesudo journalctl -u monapi -fLe chemin de node se lit avec which node (avec nvm, utiliser le chemin complet de la version installée). Voir systemd : durcissement d’un service et Node.js : mise en production.
PM2 : cluster et rechargement sans coupure
Section intitulée « PM2 : cluster et rechargement sans coupure »Pour exploiter tous les cœurs et recharger sans coupure : PM2. Les sessions et caches doivent alors être hors de la mémoire du processus (base de données, Redis).
Reverse proxy Caddy
Section intitulée « Reverse proxy Caddy »exemple.com { encode zstd gzip header { Strict-Transport-Security "max-age=31536000" -Server } reverse_proxy 127.0.0.1:3000}Caddy gère HTTPS et la compression : ne pas ajouter le middleware compression dans l’application. Les fichiers statiques peuvent être servis directement :
exemple.com { handle /assets/* { root * /srv/monapi/app/public file_server } reverse_proxy 127.0.0.1:3000}Voir Caddy : exemples réutilisables. Avec Nginx : Nginx : reverse proxy.
Pare-feu
Section intitulée « Pare-feu »L’application écoute sur 127.0.0.1 : seul le reverse proxy (80 et 443) est exposé.
sudo ufw allow 80,443/tcpsudo ss -tlnp | grep 3000 # doit afficher 127.0.0.1:3000Voir Exemple : VPS avec Caddy.
Journalisation
Section intitulée « Journalisation »import morgan from 'morgan';app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));Écrire sur la sortie standard : journald collecte (journalctl -u monapi). Ne pas journaliser de secrets, de mots de passe ni de données personnelles superflues.
Arrêt propre
Section intitulée « Arrêt propre »const serveur = app.listen(port, '127.0.0.1');
function arreter() { serveur.close(() => process.exit(0)); setTimeout(() => process.exit(1), 10_000).unref();}process.on('SIGTERM', arreter);process.on('SIGINT', arreter);systemd envoie SIGTERM à l’arrêt : le serveur termine les requêtes en cours avant de s’arrêter.
Surveillance
Section intitulée « Surveillance »Ajouter une route de santé et la tester depuis l’extérieur :
app.get('/sante', (req, res) => res.json({ statut: 'ok', uptime: process.uptime() }),);curl -fsS https://exemple.com/santeDépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
502 Bad Gateway |
Service arrêté ou mauvais port : systemctl status monapi, curl http://127.0.0.1:3000/sante |
| Le service redémarre en boucle | journalctl -u monapi -n 50 : variable manquante, port occupé, import raté |
| Limitation de débit appliquée à tout le monde | trust proxy absent |
req.protocol vaut http derrière HTTPS |
trust proxy et en-tête X-Forwarded-Proto (Caddy le transmet) |
Cookie secure jamais enregistré |
HTTPS non terminé correctement ou trust proxy manquant |
ReferenceError/Cannot find module en production |
npm ci --omit=dev : une dépendance utilisée est déclarée en devDependencies |
Erreur d’écriture dans uploads/ |
ReadWritePaths et droits du dossier |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »La procédure est identique. Le chemin de node et sa version dépendent de l’installation (voir Node.js : installation). Sur 26.04, sudo est fourni par sudo-rs : les commandes ci-dessus fonctionnent sans changement.