Due CVE pubblicate nel 2026 raccontano la stessa storia da due angolazioni diverse. Ad aprile, CVE-2026-34500 (CVSS 6,5) ha mostrato che Apache Tomcat poteva accettare un certificato client anche quando il controllo OCSP falliva “in silenzio”, nonostante il soft-fail fosse disattivato. A giugno, CVE-2026-45389 (CVSS 7,4) ha rivelato che la libreria OCaml-TLS non verificava correttamente gli attributi Key Usage ed Extended Key Usage dei certificati client, permettendo di autenticarsi con certificati non pensati per quello scopo. In entrambi i casi il mutual TLS (mTLS) era presente, configurato, “attivo”. E in entrambi i casi bastava un dettaglio di validazione trascurato per svuotarlo di significato.

Questo articolo è una guida pratica per implementare mTLS in Node.js senza cadere negli stessi errori: dalla creazione di una Certificate Authority interna fino al deployment dietro un reverse proxy Nginx, passando per i controlli di validazione che i due CVE del 2026 hanno dimostrato essere tutt’altro che scontati. Il progetto finale gira su Node.js 24.21.0 LTS “Krypton”, rilasciato l’8 settembre 2026 con OpenSSL 3.5.8 integrato, ed è pensato per team che devono proteggere API interne o comunicazioni tra microservizi secondo un modello zero trust.

Perché il mutual TLS è la difesa che molte API europee ignorano

La maggior parte delle API aziendali si affida a un solo livello di autenticazione: una chiave API, un token bearer, un JWT firmato. Il server dimostra la propria identità con un certificato TLS, ma il client si limita a presentare una stringa segreta che chiunque intercetti può riutilizzare. Il mutual TLS ribalta lo schema: anche il client deve dimostrare la propria identità con un certificato X.509, verificato da entrambe le parti prima che un solo byte di dati applicativi venga scambiato.

Nei setup a service mesh (Istio, Linkerd) l’mTLS tra sidecar è quasi sempre automatico. Il problema, secondo diverse analisi di incident response del 2025-2026, riguarda i bordi esterni: le API esposte a partner o integratori terzi continuano spesso a fidarsi di sole chiavi API o bearer token, anche quando il traffico interno è già protetto da certificati. È esattamente il punto debole che un attaccante cerca: un token rubato da un log, un secret finito per errore in un repository pubblico, un client compromesso che continua a passare come legittimo perché nessuno gli chiede “chi sei, dimostralo con un certificato”.

Cloudflare descrive l’mTLS come “a common security practice that uses client certificates to ensure traffic between client and server is bidirectionally secure and trusted” (Cloudflare API Shield). Google Cloud lo definisce “an industry standard protocol for mutual authentication between a client and a server” (Google Cloud Load Balancing docs). Sono definizioni quasi identiche perché il principio è semplice: due parti, due certificati, due verifiche incrociate prima di fidarsi.

In Europa la spinta arriva anche dalla regolamentazione. Le linee guida ENISA sul rischio, unite ai requisiti NIS2 per i settori critici, spingono le organizzazioni verso controlli di accesso basati su identità crittografiche piuttosto che su password o token statici, soprattutto per le comunicazioni macchina-a-macchina tra sistemi bancari, sanitari o infrastrutturali. Chi lavora su API B2B in Italia lo vede già nei capitolati: “autenticazione a certificato” è una voce che compare sempre più spesso nei requisiti di sicurezza dei contratti IT.

Vale la pena chiarire subito una distinzione che genera confusione: mTLS non è un sostituto della crittografia end-to-end né della gestione dei permessi applicativi. È un controllo di accesso al livello di trasporto, pensato per rispondere a una domanda precisa: questa connessione proviene davvero dal sistema che dichiara di essere, con una chiave privata che nessun altro possiede? Tutto il resto, dai permessi granulari alla logica di business, resta compito dei livelli superiori dello stack applicativo.

Cos’è il mutual TLS e in cosa differisce dall’HTTPS classico

In una connessione HTTPS standard, l’handshake TLS autentica solo il server: il browser (o il client HTTP) riceve il certificato del server, lo verifica contro una Certificate Authority fidata e avvia una sessione cifrata. Il client resta anonimo dal punto di vista crittografico, anche se poi si autentica a livello applicativo con login, cookie o token.

