## Introduction

Durcir la sécurité d’une API Express en production exige une approche méthodique et vérifiable. Déployer un conteneur est simple ; l’opérer de manière fiable, traçable et sûre l’est beaucoup moins. Un bon guide doit préciser :

- ce qu’il faut configurer ;

- comment prouver que la configuration fonctionne ;

- à quoi ressemble l’échec quand le paramétrage manque ou est incorrect.

Objectif pratique : comprendre chaque contrôle, le tester localement, documenter l’attendu et les signaux d’échec, puis répliquer sans surprise en CI/CD ou dans un environnement proche de la production.

Ce guide s’adresse aux développeurs, consultants DevOps et équipes techniques. Il couvre l’authentification et l’autorisation, la validation des entrées, la limitation de débit, les en-têtes HTTP de sécurité, CORS, la gestion des secrets, la journalisation, la sécurité des dépendances, la gestion des erreurs et les vérifications de déploiement — avec des exemples Express et Node.js immédiatement réutilisables.

---

## Méthode de travail fiable

Appliquez un cycle simple pour chaque contrôle de sécurité :

- Entrée attendue : ce que vous modifiez (fichier, variable d’environnement, option Docker, middleware, etc.).

- Action : la commande ou la ligne de configuration appliquée.

- Sortie attendue : log, en-tête HTTP, code de statut, socket ouvert/fermé, métrique, etc.

- Signal d’échec : ce qui doit échouer et comment (message, code, absence d’en-tête, refus de connexion).

Les hypothèses cachées (chemins, tags d’images, noms de réseau, fichiers .env, limites de ressources, permissions) varient selon les laptops, runners et hôtes de production. Les rendre explicites évite les écarts entre environnements.

---

## Plan d’essai local et exemples concrets

Chaque exemple suit la structure ressource → configuration → preuve → échec attendu.

### Exemple 1 — Exposition réseau minimale de l’API (Docker)

- Ressource : port et interface d’écoute de l’API Express.

- Configuration : binder sur 127.0.0.1 côté hôte pour éviter l’exposition involontaire.

Commande :

docker run --rm \
 -p 127.0.0.1:3000:3000 \
 --name api \
 your-api-image:latest 
 Vérification :

curl -i http://127.0.0.1:3000/health
# Attendu: HTTP/1.1 200 OK 
 Signal d’échec : depuis une autre machine du réseau, la connexion doit échouer. Si vous aviez publié -p 3000:3000 (sur 0.0.0.0), un accès distant réussirait — anti-signal de sécurité par défaut.

---

### Exemple 2 — Contrôle d’accès par clé d’API (authentification simple)

- Ressource : couche d’authentification sur un endpoint.

- Configuration : middleware Express vérifiant x-api-key contre process.env.API_KEY .

Code minimal :

// server.js
const express = require('express');
const app = express();

function requireApiKey(req, res, next) {
 const key = req.header('x-api-key');
 if (!key || key !== process.env.API_KEY) {
 return res.status(401).json({ error: 'unauthorized' });
 }
 next();
}

app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.get('/secure', requireApiKey, (req, res) => res.json({ data: 'secret' }));

app.listen(3000, () => console.log('API listening on 3000')); 
 Lancement local :

export API_KEY="change-me-strong-key"
node server.js 
 Tests :

# Sans clé → doit échouer
curl -i http://127.0.0.1:3000/secure
# Attendu: HTTP/1.1 401 Unauthorized

# Avec la clé correcte → doit réussir
curl -i -H 'x-api-key: change-me-strong-key' http://127.0.0.1:3000/secure
# Attendu: HTTP/1.1 200 OK 
 Signal d’échec : si /secure répond 200 sans en-tête, le middleware est absent ou API_KEY est vide/mal définie.

---

### Authentification JWT et autorisation par rôles

- Ressource : endpoints protégés, authentification stateless, autorisation fine.

- Configuration : validation d’un JWT signé, extraction de rôles, contrôle d’accès par rôle.

Code d’exemple (CommonJS) :

// auth.js
const jwt = require('jsonwebtoken');

function requireJwt(req, res, next) {
 const auth = req.header('Authorization') || '';
 const token = auth.startsWith('Bearer ') ? auth.slice(7) : null;
 if (!token) return res.status(401).json({ error: 'missing_token' });
 try {
 const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY, { algorithms: ['RS256'] });
 req.user = payload; // { sub, roles, ... }
 next();
 } catch (e) {
 return res.status(401).json({ error: 'invalid_token' });
 }
}

