Le API sono diventate il bersaglio preferito degli attaccanti in Europa. Il report Akamai 2026 sullo stato di Internet segnala che gli attacchi web nell’area EMEA hanno toccato il massimo degli ultimi due anni, con un aumento del 36% su base annua, mentre gli attacchi DDoS a livello applicativo (Layer 7, che includono le API HTTP) sono saliti del 37% nello stesso periodo, arrivando a 188 miliardi di eventi registrati a novembre 2025. In un altro studio Akamai condotto su Francia, Germania e Regno Unito, l’88% delle organizzazioni ha dichiarato di aver subito almeno un incidente legato alla sicurezza delle proprie API. Questa guida mostra come costruire, passo dopo passo, un API gateway sicuro in Node.js: autenticazione centralizzata, rate limiting, validazione dell’input, header di sicurezza e logging degli accessi, con codice pronto da adattare a un ambiente di produzione.

Per le aziende italiane ed europee il tema non è solo tecnico. Ogni violazione che espone dati personali attraverso un’API mal protetta ricade sotto il GDPR, con obblighi di notifica e possibili sanzioni indipendenti dalla causa tecnica dell’incidente. Un gateway ben configurato non elimina il rischio, ma riduce sensibilmente la superficie d’attacco esposta e produce i log necessari a dimostrare, in caso di controllo, quali controlli erano attivi nel momento della violazione.

Cos’è un API gateway e perché nel 2026 è il primo bersaglio

Un API gateway è il punto d’ingresso unico attraverso cui passano tutte le richieste dirette ai microservizi di un’applicazione. Invece di esporre ogni singolo servizio direttamente su Internet, le richieste arrivano prima al gateway, che le autentica, le filtra e le instrada verso il backend corretto. Questa centralizzazione è comoda per chi sviluppa, ma trasforma il gateway in un obiettivo ad alto valore: chi riesce a comprometterlo ottiene potenzialmente accesso a tutto ciò che sta dietro.

Il report API ThreatStats 2026 di Wallarm, intitolato “AI: The New API Risk Multiplier”, segnala che le piattaforme e gli strumenti di intelligenza artificiale hanno rappresentato il 15% delle violazioni legate alle API nel 2025, a pari merito con il software tradizionale come categoria più colpita. Wallarm attribuisce parte di questa crescita all’automazione: gli attaccanti usano bot e agenti automatici per testare migliaia di endpoint in poche ore, cercando errori di configurazione, autenticazione debole o controlli di autorizzazione mancanti (le vulnerabilità note come BOLA e IDOR, che l’OWASP API Security Top 10 colloca ai primi posti della classifica).

Un dato che dovrebbe far riflettere chi progetta i controlli di sicurezza: secondo Salt Security, il 99% dei tentativi di attacco analizzati proveniva da fonti già autenticate, cioè da credenziali o chiavi API valide usate in modo anomalo, non da traffico anonimo bloccabile con una semplice lista nera di IP. Per questo un gateway moderno non può limitarsi a verificare “chi sei”, deve anche osservare “cosa stai facendo” e a che ritmo lo stai facendo.

La logica di “zero trust” applicata alle API parte proprio da qui: nessuna richiesta viene considerata affidabile solo perché arriva da dietro un firewall aziendale o da un IP interno. Ogni chiamata, anche tra due microservizi della stessa infrastruttura, viene autenticata e autorizzata individualmente. Costruire questo comportamento in un gateway centralizzato, invece di replicarlo in ogni singolo servizio, è il modo più pratico per applicarlo senza raddoppiare il lavoro di sviluppo a ogni nuovo endpoint.

Prerequisiti: strumenti e versioni necessarie

Prima di iniziare, prepara l’ambiente di sviluppo. Il progetto usa Node.js con Express e alcuni pacchetti npm per autenticazione, rate limiting e validazione. La tabella seguente riporta le versioni verificate al momento della stesura di questa guida.

Strumento / pacchettoVersione di riferimentoRuolo nel progetto
Node.js24.20.0 LTS (“Krypton”)Runtime JavaScript
Express5.2.1Framework HTTP per il gateway
http-proxy-middleware4.2.0Proxy verso i microservizi
jsonwebtoken9.0.3Firma e verifica dei token JWT
express-rate-limit8.7.0Limitazione delle richieste
ioredis6.0.0Store condiviso per rate limiting distribuito
helmet8.3.0Header HTTP di sicurezza
express-validator7.3.2Validazione e sanitizzazione input
winston3.19.0Logging strutturato
dotenv17.4.2Variabili d’ambiente