Con mTLS l’handshake aggiunge un passaggio: il server richiede esplicitamente un certificato al client (opzione requestCert in Node.js) e lo valida prima di completare la connessione. AWS lo riassume così: “Mutual TLS authentication requires two-way authentication between the client and the server” e “clients must present X.509 certificates to verify their identity to access your API” (AWS API Gateway docs). La differenza pratica è che l’autenticazione avviene al livello di trasporto, prima che qualsiasi logica applicativa entri in gioco: un client senza certificato valido non arriva nemmeno a inviare una richiesta HTTP, la connessione viene rifiutata durante l’handshake.

Questo comporta due vantaggi concreti rispetto a token e chiavi API. Primo, un certificato non può essere “rubato” da un semplice header HTTP loggato per errore: la chiave privata resta sul client e non transita mai sulla rete. Secondo, la revoca è centralizzata: basta aggiungere il certificato compromesso a una Certificate Revocation List (CRL) o interrogare un responder OCSP, senza dover ruotare segreti su decine di sistemi. Se non hai ancora familiarità con la struttura di base di un certificato X.509, la nostra guida a OpenSSL 3.5 LTS: chiavi e certificati copre i concetti propedeutici a questo tutorial.

Prerequisiti: versioni e strumenti necessari

Prima di iniziare, verifica di avere questi strumenti installati con le versioni indicate. Il progetto è stato testato con la linea LTS corrente di Node.js, quella raccomandata per ambienti di produzione a settembre 2026.

StrumentoVersione minima consigliataNote
Node.js24.21.0 LTS “Krypton”Include OpenSSL 3.5.8, supporto fino ad aprile 2028
npm10.x (incluso in Node 24)Usato per Express e dipendenze del progetto
OpenSSL CLI3.xPer generare CA, certificati server e client
Express4.x o 5.xFramework HTTP usato negli esempi
Nginx1.25+Solo per il passo opzionale del reverse proxy

Serve anche una cartella di lavoro vuota, accesso al terminale con permessi per generare chiavi private (non serve root) e circa 45-60 minuti di tempo per completare tutti i passi, incluso il testing finale. Non è richiesta esperienza pregressa con OpenSSL, ma aiuta sapere cos’è un certificato X.509 e come funziona una coppia di chiavi pubblica/privata.

Passo 1 e 2: progetta la gerarchia dei certificati e crea la CA interna

Il primo errore che si commette con mTLS è saltare la progettazione e generare certificati “a mano” senza una gerarchia chiara. Per un ambiente di produzione servono almeno tre livelli: una Certificate Authority interna (root CA), che firma il certificato del server e i certificati dei client. Non usare mai la stessa CA pubblica che gestisce i certificati del tuo sito web (per quello resta valido il flusso descritto nella nostra guida a Let’s Encrypt e Certbot) per firmare i certificati client: la CA interna deve restare separata e la sua chiave privata va custodita offline o in un vault dedicato (HashiCorp Vault, AWS KMS, o anche un semplice HSM se il budget lo consente).

Crea una cartella ca/ e genera la chiave privata e il certificato autofirmato della root CA:

mkdir -p mtls-project/ca mtls-project/server mtls-project/client
cd mtls-project

# Chiave privata della CA (4096 bit, protetta da passphrase)
openssl genrsa -aes256 -out ca/ca-key.pem 4096

# Certificato autofirmato della CA, valido 10 anni
openssl req -x509 -new -nodes -key ca/ca-key.pem -sha256 -days 3650 \
  -out ca/ca-cert.pem \
  -subj "/C=IT/O=Shattered Labs/OU=Security/CN=Shattered Internal CA"

Il flag -aes256 cifra la chiave della CA con una passphrase: senza questa protezione, chiunque ottenga accesso al filesystem del server dove risiede la chiave può firmare certificati client illimitati e impersonare qualsiasi identità. Annota la passphrase in un password manager aziendale, non in un file di testo nella stessa cartella.

Passo 3 e 4: genera i certificati di server e client

Il certificato del server segue lo schema TLS classico, con l’aggiunta di un Subject Alternative Name (SAN) corretto: senza SAN, Node.js e i browser moderni rifiutano la connessione anche se il Common Name è giusto.