function requireRole(...allowed) {
 return (req, res, next) => {
 const roles = (req.user && req.user.roles) || [];
 const ok = roles.some(r => allowed.includes(r));
 if (!ok) return res.status(403).json({ error: 'forbidden' });
 next();
 };
}

module.exports = { requireJwt, requireRole }; 
 Intégration :

// server-jwt.js
const express = require('express');
const { requireJwt, requireRole } = require('./auth');
const app = express();

app.get('/me', requireJwt, (req, res) => res.json({ user: req.user.sub }));
app.get('/admin', requireJwt, requireRole('admin'), (req, res) => res.json({ ok: true }));

app.listen(3000, () => console.log('API listening on 3000')); 
 Tests (générer un JWT RS256 avec roles:["admin"] ) :

# Sans token
curl -i http://127.0.0.1:3000/me
# Attendu: 401

# Avec token valide sans rôle admin
curl -i -H "Authorization: Bearer $JWT_NO_ADMIN" http://127.0.0.1:3000/admin
# Attendu: 403

# Avec token valide et rôle admin
curl -i -H "Authorization: Bearer $JWT_ADMIN" http://127.0.0.1:3000/admin
# Attendu: 200 
 Signaux d’échec :

- 200 sans token → middleware non branché.

- 200 sur /admin avec token sans rôle → bug de logique d’autorisation.

---

### Validation des entrées (prévenir l’injection et les 400/500 inutiles)

- Ressource : corps, paramètres, requêtes.

- Configuration : schémas de validation systématiques, rejet explicite des champs inconnus.

Exemple avec zod et un middleware simple :

// validation.js
const { z } = require('zod');

const createUserSchema = z.object({
 email: z.string().email(),
 password: z.string().min(12).max(128),
 name: z.string().min(1),
}).strict(); // rejette les champs non attendus

function validate(schema) {
 return (req, res, next) => {
 const parse = schema.safeParse(req.body);
 if (!parse.success) {
 return res.status(400).json({ error: 'invalid_input', details: parse.error.issues });
 }
 req.validated = parse.data;
 next();
 };
}

module.exports = { createUserSchema, validate }; 
 Intégration :

// server-validate.js
const express = require('express');
const { createUserSchema, validate } = require('./validation');
const app = express();
app.use(express.json({ limit: '1mb' }));

app.post('/users', validate(createUserSchema), (req, res) => {
 // Utilisez req.validated
 res.status(201).json({ id: 'u_123', email: req.validated.email });
});

app.listen(3000); 
 Tests :

# Mauvais email → 400
curl -i -H 'Content-Type: application/json' -d '{"email":"x","password":"0123456789abcd","name":"A"}' http://127.0.0.1:3000/users

# Champ en trop → 400 (strict)
curl -i -H 'Content-Type: application/json' -d '{"email":"a@b.c","password":"0123456789abcd","name":"A","role":"admin"}' http://127.0.0.1:3000/users

# OK → 201
curl -i -H 'Content-Type: application/json' -d '{"email":"a@b.c","password":"0123456789abcd","name":"A"}' http://127.0.0.1:3000/users 
 Signaux d’échec : absence de 400 sur données invalides, ou crash 500 → schéma manquant/mauvaise gestion d’erreurs.

---

### Limitation de débit et protection contre la force brute

- Ressource : endpoints sensibles (auth, création de ressources).

- Configuration : express-rate-limit et ralentissement progressif.

Exemple :

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
 windowMs: 15 * 60 * 1000, // 15 minutes
 max: 100, // 100 requêtes / fenêtre / IP
 standardHeaders: true,
 legacyHeaders: false,
});

// Plus strict pour /login
const authLimiter = rateLimit({ windowMs: 10 * 60 * 1000, max: 10 });

app.use(limiter);
app.post('/login', authLimiter, (req, res) => { /* ... */ }); 
 Test :

for i in $(seq 1 12); do curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:3000/login; done
# Les dernières requêtes doivent retourner 429 
 Signal d’échec : absence de 429 après dépassement des seuils → middleware non appliqué.

Astuce : pour éviter le contournement IP derrière un proxy, configurez app.set('trust proxy', 1) côté Express lorsque vous êtes derrière un reverse proxy.

---