Servono inoltre: un editor di codice, Docker (per il passo finale di deployment), un’istanza Redis locale o in un container, e conoscenze di base di JavaScript asincrono e del protocollo HTTP. Non è necessaria esperienza pregressa con i gateway commerciali: costruiremo tutto da zero per capire ogni singolo meccanismo di difesa, e alla fine vedremo anche come questi stessi principi si applicano a gateway già pronti come Kong, NGINX o AWS API Gateway.

Passo 1: struttura del progetto e installazione delle dipendenze

Crea una cartella per il progetto e inizializza il pacchetto npm. La struttura che useremo separa la configurazione, i middleware di sicurezza e le rotte, in modo da poter aggiungere o rimuovere controlli senza toccare il resto del codice.

mkdir api-gateway-sicuro && cd api-gateway-sicuro
npm init -y
npm install [email protected] [email protected] [email protected] \
  [email protected] [email protected] [email protected] \
  [email protected] [email protected] [email protected]

mkdir -p src/middleware src/routes src/config
touch src/server.js src/config/env.js .env .gitignore

Nel file .gitignore aggiungi almeno node_modules, .env e i log locali: il file .env conterrà segreti (chiave JWT, credenziali Redis) e non va mai versionato. Nel file .env inserisci i valori di base:

PORT=3000
JWT_SECRET=cambia-questo-valore-con-32-byte-casuali
REDIS_URL=redis://localhost:6379
BACKEND_USERS_URL=http://localhost:4001
BACKEND_ORDERS_URL=http://localhost:4002
NODE_ENV=development

Genera il valore di JWT_SECRET con un comando che produce byte veramente casuali, non una stringa scelta a mano: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". In produzione questo segreto va gestito con un secret manager (AWS Secrets Manager, HashiCorp Vault o equivalente), mai scritto in un file di configurazione che finisce in un repository.

Passo 2: il gateway di base con proxy verso i microservizi

Il cuore del gateway è il proxy inverso: riceve la richiesta del client e la inoltra al microservizio corretto in base al percorso. Con http-proxy-middleware questa parte richiede poche righe, ma è il punto in cui va inserita tutta la catena di controlli di sicurezza, prima che la richiesta lasci il gateway.

// src/server.js
require('dotenv').config();
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const helmet = require('helmet');

const app = express();
app.disable('x-powered-by');
app.use(helmet());
app.use(express.json({ limit: '100kb' }));