# Chiave e CSR del server
openssl genrsa -out server/server-key.pem 2048
openssl req -new -key server/server-key.pem -out server/server.csr \
  -subj "/C=IT/O=Shattered Labs/CN=api.esempio.local"

# File di estensioni con SAN e keyUsage corretti
cat > server/server-ext.cnf <

Per i certificati client, l'attributo chiave è extendedKeyUsage=clientAuth. È esattamente il campo che CVE-2026-45389 ha dimostrato non essere validato correttamente in OCaml-TLS: un certificato pensato per firmare email o per l'autenticazione server, privo dell'attributo clientAuth, non dovrebbe mai essere accettato per l'autenticazione client. Genera un certificato per client, ripetendo il processo con un CN univoco per ogni identità:

openssl genrsa -out client/client-key.pem 2048
openssl req -new -key client/client-key.pem -out client/client.csr \
  -subj "/C=IT/O=Shattered Labs/OU=Servizio Fatturazione/CN=client-fatturazione-01"

cat > client/client-ext.cnf <

Nota la durata più breve per il certificato client (365 giorni contro 825 per il server): più un'identità è granulare e distribuita, più conviene tenerla a scadenza breve, per limitare i danni in caso di chiave compromessa. Ripeti questo passo per ogni client che deve poter parlare con la tua API, usando un CN diverso per ciascuno: questo ti permette in seguito di distinguere e revocare le identità singolarmente.

Passo 5: configura il server HTTPS in Node.js con mTLS

Con i certificati pronti, il server Node.js richiede tre opzioni fondamentali sull'oggetto passato a https.createServer: cert e key per l'identità del server, ca per la lista di CA fidate contro cui validare i certificati client, requestCert: true per chiedere effettivamente un certificato, e rejectUnauthorized: true per rifiutare chi non lo presenta o lo presenta non valido.

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

const app = express();

app.get('/api/dati', (req, res) => {
  const cert = req.socket.getPeerCertificate();
  res.json({
    messaggio: 'Accesso autorizzato via mTLS',
    clientCN: cert.subject && cert.subject.CN
  });
});

const options = {
  key: fs.readFileSync('server/server-key.pem'),
  cert: fs.readFileSync('server/server-cert.pem'),
  ca: [fs.readFileSync('ca/ca-cert.pem')],
  requestCert: true,
  rejectUnauthorized: true
};

https.createServer(options, app).listen(8443, () => {
  console.log('Server mTLS in ascolto su https://localhost:8443');
});

Con rejectUnauthorized: true, Node.js chiude la connessione a livello TLS prima ancora che Express veda la richiesta se il certificato client non è firmato da una delle CA elencate in ca. È il comportamento corretto per la maggior parte dei casi, ma non basta da solo: verifica sempre anche i campi del certificato a livello applicativo, come mostrato nel passo successivo.

Passo 6: valida Key Usage ed Extended Key Usage (la lezione di CVE-2026-45389)

Il caso OCaml-TLS ha dimostrato un punto sottile: un certificato firmato da una CA fidata, con catena di trust valida, può comunque essere il certificato "sbagliato" se non ha l'Extended Key Usage clientAuth. Molte implementazioni si fermano al controllo "la CA lo ha firmato, quindi va bene", senza controllare per cosa quel certificato era stato emesso. Node.js non fa questa verifica in automatico: va aggiunta esplicitamente.

const crypto = require('crypto');

function verificaClientAuth(pemCert) {
  const x509 = new crypto.X509Certificate(pemCert);
  const eku = x509.extKeyUsage || [];
  // OID 1.3.6.1.5.5.7.3.2 corrisponde a clientAuth
  if (!eku.includes('1.3.6.1.5.5.7.3.2')) {
    throw new Error('Certificato privo di Extended Key Usage clientAuth');
  }
  return true;
}

app.use((req, res, next) => {
  const cert = req.socket.getPeerCertificate();
  if (!cert || !cert.raw) {
    return res.status(401).json({ errore: 'Certificato client mancante' });
  }
  try {
    const pem = '-----BEGIN CERTIFICATE-----\n' +
      cert.raw.toString('base64').match(/.{1,64}/g).join('\n') +
      '\n-----END CERTIFICATE-----';
    verificaClientAuth(pem);
    next();
  } catch (err) {
    res.status(403).json({ errore: err.message });
  }
});