### CORS (exposition contrôlée aux navigateurs)

- Ressource : origine et méthodes autorisées côté navigateur.

- Configuration : paquet cors avec liste d’origines de confiance et méthodes limitées.

Exemple :

const cors = require('cors');

const allowlist = [
 'https://app.example.com',
 'https://admin.example.com'
];

app.use(cors({
 origin: (origin, cb) => {
 if (!origin) return cb(null, false); // désactive CORS pour requêtes sans origin
 cb(null, allowlist.includes(origin));
 },
 methods: ['GET', 'POST', 'PUT', 'DELETE'],
 allowedHeaders: ['Authorization', 'Content-Type', 'x-api-key'],
 maxAge: 600,
 credentials: false,
})); 
 Tests :

# Origine non autorisée → pas d'en-têtes CORS
curl -i -H 'Origin: https://evil.local' http://127.0.0.1:3000/health | grep -i 'access-control-allow-origin' || echo 'CORS: non exposé (OK)'

# Origine autorisée → en-tête présent
curl -i -H 'Origin: https://app.example.com' http://127.0.0.1:3000/health | grep -i 'access-control-allow-origin' 
 Signal d’échec : présence d’un Access-Control-Allow-Origin: * inattendu ou exposition globale.

Note : si vous utilisez des cookies/session, activez credentials:true , forcez HTTPS et définissez SameSite=None; Secure côté cookie.

---

### En-têtes de protection HTTP et durcissement Express

- Ressource : surface d’attaque côté client.

- Configuration : helmet , désactivation de x-powered-by , HSTS, MIME sniffing, etc.

Exemple :

const helmet = require('helmet');

app.disable('x-powered-by');
app.use(helmet({
 contentSecurityPolicy: false, // activez et affinez si vous servez du HTML
 frameguard: { action: 'deny' },
 hsts: { maxAge: 15552000, includeSubDomains: true, preload: false },
 xssFilter: true,
})); 
 Vérification :

curl -i http://127.0.0.1:3000/health | grep -E 'X-Content-Type-Options|X-Frame-Options|Strict-Transport-Security' || true 
 Signal d’échec : absence d’en-têtes attendus → ordre de middlewares incorrect, route qui court-circuite ou helmet manquant.

Tableau utile (extraits) :

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">En-tête</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">But</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Attendu</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">X-Content-Type-Options</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Empêche le MIME sniffing</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">nosniff</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">X-Frame-Options</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Empêche l’iframe clickjacking</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">DENY/SAMEORIGIN</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Strict-Transport-Security</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Force HTTPS côté client</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">max-age≥15552000</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Referrer-Policy</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Réduit les fuites de référent</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">no-referrer / strict-origin-when-cross-origin</td></tr></tbody></table></div>
---

### Secrets d’exécution (.env, variables, durcissement)

- Ressource : clés API, tokens DB, clés JWT.

- Configuration : variables d’environnement, .env ignoré par Git, journalisation épurée.

.env local (non committé) :

API_KEY=change-me-strong-key
MONGODB_URI=mongodb://mongo:27017/app
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..." 
 Lancement Docker :

docker run --rm \
 --env-file .env \
 -p 127.0.0.1:3000:3000 \
 --name api your-api-image:latest 
 Vérification :

docker inspect api --format '{{json .Config.Env}}' | jq '.' | grep API_KEY
# Attendu: variable présente côté conteneur 
 Signaux d’échec :

- Secret dans les logs → filtrer/redacter, éviter console.log(process.env) .

- .env versionné → ajouter au .gitignore et vérifier : git check-ignore -v .env .

Bonnes pratiques :

- Utiliser des secrets spécifiques par environnement et rotatifs.

- En production, injecter via le gestionnaire de secrets de l’infra (variables d’environnement au runtime) et ne jamais les baker dans l’image.

---

### Permissions, utilisateur non-root et système de fichiers en lecture seule

- Ressource : identités d’exécution et accès disque.

- Configuration : exécuter en non-root, --read-only , --tmpfs , limites CPU/Mémoire.

Commande :

docker run --rm \
 --env-file .env \
 -p 127.0.0.1:3000:3000 \
 --user node \
 --read-only \
 --tmpfs /tmp:rw,noexec,nosuid,size=64m \
 --memory 256m --cpus 0.5 \
 --name api your-api-image:latest 
 Vérifications :