// Rotte pubbliche (login, health check) definite prima del proxy autenticato
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Proxy verso il microservizio utenti
app.use('/api/users', createProxyMiddleware({
  target: process.env.BACKEND_USERS_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

// Proxy verso il microservizio ordini
app.use('/api/orders', createProxyMiddleware({
  target: process.env.BACKEND_ORDERS_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
}));

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Gateway attivo sulla porta ${port}`));

A questo stadio il gateway funziona, ma è ancora scoperto: chiunque conosca l’URL può chiamare direttamente i microservizi passando dal proxy, senza fornire alcuna credenziale. Nei prossimi passi aggiungiamo, uno alla volta, i controlli che mancano: autenticazione, autorizzazione, limitazione del traffico, validazione dell’input e osservabilità.

Passo 3: autenticazione JWT centralizzata

Uno dei vantaggi principali di un gateway è poter verificare l’identità del chiamante una sola volta, invece di duplicare la logica di autenticazione in ogni microservizio. Il pattern più diffuso usa i JSON Web Token: il client ottiene un token firmato al momento del login, e lo allega a ogni richiesta successiva nell’header Authorization. Se vuoi approfondire il funzionamento dei token e la loro implementazione lato applicazione, la nostra guida sull’autenticazione JWT in Node.js copre la parte lato singolo servizio. Qui vediamo come applicarla a livello di gateway.

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

function verificaToken(req, res, next) {
  const header = req.headers['authorization'];
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token mancante' });
  }

  const token = header.split(' ')[1];

  jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }, (err, payload) => {
    if (err) {
      return res.status(401).json({ error: 'Token non valido o scaduto' });
    }
    req.user = payload; // { sub, ruolo, scope, iat, exp }
    next();
  });
}

module.exports = { verificaToken };

Nota la voce algorithms: ['HS256'] passata esplicitamente a jwt.verify: senza questo vincolo, un client malevolo potrebbe provare a inviare un token firmato con l’algoritmo none o con una chiave pubblica al posto di quella simmetrica, un attacco noto contro implementazioni JWT permissive. Specificare sempre l’algoritmo atteso chiude questa falla alla radice.

Applica il middleware alle rotte protette in server.js, aggiungendo la riga prima di ogni createProxyMiddleware:

const { verificaToken } = require('./middleware/auth');

app.use('/api/users', verificaToken, createProxyMiddleware({
  target: process.env.BACKEND_USERS_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

Passo 4: autorizzazione basata su ruoli e scope

Autenticare un utente risponde alla domanda “chi sei”, ma non basta: bisogna anche stabilire “cosa puoi fare”. La maggior parte degli incidenti legati alle API nel 2025-2026, secondo le analisi di Akamai e Wallarm citate in apertura, nasce proprio da controlli di autorizzazione mancanti o troppo permissivi, non da password deboli. Un utente autenticato che riesce a leggere i dati di un altro utente semplicemente cambiando un ID nell’URL è un classico caso di BOLA (Broken Object Level Authorization).

// src/middleware/autorizza.js
function richiedeRuolo(...ruoliConsentiti) {
  return (req, res, next) => {
    if (!req.user || !ruoliConsentiti.includes(req.user.ruolo)) {
      return res.status(403).json({ error: 'Permessi insufficienti' });
    }
    next();
  };
}

// Verifica che l'utente stia accedendo solo alle proprie risorse
function proprietaRisorsa(req, res, next) {
  const idRichiesto = req.params.id || req.body.userId;
  if (req.user.ruolo !== 'admin' && idRichiesto && idRichiesto !== req.user.sub) {
    return res.status(403).json({ error: 'Non puoi accedere a risorse di altri utenti' });
  }
  next();
}

module.exports = { richiedeRuolo, proprietaRisorsa };

Il middleware proprietaRisorsa è deliberatamente semplice: confronta l’ID richiesto con l’identità presente nel token, a meno che l’utente non sia amministratore. In un sistema reale questa logica va adattata al modello dati, ma il principio resta: non fidarti mai di un ID passato dal client senza verificare che corrisponda a chi ha effettuato la richiesta.

Passo 5: rate limiting distribuito con Redis

Il rate limiting è la difesa più diretta contro gli abusi da credenziali valide descritti da Salt Security. Un limite basato solo sulla memoria del processo Node.js funziona su un singolo server, ma smette di essere efficace appena il gateway viene distribuito su più istanze dietro un load balancer, perché ogni istanza conterebbe le richieste in modo indipendente. Per questo usiamo Redis come store condiviso. Abbiamo trattato il tema in dettaglio nella guida dedicata al rate limiting in Node.js. Qui lo adattiamo al contesto multi-istanza di un gateway.

// src/middleware/limiter.js
const rateLimit = require('express-rate-limit');
const { RedisStore } = require('express-rate-limit');
const Redis = require('ioredis');

const redisClient = new Redis(process.env.REDIS_URL);

const limiter = rateLimit({
  windowMs: 60 * 1000, // finestra di 1 minuto
  limit: (req) => (req.user?.ruolo === 'admin' ? 300 : 60),
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.user?.sub || req.ip,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
  message: { error: 'Troppe richieste, riprova tra qualche istante' },
});

module.exports = { limiter };

Il parametro keyGenerator conta le richieste per utente autenticato (req.user.sub) invece che per indirizzo IP quando possibile: questo evita che un’intera azienda dietro un unico IP condiviso venga bloccata per il comportamento di un solo utente, e allo stesso tempo impedisce a un singolo account compromesso di generare traffico illimitato cambiando IP.

Passo 6: validare e sanitizzare l’input in ingresso

Ogni richiesta che attraversa il gateway dovrebbe essere validata prima di raggiungere il microservizio a valle: un controllo centralizzato riduce il rischio che un singolo servizio dimentichi di validare un campo e diventi il punto debole dell’intera architettura. La guida sulla validazione input in Node.js tratta gli schemi di validazione in profondità. Qui vediamo un esempio applicato a una rotta del gateway.

// src/routes/ordini.js
const { body, param, validationResult } = require('express-validator');

const validaCreazioneOrdine = [
  body('prodottoId').isUUID().withMessage('ID prodotto non valido'),
  body('quantita').isInt({ min: 1, max: 100 }).withMessage('Quantità fuori range'),
  body('note').optional().isLength({ max: 500 }).trim().escape(),
  param('id').optional().isUUID(),
  (req, res, next) => {
    const errori = validationResult(req);
    if (!errori.isEmpty()) {
      return res.status(400).json({ errori: errori.array() });
    }
    next();
  },
];

module.exports = { validaCreazioneOrdine };

Il metodo .escape() sul campo note neutralizza i caratteri HTML pericolosi, riducendo il rischio di iniezioni se quel testo viene poi mostrato in un’interfaccia web. Limitare esplicitamente tipo, formato e lunghezza di ogni campo, invece di accettare qualsiasi JSON valido, è quello che separa un endpoint robusto da uno che accetta payload arbitrari costruiti ad arte.

Passo 7: header di sicurezza, CORS e protezione dei dati in transito

Il pacchetto Helmet, già inserito al passo 2, imposta una serie di header HTTP che riducono la superficie d’attacco lato browser (protezione contro il MIME sniffing, header Strict-Transport-Security, politiche di riferimento più severe). Va comunque configurato con attenzione al contesto: un gateway che serve sia API sia contenuti statici richiede una Content Security Policy diversa da un gateway puramente API.

const cors = require('cors');

app.use(helmet({
  contentSecurityPolicy: { directives: { defaultSrc: ["'none'"] } },
  crossOriginResourcePolicy: { policy: 'same-site' },
}));

app.use(cors({
  origin: ['https://app.tuodominio.it', 'https://admin.tuodominio.it'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
  maxAge: 600,
}));

Evita la tentazione, comune in fase di sviluppo, di impostare origin: '*' insieme a credentials: true: i browser rifiutano questa combinazione per motivi di sicurezza, ed è comunque una configurazione da non portare mai in produzione. Elenca esplicitamente i domini autorizzati, anche se la lista va aggiornata a ogni nuovo frontend collegato al gateway. In produzione, l’intero gateway deve essere raggiungibile solo via HTTPS: la nostra guida su Let’s Encrypt e Certbot spiega come ottenere certificati TLS gratuiti e rinnovarli automaticamente.

Passo 8: logging strutturato e audit trail

Senza log dettagliati, individuare un abuso in corso o ricostruire un incidente dopo i fatti diventa quasi impossibile. Un gateway dovrebbe registrare, per ogni richiesta, chi ha chiamato, quale endpoint, con quale esito e in quanto tempo, in un formato strutturato che un sistema SIEM possa ingerire facilmente. Questo tipo di log è anche ciò che, in caso di incidente, permette di rispondere in poche ore invece che in giorni alla domanda “quali dati sono stati esposti e da chi”, un requisito pratico oltre che di conformità normativa.

// src/middleware/logging.js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
  transports: [
    new winston.transports.File({ filename: 'gateway-audit.log' }),
    new winston.transports.Console(),
  ],
});

function auditLog(req, res, next) {
  const inizio = Date.now();
  res.on('finish', () => {
    logger.info('richiesta_gateway', {
      utente: req.user?.sub || 'anonimo',
      metodo: req.method,
      percorso: req.originalUrl,
      stato: res.statusCode,
      durataMs: Date.now() - inizio,
      ip: req.ip,
    });
  });
  next();
}

module.exports = { auditLog, logger };

Piazza auditLog come primo middleware dell’applicazione, prima ancora dell’autenticazione: in questo modo anche i tentativi falliti (token mancante, IP bloccato) finiscono nel registro. Questi log alimentano poi un sistema di rilevamento: chi ha configurato un SIEM con Wazuh può reindirizzare direttamente questo output verso la propria pipeline, come descritto nella guida Wazuh per installazione e configurazione SIEM.

Passo 9: individuare e chiudere le shadow API

Le “shadow API” sono endpoint attivi ma non documentati o dimenticati: versioni precedenti di un’API mai disattivate, ambienti di test raggiungibili dall’esterno, rotte di debug lasciate accese per errore. Sia Akamai sia Wallarm indicano queste API fantasma come un fattore di rischio crescente, perché non ricevono gli stessi controlli e aggiornamenti delle API documentate. Con un gateway centralizzato, ogni richiesta passa comunque per un unico punto: puoi sfruttare questo fatto per costruire un inventario reale del traffico.

// src/middleware/inventario.js
const endpointVisti = new Map();

function tracciaEndpoint(req, res, next) {
  const chiave = `${req.method} ${req.route?.path || req.path}`;
  const conteggio = endpointVisti.get(chiave) || 0;
  endpointVisti.set(chiave, conteggio + 1);
  next();
}

// Esponi l'inventario solo agli amministratori
function inventarioAttuale(req, res) {
  res.json(Object.fromEntries(endpointVisti));
}

module.exports = { tracciaEndpoint, inventarioAttuale };

Un esempio così semplice va bene per capire il concetto, ma in un ambiente con più istanze del gateway serve un contatore condiviso (per esempio su Redis, come già fatto per il rate limiting) e un confronto periodico tra gli endpoint effettivamente chiamati e quelli documentati nello schema OpenAPI. Qualsiasi endpoint che riceve traffico ma non compare nello schema ufficiale va indagato: potrebbe essere un’integrazione legittima non documentata, oppure un varco che nessuno sta monitorando.

Passo 10: gestire agenti automatici e traffico autenticato anomalo

Il dato di Salt Security citato all’inizio, il 99% degli attacchi analizzati partito da fonti autenticate, richiede un livello di difesa che va oltre l’autenticazione stessa: bisogna osservare i pattern di comportamento. Un client legittimo chiama tipicamente pochi endpoint in sequenze prevedibili, mentre un bot che sta enumerando l’intero spazio di ID di un’API genera un pattern molto diverso, anche restando sotto il limite di richieste al minuto.

// src/middleware/anomalie.js
const NodeCache = require('node-cache');
const cachePattern = new NodeCache({ stdTTL: 300 });

function rilevaScansioneSequenziale(req, res, next) {
  if (!req.user) return next();
  const chiave = `pattern:${req.user.sub}`;
  const idRichiesti = cachePattern.get(chiave) || [];
  const idCorrente = req.params.id;

  if (idCorrente) {
    idRichiesti.push(idCorrente);
    cachePattern.set(chiave, idRichiesti.slice(-20));

    const numeriUnici = new Set(idRichiesti.filter(id => /^\d+$/.test(id)));
    if (numeriUnici.size >= 15) {
      logger.warn('possibile_enumerazione', { utente: req.user.sub, endpoint: req.path });
      return res.status(429).json({ error: 'Attività sospetta rilevata, contatta il supporto' });
    }
  }
  next();
}

module.exports = { rilevaScansioneSequenziale };

Questo controllo intercetta il caso specifico di un utente che richiede ID numerici sequenziali (1, 2, 3…) in rapida successione, un pattern tipico di uno script che sta enumerando risorse altrui. Non sostituisce un sistema di rilevamento anomalie completo, ma dimostra il principio: la difesa più efficace contro il traffico autenticato malevolo guarda al comportamento, non solo alla validità del token.

Passo 11: testare il gateway end-to-end

Prima di passare al deployment, verifica ogni controllo con richieste reali. Avvia il gateway con node src/server.js e prova questi scenari con curl.

# Richiesta senza token: deve tornare 401
curl -i http://localhost:3000/api/users/42

# Richiesta con token valido
curl -i http://localhost:3000/api/orders \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{"prodottoId":"550e8400-e29b-41d4-a716-446655440000","quantita":2}'

# Test del rate limit: ripeti 70 volte in un minuto
for i in $(seq 1 70); do curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:3000/api/orders -H "Authorization: Bearer $TOKEN"; done

L’output atteso per il primo test è un corpo JSON simile a {"error":"Token mancante"} con stato 401. Per il test del rate limit, le prime 60 richieste (per un utente non amministratore, secondo la configurazione del passo 5) devono tornare 200 o il codice del backend, mentre dalla 61esima in poi il gateway deve rispondere 429 con il messaggio configurato. Se non vedi questo comportamento, salta alla sezione di risoluzione dei problemi più avanti in questa guida.

Passo 12: deployment in produzione con Docker

In produzione il gateway va eseguito in un container separato dai microservizi, con Redis raggiungibile in rete interna e mai esposto direttamente su Internet. Un Dockerfile minimale per questo progetto:

FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]

Nota l’istruzione USER node: l’immagine ufficiale di Node.js include già un utente non privilegiato, ed eseguire il processo come root dentro il container è un errore comune che amplia inutilmente l’impatto di un’eventuale compromissione. In un file docker-compose.yml puoi collegare il gateway a Redis e ai microservizi sulla stessa rete interna, esponendo pubblicamente solo la porta del gateway.

Errori comuni da evitare quando metti in sicurezza un API gateway

  • Validare solo lato frontend. Un controllo fatto solo nell’interfaccia utente non protegge nulla: chiunque può chiamare l’API direttamente con curl o Postman, saltando completamente il frontend.
  • Usare lo stesso segreto JWT per ambienti diversi. Condividere la chiave tra sviluppo, staging e produzione significa che una fuga di dati in un ambiente meno protetto compromette anche quello di produzione.
  • Rate limit basato solo sull’indirizzo IP. Dietro un NAT aziendale o una VPN, centinaia di utenti legittimi condividono lo stesso IP: un limite troppo aggressivo per IP li blocca tutti insieme, mentre uno per singolo utente autenticato è più preciso.
  • Restituire messaggi di errore troppo dettagliati. Un errore che rivela lo stack trace o la struttura del database aiuta l’attaccante a mappare il sistema; restituisci messaggi generici al client e i dettagli solo nei log interni.
  • Dimenticare la scadenza dei token. Token JWT con una durata di validità di mesi o senza scadenza trasformano ogni fuga di credenziali in un problema permanente; usa scadenze brevi (15-60 minuti) con un meccanismo di refresh token separato.
  • Non aggiornare le dipendenze del gateway. Il gateway è il componente più esposto dell’architettura: una vulnerabilità in Express o in una libreria di parsing JSON diventa immediatamente sfruttabile da Internet.
  • Autorizzare in base al ruolo ma non al contesto. Un amministratore che opera normalmente da un ufficio in Italia e che improvvisamente effettua centinaia di chiamate da un IP estero nel giro di pochi minuti merita un controllo aggiuntivo, anche se il suo token è tecnicamente valido.
  • Bloccare tutto il traffico dopo un errore di configurazione. Un middleware di sicurezza scritto senza gestione delle eccezioni può far cadere l’intero gateway per un singolo campo mancante; avvolgi sempre la logica critica in blocchi try/catch e restituisci un errore controllato invece di un crash del processo.

Risoluzione dei problemi più comuni

SintomoCausa probabileSoluzione
Il gateway risponde sempre 401 anche con token validoAlgoritmo JWT non corrispondente o clock skew tra client e serverVerifica che algorithms in jwt.verify corrisponda a quello usato per firmare, e sincronizza gli orologi dei server con NTP
Errore ECONNREFUSED verso il backendIl microservizio target non è raggiungibile dall’URL configuratoControlla le variabili BACKEND_*_URL e che il servizio sia in ascolto sulla porta attesa
Il rate limit non blocca nullaLo store Redis non è collegato o la chiave generata è sempre diversaVerifica la connessione Redis con redis-cli ping e controlla il valore restituito da keyGenerator
CORS blocca le richieste dal frontend in sviluppoL’origine locale (es. localhost:5173) non è nella lista consentitaAggiungi l’origine di sviluppo all’array origin, rimuovendola prima del deploy in produzione
I log non contengono l’utente della richiestaIl middleware di logging è stato piazzato dopo quello di autenticazioneSposta auditLog prima di verificaToken, oppure leggi req.user nell’evento finish come nell’esempio
Le richieste PUT/DELETE falliscono ma le GET funzionanoCORS non consente il metodo, oppure manca la gestione della richiesta preflight OPTIONSAggiungi i metodi mancanti all’array methods del middleware cors
Il container Docker si riavvia in loopRedis non è ancora pronto quando il gateway prova a connettersiAggiungi un healthcheck e una politica di retry alla connessione ioredis, o usa depends_on con condition: service_healthy in Compose
Il payload JSON di grandi dimensioni viene rifiutatoIl limite impostato in express.json({ limit }) è troppo basso per il caso d’usoAumenta il limite solo sulle rotte che ne hanno davvero bisogno, non globalmente

Consigli avanzati per scalare in produzione

Quando il traffico cresce, alcune scelte fatte in questa guida per semplicità vanno riviste. Il rate limiting basato su Redis regge bene fino a poche migliaia di richieste al secondo, oltre quella soglia conviene valutare un algoritmo a token bucket implementato direttamente nel livello di rete (per esempio con NGINX o Envoy davanti al gateway Node.js). Per l’autenticazione, valuta il passaggio da JWT autofirmati a un provider di identità esterno basato su OpenID Connect, così da centralizzare anche la gestione degli utenti e il logout globale, un limite intrinseco dei JWT stateless. Il protocollo di riferimento resta OAuth 2.0 (RFC 6749), su cui si basano sia OpenID Connect sia la maggior parte dei provider di identità commerciali. La guida su OAuth 2.0 e OpenID Connect in Node.js mostra come integrare un provider esterno mantenendo la stessa struttura di middleware vista qui.

Se il tuo gateway serve più team o più prodotti, separa le policy per namespace: ogni gruppo di rotte (/api/users, /api/orders, ecc.) può avere limiti di frequenza e regole di autorizzazione diverse, invece di applicare una configurazione unica a tutto il traffico. Infine, testa periodicamente il gateway con strumenti di scansione dedicati alle API: un tool come Burp Suite permette di simulare tentativi di bypass dell’autorizzazione prima che lo faccia un attaccante reale.

Per chi preferisce non scrivere un gateway da zero, i principi visti in questa guida, autenticazione centralizzata, rate limiting per identità, validazione dell’input e audit log, si applicano identici a gateway già pronti come Kong, Express Gateway o i servizi gestiti di AWS, Azure e Google Cloud: cambia la sintassi di configurazione, non la logica di difesa. Il Secure API Gateway Blueprint di OWASP è un buon punto di riferimento indipendente dal fornitore per verificare di non aver dimenticato nessun controllo.

Il progetto completo: come è organizzato

A questo punto il progetto ha una struttura completa e ogni file ha una responsabilità precisa: server.js collega i middleware nell’ordine corretto (logging, header di sicurezza, autenticazione, autorizzazione, rate limit, proxy), mentre le cartelle middleware e routes contengono la logica isolata e testabile singolarmente. Questo ordine non è arbitrario: un controllo di autorizzazione eseguito prima di quello di autenticazione, per esempio, non avrebbe senso perché non esisterebbe ancora un req.user da verificare. La sequenza corretta, dalla richiesta in ingresso al proxy verso il backend, è: audit log, header di sicurezza (Helmet e CORS), autenticazione JWT, autorizzazione per ruolo, rate limiting, validazione dell’input, proxy verso il microservizio.

Con questa base puoi aggiungere nuovi microservizi semplicemente registrando un nuovo blocco createProxyMiddleware, ereditando automaticamente tutti i controlli di sicurezza già configurati a monte, senza doverli riscrivere in ogni singolo servizio.

Come i controlli costruiti in questa guida coprono la OWASP API Security Top 10

Vale la pena rileggere il progetto con in mano la classifica OWASP API Security Top 10, il riferimento più usato dai team di sicurezza per valutare un’architettura di API. La voce in cima alla lista, Broken Object Level Authorization, è quella che abbiamo affrontato al passo 4 con il middleware proprietaRisorsa: senza quel controllo, qualsiasi utente autenticato potrebbe leggere le risorse di un altro semplicemente cambiando un identificativo nell’URL. La seconda voce, Broken Authentication, è coperta dal middleware JWT del passo 3, con l’accortezza di fissare esplicitamente l’algoritmo di verifica.

Il quarto punto della classifica, Unrestricted Resource Consumption, è esattamente il problema che il rate limiting del passo 5 e la validazione della dimensione del payload (express.json({ limit: '100kb' })) sono pensati per contenere: senza questi limiti, un singolo client potrebbe esaurire CPU, memoria o connessioni al database del backend con richieste ripetute o payload enormi. La voce sulla configurazione errata della sicurezza (Security Misconfiguration) è indirizzata dagli header impostati con Helmet al passo 7 e dalla gestione rigorosa di CORS: la documentazione ufficiale di Express sulle best practice di sicurezza elenca ulteriori accorgimenti applicabili allo stesso livello.

Infine, la voce dedicata all’inventario e alla gestione del ciclo di vita delle API (Improper Inventory Management) è quella che il passo 9 affronta con il tracciamento degli endpoint: senza un inventario aggiornato, è impossibile sapere quali API sono davvero esposte e quali andrebbero dismesse. Chi gestisce un gateway con volumi di traffico elevati troverà utile anche la lettura del materiale di riferimento NGINX sulle architetture a microservizi, che descrive pattern di instradamento complementari a quelli visti qui.

Domande frequenti

Qual è la differenza tra un API gateway e un semplice reverse proxy?

Un reverse proxy come NGINX in configurazione base instrada le richieste verso i server a valle senza applicare logica applicativa. Un API gateway aggiunge un livello di intelligenza sopra il proxy: autenticazione, autorizzazione, rate limiting per utente, trasformazione delle richieste e aggregazione di più servizi in un’unica risposta. Il progetto di questa guida è un gateway proprio perché combina il proxy con questi controlli applicativi.

Conviene scrivere un API gateway da zero o usarne uno già pronto?

Dipende dalla scala. Per un numero limitato di microservizi, un gateway Node.js come quello descritto qui è facile da mantenere e da adattare a esigenze specifiche: conosci ogni riga di codice e puoi modificare il comportamento in pochi minuti. Con decine di servizi e team diversi, un gateway gestito o open source come Kong, Tyk o i servizi cloud nativi offre funzionalità già pronte (dashboard, plugin, alta disponibilità, gestione multi-tenant) che richiederebbero molto tempo da sviluppare e mantenere internamente. Molti team iniziano con una soluzione simile a quella di questa guida e migrano verso un prodotto dedicato quando il numero di microservizi supera la decina.

I JWT vanno salvati nel localStorage del browser?

È sconsigliato: il localStorage è accessibile da qualsiasi script eseguito nella pagina, quindi vulnerabile a XSS. La pratica più sicura è salvare il token in un cookie con attributi HttpOnly, Secure e SameSite=Strict, così che JavaScript lato client non possa leggerlo direttamente.

Come si gestisce la scadenza e il rinnovo dei token senza far rifare il login all’utente?

Con una coppia di token: un access token di breve durata (15-60 minuti) usato per le chiamate API, e un refresh token di durata più lunga, salvato in modo sicuro, usato solo per ottenere un nuovo access token quando il precedente scade. Il gateway deve esporre un endpoint dedicato al rinnovo, separato dalle rotte proxy verso i microservizi.

Il rate limiting basta da solo a fermare un attacco DDoS?

No. Il rate limiting applicativo, come quello costruito in questa guida, protegge dagli abusi di singoli utenti o account compromessi, ma un attacco DDoS volumetrico, quello che secondo Akamai ha raggiunto i 188 miliardi di eventi Layer 7 in EMEA a novembre 2025, va fermato a monte, a livello di rete o con un servizio specializzato (CDN con protezione anti-DDoS, WAF cloud). I due livelli di difesa sono complementari, non alternativi: il primo assorbe il volume, il secondo governa il comportamento di chi resta.

Quanto spesso va ruotato il segreto usato per firmare i JWT?

Non esiste una regola fissa valida per ogni contesto, ma una buona pratica è ruotarlo periodicamente (per esempio ogni 90 giorni) e immediatamente in caso di sospetta compromissione. Per evitare di invalidare tutte le sessioni attive durante la rotazione, molte implementazioni supportano più chiavi valide in parallelo per un periodo di transizione, identificate da un campo kid nell’header del token.

Serve un WAF davanti a un API gateway già protetto da questi controlli?

Sì, i due strumenti coprono livelli diversi. Un WAF filtra pattern noti di attacco (SQL injection, path traversal) a livello di rete prima ancora che la richiesta raggiunga l’applicazione, mentre i controlli visti in questa guida gestiscono identità, autorizzazione e logica di business specifica dell’API. Usarli insieme riduce il rischio complessivo più di quanto farebbe uno solo dei due.