Questo middleware usa la classe crypto.X509Certificate, disponibile in Node.js dalla versione 15.6 e stabile nelle release LTS più recenti, per leggere l'attributo extKeyUsage direttamente dal certificato presentato in connessione. Se manca l'OID di clientAuth, la richiesta viene rifiutata con 403 prima di raggiungere qualsiasi route applicativa. È lo stesso controllo che, se presente in OCaml-TLS prima della versione 2.1.0, avrebbe impedito lo scenario descritto in CVE-2026-45389.

Passo 7: scrivi il client Node.js che presenta il certificato

Sul lato client, la libreria nativa https accetta le stesse tre credenziali (chiave privata, certificato, CA) passate come opzioni della richiesta. Se il certificato del server non è firmato da una CA pubblica riconosciuta (come nel caso di un ambiente interno con CA privata), va indicata esplicitamente anche la CA con cui validare il server.

// client.js
const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'localhost',
  port: 8443,
  path: '/api/dati',
  method: 'GET',
  key: fs.readFileSync('client/client-key.pem'),
  cert: fs.readFileSync('client/client-cert.pem'),
  ca: [fs.readFileSync('ca/ca-cert.pem')]
};

const req = https.request(options, (res) => {
  let body = '';
  res.on('data', (chunk) => { body += chunk; });
  res.on('end', () => console.log('Risposta:', body));
});

req.on('error', (err) => console.error('Errore connessione:', err.message));
req.end();

Eseguendo node server.js in un terminale e node client.js in un altro, l'output atteso è:

Risposta: {"messaggio":"Accesso autorizzato via mTLS","clientCN":"client-fatturazione-01"}

Se rimuovi temporaneamente cert e key dalle opzioni del client e riesegui la richiesta, la connessione fallisce già durante l'handshake TLS con un errore del tipo ECONNRESET lato client e UNABLE_TO_VERIFY_LEAF_SIGNATURE o simile lato server: è la prova che l'autenticazione avviene prima che Express veda anche solo un header della richiesta.

Passo 8: gestisci la revoca con CRL e OCSP (la lezione di CVE-2026-34500)

Emettere certificati è la parte facile. Revocarli quando un dipendente lascia l'azienda, un servizio viene dismesso o una chiave privata finisce in un repository pubblico è dove molte implementazioni mTLS falliscono in silenzio, come ha mostrato CVE-2026-34500 in Tomcat: il controllo OCSP poteva fallire "in soft" (accettando comunque il certificato) anche quando l'amministratore aveva esplicitamente disattivato quel comportamento.

Il meccanismo più semplice da implementare in proprio è una Certificate Revocation List: un elenco di numeri seriali di certificati non più validi, che il server consulta a ogni handshake.

# Genera la CRL iniziale (vuota)
openssl ca -config ca/ca.cnf -gencrl -out ca/ca-crl.pem

# Revoca un certificato compromesso
openssl ca -config ca/ca.cnf -revoke client/client-cert-compromesso.pem

# Rigenera la CRL dopo ogni revoca
openssl ca -config ca/ca.cnf -gencrl -out ca/ca-crl.pem

In Node.js la CRL va caricata insieme alla CA, passandola come opzione crl di https.createServer: il motore TLS nativo la consulta automaticamente e rifiuta i certificati il cui numero seriale compare nella lista. Il punto critico, esattamente quello aggirato in Tomcat, è decidere cosa succede quando la CRL non è raggiungibile o non aggiornata: la scelta corretta per ambienti sensibili è il fail-closed, cioè rifiutare la connessione se la CRL non può essere verificata, invece di accettarla "per non bloccare il servizio". Rigenera e ridistribuisci la CRL su un ciclo automatico (ogni ora o ogni giorno, a seconda della sensibilità del sistema) invece di farlo manualmente dopo un incidente.

Passo 9: registra e monitora le connessioni mTLS

Un'identità basata su certificato è più utile di un token anche a scopo di audit: ogni richiesta può essere associata a un CN specifico, non solo a un indirizzo IP o a una chiave API condivisa da più client. Aggiungi un logging strutturato che registri l'identità del chiamante a ogni richiesta.