docker exec -it api sh -c 'id && touch /root/test || echo EROFS-or-permission'
# Attendu: utilisateur non-root, écriture bloquée hors /tmp (EROFS/permission) 
 Signal d’échec : si l’écriture fonctionne partout, --read-only est absent ou un volume RW couvre le système.

---

### Réseau interne avec MongoDB (pas d’exposition publique)

- Ressource : connectivité API ↔ DB uniquement en réseau interne.

- Configuration : réseau Docker dédié, aucun port publié pour MongoDB.

Mise en place :

docker network create api-net

docker run -d --name mongo --network api-net mongo:7

docker run --rm \
 --env-file .env \
 --network api-net \
 -p 127.0.0.1:3000:3000 \
 --name api your-api-image:latest 
 Vérifications :

# Depuis l'hôte, l'accès direct au port 27017 doit échouer
nc -zv 127.0.0.1 27017 || echo "MongoDB non exposé: OK"

# Depuis l'API, la résolution interne doit marcher
docker exec -it api sh -c 'getent hosts mongo && echo OK' 
 Signal d’échec : -p 27017:27017 publié par erreur → nc -zv 127.0.0.1 27017 réussit.

---

### Journalisation, traçabilité et corrélation

- Ressource : logs exploitables et non bavards en secrets.

- Configuration : logs JSON structurés, redaction des champs sensibles, correlation ID.

Exemple avec pino  :

const pino = require('pino');
const logger = pino({ level: process.env.LOG_LEVEL || 'info', redact: ['req.headers.authorization', 'password', 'token'] });

function attachLogger(req, res, next) {
 const rid = req.header('x-request-id') || Math.random().toString(36).slice(2);
 req.id = rid;
 req.log = logger.child({ reqId: rid });
 res.setHeader('x-request-id', rid);
 next();
}

app.use(attachLogger);
app.use((req, res, next) => { req.log.info({ method: req.method, url: req.url }, 'request'); next(); }); 
 Test :

curl -i http://127.0.0.1:3000/health | grep -i x-request-id
# Attendu: en-tête présent, logs JSON contenant reqId 
 Signal d’échec : secrets visibles dans les logs, absence d’identifiant corrélé entre services.

---

### Sécurité des dépendances et de la chaîne Node.js

- Ressource : bibliothèques et runtime.

- Configuration : mises à jour, audit, builds reproductibles.

Commandes utiles :

# Verrouiller les versions et installer proprement en CI
npm ci --omit=dev

# Identifier les vulnérabilités
npm audit --production

# Vérifier les mises à jour de sécurité
npm outdated

# Lancer les tests de sécurité SAST/DB (si outil externe disponible)
# ex: snyk test, dependabot, etc. 
 Bonnes pratiques Dockerfile (extrait) :

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
USER node
CMD ["node", "server.js"] 
 Signaux d’échec : installation non déterministe ( npm install sans lockfile), exécution en root, versions obsolètes connues vulnérables.

---

### Gestion des erreurs (ne pas fuiter en production)

- Ressource : réponses HTTP cohérentes, pas de stacktrace en clair.

- Configuration : middleware d’erreurs central, shape de réponse stable, NODE_ENV=production .

Exemple :

// Middleware 404 explicite
app.use((req, res) => res.status(404).json({ error: 'not_found' }));

// Middleware d'erreurs
app.use((err, req, res, next) => {
 const status = err.status || 500;
 const body = { error: status === 500 ? 'internal_error' : err.message };
 if (process.env.NODE_ENV !== 'production') body.stack = err.stack;
 req?.log?.error({ err, status }, 'unhandled_error');
 res.status(status).json(body);
}); 
 Tableau de codes conseillés (exemples) :

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Cas</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Code</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Corps minimal</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Données invalides</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">400</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "invalid_input" }</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Manque d’authentification</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">401</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "unauthorized" }</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Interdit (pas le bon rôle)</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">403</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "forbidden" }</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Ressource absente</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">404</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "not_found" }</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Trop de requêtes</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">429</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "rate_limited" }</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Erreur serveur</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">500</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">{ error: "internal_error" }</td></tr></tbody></table></div>
Signaux d’échec : stacktrace renvoyée en clair en production, formats incohérents compliquant les clients.

---

## Vérifications de déploiement (pré-prod/production)

Checkpoints concrets à exécuter avant/après déploiement :