app.use((req, res, next) => {
  const cert = req.socket.getPeerCertificate();
  const logEntry = {
    timestamp: new Date().toISOString(),
    clientCN: cert.subject ? cert.subject.CN : 'sconosciuto',
    serial: cert.serialNumber,
    validoFino: cert.valid_to,
    metodo: req.method,
    percorso: req.path,
    ip: req.socket.remoteAddress
  };
  console.log(JSON.stringify(logEntry));
  next();
});

In produzione questi log vanno inviati a un sistema centralizzato (Wazuh, un SIEM, o anche solo un aggregatore come Loki) per poter correlare tentativi di accesso con certificati scaduti, revocati o con CN inattesi. Un picco improvviso di connessioni rifiutate per certificato scaduto, ad esempio, è spesso il primo segnale che un job di rotazione automatica si è fermato senza che nessuno se ne accorgesse.

Passo 10: automatizza la rotazione dei certificati

Certificati client con validità di un anno significano che, prima o poi, qualcuno dimenticherà di rinnovarli, e un servizio smetterà di funzionare nel weekend sbagliato. Lo script seguente calcola i giorni residui prima della scadenza e genera un avviso quando si scende sotto una soglia, da integrare in un cron job o in una pipeline CI/CD.

#!/usr/bin/env node
const { execSync } = require('child_process');

const SOGLIA_GIORNI = 30;
const certificati = ['server/server-cert.pem', 'client/client-cert.pem'];

certificati.forEach((percorso) => {
  const output = execSync(`openssl x509 -enddate -noout -in ${percorso}`).toString();
  const scadenza = new Date(output.replace('notAfter=', '').trim());
  const giorniResidui = Math.floor((scadenza - new Date()) / (1000 * 60 * 60 * 24));

  if (giorniResidui <= SOGLIA_GIORNI) {
    console.warn(`ATTENZIONE: ${percorso} scade tra ${giorniResidui} giorni`);
    process.exitCode = 1;
  } else {
    console.log(`${percorso} valido ancora ${giorniResidui} giorni`);
  }
});

Pianifica questo controllo con una frequenza giornaliera e collegalo a un canale di notifica (email, Slack, o il tuo sistema di ticketing). Per ambienti più maturi, vale la pena valutare uno strumento dedicato alla gestione del ciclo di vita dei certificati, come step-ca di Smallstep, che automatizza sia l'emissione sia il rinnovo senza intervento manuale.

Passo 11: metti mTLS dietro un reverse proxy Nginx

In molte architetture, la terminazione TLS non avviene direttamente nel processo Node.js ma su un reverse proxy che poi inoltra il traffico all'applicazione. Nginx supporta l'mTLS in modo nativo con poche direttive, e può anche passare l'identità del client all'applicazione tramite header, se la connessione interna resta su rete fidata.

server {
    listen 443 ssl;
    server_name api.esempio.local;

    ssl_certificate     /etc/nginx/certs/server-cert.pem;
    ssl_certificate_key /etc/nginx/certs/server-key.pem;

    ssl_client_certificate /etc/nginx/certs/ca-cert.pem;
    ssl_verify_client on;
    ssl_crl /etc/nginx/certs/ca-crl.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header X-Client-CN $ssl_client_s_dn_cn;
        proxy_set_header X-Client-Verify $ssl_client_verify;
    }
}

Con questa configurazione, Nginx fa da guardiano: rifiuta le connessioni senza certificato valido prima ancora che raggiungano Node.js, e passa l'identità verificata come header X-Client-CN. L'applicazione Node.js a quel punto può fidarsi dell'header solo se il canale tra Nginx e Node.js è protetto (localhost, rete privata, o mTLS interno): esporre quella porta interna direttamente su internet vanificherebbe tutta la protezione a monte.

Passo 12: testa tutto end-to-end, il progetto completo

A questo punto la struttura del progetto dovrebbe assomigliare a questa, con tutti i file generati nei passi precedenti:

mtls-project/
├── ca/
│   ├── ca-key.pem
│   ├── ca-cert.pem
│   └── ca-crl.pem
├── server/
│   ├── server-key.pem
│   ├── server-cert.pem
│   └── server-ext.cnf
├── client/
│   ├── client-key.pem
│   ├── client-cert.pem
│   └── client-ext.cnf
├── server.js
├── client.js
├── check-scadenze.js
└── package.json

Esegui la sequenza di test completa: avvia il server con node server.js, verifica con un client legittimo (node client.js, output atteso: risposta 200 con il CN corretto), poi ripeti il test con un certificato scaduto, uno emesso da una CA diversa e uno privo di clientAuth nell'Extended Key Usage. In tutti e tre i casi la connessione deve essere rifiutata, con messaggi di errore diversi a seconda del punto in cui fallisce la verifica (handshake TLS per CA sbagliata o certificato scaduto, middleware applicativo per EKU mancante). Solo quando tutti e quattro gli scenari si comportano come previsto il sistema è pronto per un ambiente di staging.

Verifica la configurazione con curl e openssl s_client prima di scrivere codice client

Prima ancora di scrivere un client Node.js, conviene verificare che server e certificati funzionino con strumenti da riga di comando: se qualcosa non torna a questo livello, il problema è nei certificati, non nel codice applicativo. curl supporta nativamente mTLS con tre flag.

# Richiesta autenticata con certificato client
curl --cert client/client-cert.pem --key client/client-key.pem \
  --cacert ca/ca-cert.pem https://localhost:8443/api/dati

# Stesso test, ma senza certificato client: deve fallire
curl --cacert ca/ca-cert.pem https://localhost:8443/api/dati

La prima richiesta deve restituire il JSON con il CN del client, la seconda deve fallire con un errore del tipo curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL oppure un rifiuto esplicito della connessione, a seconda della versione di OpenSSL installata sul sistema. Per ispezionare l'intero handshake, incluso il certificato presentato dal server e la catena di validazione, openssl s_client resta lo strumento più dettagliato:

openssl s_client -connect localhost:8443 \
  -cert client/client-cert.pem -key client/client-key.pem \
  -CAfile ca/ca-cert.pem -state -brief

Nell'output cerca la riga Verification: OK: se compare invece Verify return code con un numero diverso da zero, la causa è quasi sempre una CA sbagliata, un certificato scaduto o una catena di firma incompleta. Questo passaggio di verifica manuale, spesso saltato per fretta, avrebbe permesso di individuare in fase di test comportamenti come quello di CVE-2026-34500: bastava osservare che la verifica riportava un esito positivo anche con un certificato che avrebbe dovuto essere rifiutato per revoca.

mTLS vs API Key vs OAuth Bearer vs JWT: quale scegliere

Nessuno di questi meccanismi è universalmente "migliore": la scelta dipende da chi deve autenticarsi (utenti umani o macchine) e da quanto è sensibile il canale.

MeccanismoAdatto aRischio principaleComplessità operativa
mTLSComunicazioni machine-to-machine, microservizi, B2BGestione del ciclo di vita dei certificatiAlta (CA, rotazione, revoca)
API KeyIntegrazioni semplici, prototipazioneFurto o leak della chiave, nessuna scadenza naturaleBassa
OAuth 2.0 Bearer TokenApplicazioni utente, deleghe di accessoToken replay se intercettato senza TLSMedia
JWT firmatoAutenticazione stateless tra serviziRevoca difficile prima della scadenza naturaleMedia

Nella pratica, le architetture più mature combinano più livelli: mTLS per proteggere il canale di trasporto tra servizi interni, e un JWT o un token OAuth applicativo sopra quel canale per portare informazioni su permessi e scope. L'OWASP API Security Project colloca l'autenticazione debole tra le categorie di rischio più ricorrenti per le API (OWASP API Security Top 10), e l'mTLS affronta esattamente quella categoria alla radice, spostando la verifica di identità dal livello applicativo al livello di trasporto. Se stai già proteggendo un API gateway con altre tecniche, la nostra guida a API Gateway sicuro in Node.js e l'analisi delle vulnerabilità OWASP Top 10 in Node.js sono un buon complemento a questo tutorial.

Errori comuni da evitare

Questi sono gli errori più frequenti osservati in implementazioni mTLS reali, spesso responsabili di falle silenti che passano inosservate per mesi.