- Ports ouverts attendus uniquement : ss -lntp ou netstat -tulpen côté hôte.

- Santé : endpoints /health et /ready distincts (liveness / readiness).

- TLS : terminée en amont (LB) avec redirection HTTPS et HSTS côté API si nécessaire.

- trust proxy activé si API derrière un proxy : app.set('trust proxy', 1) .

- Variables d’environnement présentes : printenv | sort dans le conteneur.

- NODE_ENV=production effectif, pas de dépendances dev en runtime.

- Log JSON et x-request-id visibles de bout en bout.

- Limiteurs actifs (tests 429) et CORS vérifié sur domaines autorisés.

Exemples rapides :

# Santé
curl -i http://127.0.0.1:3000/health

# Readiness
curl -i http://127.0.0.1:3000/ready

# Redirection HTTPS forcée (si configurée)
curl -I http://api.example.com | grep -i location

# HSTS (si HTTPS servi par l’API)
curl -I https://api.example.com | grep -i strict-transport-security 
 ---

## Tableau récapitulatif des contrôles et vérifications

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Objectif</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Mécanisme</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Paquet/outil</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Vérification</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Échec attendu</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Exposition minimale</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Bind 127.0.0.1</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Docker</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">curl 200 local</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Accès distant possible</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Auth simple</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">x-api-key</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Express</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">401 sans clé</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">200 sans clé</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Auth/AutZ avancée</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">JWT + rôles</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">jsonwebtoken</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">401/403 selon cas</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">200 sans rôle</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Validation</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Schémas</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">zod</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">400 invalide</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">200 sur payload invalide</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rate limiting</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Fenêtre + max</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">express-rate-limit</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">429 après N</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Jamais 429</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">CORS</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Liste d’origines</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">cors</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">En-têtes CORS ciblés</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">&lt;code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded"&gt;*&lt;/code&gt; inattendu</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">En-têtes sécurité</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Helmet</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">helmet</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Présence HSTS, X-CTO</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Absence d’en-têtes</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Secrets</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Env + .gitignore</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Docker/env</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Présence Env</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Secret loggé/committé</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Permissions</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">non-root + RO FS</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Docker</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">EROFS hors /tmp</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Écriture partout</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Réseau interne</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Réseau dédié</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Docker</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">&lt;code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded"&gt;nc&lt;/code&gt; échoue host</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">DB exposée</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Logs</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">JSON + reqId</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">pino</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">x-request-id</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Pas d’ID corrélé</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Dépendances</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Audit/lock</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">npm audit/ci</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">0 vuln crit</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Packages vulnérables</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Erreurs</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Handler central</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Express</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">4xx/5xx propres</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Stack en clair</td></tr></tbody></table></div>
---

## Conclusion

Le durcissement d’une API Express devient efficace quand chaque contrôle est traité comme un composant testable : configuration explicite, preuve locale reproductible et signaux d’échec clairs. En appliquant systématiquement : authentification/autorisation robustes, validation stricte des entrées, limitation de débit, en-têtes de sécurité, CORS maîtrisé, secrets bien gérés, journalisation structurée, dépendances à jour, gestion d’erreurs propre et vérifications de déploiement, vous améliorez concrètement la sécurité et la résilience de votre API.

---

## Liste de vérification finale (à cocher avant prod)

- [ ] L’API n’est pas exposée au-delà de ce qui est nécessaire (bind/pare-feu/ingress).

- [ ] Authentification en place (API key/JWT), autorisation par rôle testée (401/403).

- [ ] Validation stricte de toutes les entrées (schémas, 400 sur invalide).

- [ ] Limitation de débit active sur endpoints sensibles (429 vérifié).

- [ ] CORS restreint aux origines autorisées (pas de * involontaire).

- [ ] En-têtes de sécurité présents (Helmet, HSTS si pertinent, X-CTO, XFO, RP).

- [ ] Secrets injectés via l’environnement, jamais commités ni loggés.

- [ ] Process non-root, FS en lecture seule, tmpfs configuré.

- [ ] Réseau interne pour la base de données, aucun port DB publié.

- [ ] Logs JSON, redaction des secrets, x-request-id de bout en bout.

- [ ] npm ci --omit=dev , npm audit --production propres, Node à jour.

- [ ] Middleware d’erreurs central, pas de stacktrace en clair en production.

- [ ] Endpoints /health et /ready opérationnels, NODE_ENV=production actif.