ErroreConseguenzaCome evitarlo
Usare rejectUnauthorized: false "temporaneamente" per debugmTLS diventa decorativo, qualsiasi client si connetteDebug con certificati di test corretti, mai disattivando la verifica
Non verificare Key Usage / Extended Key UsageCertificati emessi per altri scopi vengono accettati come client (vedi CVE-2026-45389)Aggiungere il controllo esplicito con crypto.X509Certificate
CRL o OCSP che falliscono "soft" per defaultCertificati revocati continuano a essere accettati (vedi CVE-2026-34500)Configurare fail-closed sui controlli di revoca
Riutilizzare la stessa coppia di chiavi per più identità clientImpossibile distinguere o revocare singolarmente i client compromessiUn certificato univoco per ogni servizio o identità
Custodire la chiave privata della CA sullo stesso server applicativoUn solo compromesso permette di firmare certificati illimitatiChiave della CA offline o in un vault dedicato
Certificati con validità pluriennale per ridurre il lavoro di rotazioneFinestra di esposizione enorme in caso di chiave rubataValidità breve (giorni o pochi mesi) più automazione del rinnovo

Risoluzione dei problemi più comuni

Ecco i problemi che si incontrano più spesso durante l'implementazione, con la causa più probabile e la soluzione. Se il tuo errore non compare in questa lista, il primo passo di diagnosi resta sempre lo stesso: isola il problema con openssl s_client prima di sospettare del codice Node.js, così sai se il difetto è nella catena di certificati o nella logica applicativa.

  • Errore "UNABLE_TO_VERIFY_LEAF_SIGNATURE": il client non ha ricevuto o non sta usando il certificato della CA corretta nell'opzione ca. Verifica che il percorso del file sia giusto e che il certificato caricato corrisponda a quello usato per firmare il certificato del server.
  • Connessione chiusa con "ECONNRESET" appena avviata: il server ha requestCert: true ma il client non presenta alcun certificato. Controlla che cert e key siano effettivamente impostati nelle opzioni della richiesta client.
  • "ERR_OSSL_X509_KEY_VALUES_MISMATCH": la chiave privata caricata non corrisponde al certificato. Succede spesso dopo aver rigenerato un certificato senza rigenerare anche la chiave, o mischiando file di ambienti diversi.
  • Il server accetta certificati che dovrebbero essere scaduti: l'orologio di sistema del server è desincronizzato. Verifica NTP: OpenSSL e Node.js si fidano dell'orario locale della macchina per valutare la validità temporale del certificato.
  • Il campo cert.subject.CN risulta undefined nel middleware: probabile che rejectUnauthorized sia impostato su false, nel qual caso Node.js consente comunque la connessione ma popola in modo incompleto l'oggetto certificato.
  • Nginx risponde 400 "No required SSL certificate was sent": la direttiva ssl_verify_client è impostata su on ma il client non ha certificato. Se alcuni endpoint devono restare pubblici, usa ssl_verify_client optional e verifica $ssl_client_verify nella logica applicativa.
  • La CRL non viene mai consultata dal server Node.js: manca l'opzione crl in https.createServer, oppure il file CRL non è nel formato PEM atteso. Verifica con openssl crl -in ca-crl.pem -text -noout che il file sia leggibile.
  • Le performance calano sensibilmente con molte connessioni simultanee: l'handshake mTLS è più costoso di un handshake TLS standard per via della verifica aggiuntiva del certificato client. Valuta il riuso delle sessioni TLS (session resumption) e, per volumi alti, la terminazione a livello di load balancer con hardware o istanze dedicate.

Consigli avanzati per produzione e conformità in Europa

Una volta superato il prototipo, alcune scelte fanno la differenza tra un mTLS che regge in produzione e uno che diventa un incubo operativo. Primo: automatizza l'intero ciclo di vita dei certificati con un protocollo come ACME (lo stesso di Let's Encrypt, adattabile a CA interne) o uno strumento dedicato, invece di script artigianali eseguiti manualmente. Vale anche la pena combinare mTLS con l'hardening dell'accesso SSH ai server che ospitano la CA: la nostra guida a chiavi SSH Ed25519 e hardening server copre esattamente questo scenario. Secondo: separa sempre la CA "root" da una CA "intermedia" che firma i certificati operativi, così la root può restare offline quasi permanentemente, riducendo drasticamente la superficie di attacco sulla chiave più critica del sistema.

Terzo, per chi opera in settori toccati da NIS2 (energia, sanità, finanza, infrastrutture digitali), documenta la gerarchia dei certificati e le procedure di revoca come parte del piano di risposta agli incidenti: un audit chiederà quasi certamente come viene gestita la compromissione di un'identità macchina-a-macchina, e "revochiamo manualmente quando ce ne accorgiamo" non è una risposta che regge. Quarto, tieni d'occhio la roadmap di Node.js: la versione 26.x diventerà LTS a ottobre 2026, estendendo il supporto TLS fino al 2029, e pianificare la migrazione con un anno di anticipo evita di trovarsi a gestire mTLS su una versione di Node.js fuori supporto proprio mentre esce una nuova falla nello stack TLS.

Infine, se l'infrastruttura cresce verso decine di microservizi, valuta un service mesh (Istio, Linkerd) che gestisce l'mTLS interno automaticamente tra sidecar, riservando l'implementazione manuale descritta in questa guida ai punti di ingresso esposti a client esterni, dove il controllo granulare rimane indispensabile.

Quando mTLS non è la scelta giusta

Non tutte le API hanno bisogno di mTLS, e forzarlo dove non serve aggiunge solo complessità operativa senza un beneficio proporzionato. Per API pubbliche rivolte a sviluppatori esterni con centinaia o migliaia di integrazioni, gestire un certificato per ciascun client diventa rapidamente un collo di bottiglia: emissione, distribuzione sicura della chiave privata e supporto quando qualcosa va storto richiedono un processo che poche organizzazioni hanno la capacità di scalare. In questi casi, OAuth 2.0 con rotazione frequente dei token o chiavi API abbinate a rate limiting aggressivo restano scelte più pratiche.

Lo stesso vale per applicazioni consumer con utenti finali su browser o app mobile: distribuire e proteggere un certificato client su un dispositivo controllato dall'utente, e non dall'organizzazione, introduce problemi di gestione che raramente giustificano il beneficio rispetto a un buon sistema di autenticazione a più fattori. mTLS rende il massimo quando entrambe le parti della connessione sono sistemi che l'organizzazione controlla direttamente: server interni, microservizi, integrazioni B2B con partner con cui esiste già un rapporto contrattuale che permette lo scambio sicuro di certificati.

Domande frequenti

Che differenza c'è tra TLS e mTLS?

Nel TLS classico solo il server dimostra la propria identità con un certificato. Nell'mTLS anche il client deve presentare un certificato X.509, verificato dal server prima di stabilire la connessione: l'autenticazione diventa bidirezionale invece che a senso unico.

mTLS sostituisce OAuth 2.0 o i JWT?

No, li completa. mTLS protegge il canale di trasporto e l'identità della macchina o del servizio che si connette. OAuth e JWT gestiscono permessi, scope e deleghe a livello applicativo. Molte architetture usano entrambi insieme.

Quanto deve durare un certificato client in produzione?

Non esiste un numero universale, ma la tendenza è verso durate sempre più brevi (settimane o pochi mesi) combinate con rinnovo automatico, per limitare la finestra di esposizione in caso di chiave privata compromessa.

Cosa succede se il certificato client scade durante una connessione attiva?

Una connessione TLS già stabilita non viene interrotta automaticamente alla scadenza: la verifica avviene durante l'handshake iniziale. È quindi importante monitorare le scadenze e rinnovare i certificati prima che il client debba stabilire una nuova connessione.

È obbligatorio usare una CA interna, o si può usare una CA pubblica anche per i client?

Per i certificati client, quasi sempre conviene una CA interna: le CA pubbliche sono pensate per identificare server pubblici (domini), non identità interne come "servizio-fatturazione" o "client-partner-01". Una CA interna dà pieno controllo su emissione e revoca.

mTLS funziona con i browser web?

Sì, i browser moderni supportano i certificati client, ma l'esperienza utente (selezione manuale del certificato, gestione dell'importazione) lo rende poco pratico per utenti finali generici. È molto più diffuso per comunicazioni machine-to-machine, API B2B e microservizi, dove il certificato viene distribuito e gestito centralmente dal team IT invece che dall'utente stesso.

Che impatto ha mTLS sulle performance rispetto a HTTPS normale?

L'handshake iniziale è leggermente più costoso per via della verifica aggiuntiva del certificato client, ma con il riuso delle sessioni TLS l'impatto sulle richieste successive è marginale nella maggior parte dei carichi di lavoro reali.