Il 11 maggio 2026 un attaccante ha impiegato sei minuti per pubblicare 84 pacchetti malevoli su 42 librerie della famiglia @tanstack/*, partendo da un’unica pipeline GitHub Actions compromessa. Entro fine giornata il conteggio era salito a 373 versioni infette su 169 pacchetti npm distinti, con contaminazioni parallele su PyPI. Non è un caso isolato: è il sintomo di un problema che ogni team Node.js deve affrontare ora, non “prima o poi”. Questa guida mostra passo per passo come blindare la pubblicazione dei tuoi pacchetti npm con provenance, Sigstore e SLSA, così che chi installa il tuo codice possa verificare da dove arriva davvero.
Perché la supply chain npm è il bersaglio numero uno nel 2026
I numeri del 2025-2026 raccontano una escalation netta. Il report Sonatype sullo stato della supply chain software ha contato oltre 1.233.000 pacchetti open source malevoli in modo cumulativo su npm, PyPI, Maven Central, NuGet e Hugging Face, con 454.600 nuovi casi identificati solo nel 2025: una crescita di circa il 75% su base annua. ReversingLabs, nel suo report 2025-2026, è arrivata a una conclusione ancora più netta: l’attività malevola su npm è più che raddoppiata nel 2025 e il registro rappresenta oggi quasi il 90% di tutto il malware open source rilevato tra i principali ecosistemi.
StepSecurity, che pubblica un censimento annuale degli incidenti supply chain, ha registrato 56 compromissioni distinte tra agosto 2025 e agosto 2026: un attacco ogni tre giorni circa da marzo 2026 in poi, contro una media di uno al mese l’anno precedente. Il picco è arrivato tra aprile e giugno 2026, con 9, 9 e 10 incidenti mensili rispettivamente. Il pattern che emerge da questi casi non è più il typosquatting ingenuo di qualche anno fa, ma la compromissione diretta delle pipeline CI/CD: account maintainer rubati, token OIDC intercettati, workflow GitHub Actions manomessi. È esattamente lo scenario che ha colpito Axios il 31 marzo 2026, quando un account maintainer compromesso ha permesso di iniettare in una libreria con circa 100 milioni di download settimanali un dropper RAT eseguito in fase di postinstall.
La buona notizia è che npm, GitHub e la fondazione Sigstore hanno costruito negli ultimi anni uno strumento concreto per rispondere a questo problema: la provenance. Non elimina il rischio di un account rubato, ma rende visibile e verificabile da chi consuma il pacchetto qual è la pipeline, il commit e il repository da cui è nato. In questa guida costruiamo un progetto Node.js reale e lo pubblichiamo passo per passo con provenance attiva, firma Sigstore e verifica automatica in CI.
Un’analisi Phoenix Security condotta nel 2026 sulle campagne 2025 offre un altro angolo utile: su 14 campagne malevole censite, per un totale di 111 pacchetti indicizzati come dannosi, npm da solo contava 521 voci malevole con un tasso di coinvolgimento del 79,3% rispetto agli altri registri osservati (PyPI e il marketplace di estensioni VS Code compresi). Diversi di questi casi, secondo lo stesso report, sfruttano codice generato con strumenti di intelligenza artificiale per rendere pacchetti e descrizioni più difficili da individuare con i controlli euristici tradizionali, un dettaglio che rende ancora più urgente spostare la difesa dal singolo file sospetto alla verifica dell’intera pipeline che lo ha prodotto.
| Incidente | Data | Impatto | Vettore |
|---|---|---|---|
| TanStack / TeamPCP | 11 maggio 2026 | 373 versioni malevole su 169 pacchetti npm + PyPI | Pipeline GitHub Actions compromessa |
| Axios | 31 marzo 2026 | ~100M download settimanali esposti a un dropper RAT | Account maintainer hijackato |
| GhostActions | 2 settembre 2025 | Workflow GitHub Actions malevoli iniettati a valle | Account maintainer PyPI compromesso |
| Cluster di marzo 2026 | 1-12 marzo 2026 | 5 attacchi distinti in 12 giorni tra npm e altri registri | Account e pipeline multiple |
Provenance, Sigstore e SLSA: i concetti da conoscere prima di iniziare
Prima di toccare la riga di comando conviene chiarire tre termini che si sovrappongono spesso nella documentazione. La provenance è un’attestazione: un documento firmato che descrive da dove arriva un artefatto software, quale commit, quale repository, quale workflow CI lo ha generato. Quando pubblichi con npm publish --provenance, npm non firma il pacchetto con una chiave privata che devi gestire tu. Chiede invece un token OIDC al runner CI, lo scambia tramite la Certificate Authority Fulcio di Sigstore per ottenere un certificato di firma a vita brevissima, firma un’istruzione in formato in-toto in stile SLSA e la registra nel log di trasparenza Rekor.
Sigstore è il progetto open source (sotto l’ombrello Linux Foundation e OpenSSF) che fornisce questa infrastruttura di firma keyless: Fulcio per i certificati temporanei, Rekor come registro pubblico e immutabile delle firme. Puoi consultare l’architettura completa sul sito ufficiale di Sigstore. SLSA (Supply-chain Levels for Software Artifacts, documentato su slsa.dev) è invece il framework che definisce lo standard delle attestazioni: quali metadati deve contenere una dichiarazione di provenienza per essere considerata affidabile. L’attestazione che npm genera segue proprio questo formato, indicando il subject (il digest del tarball pubblicato), il builder (l’URL del workflow che ha eseguito la build) e i materials (commit SHA e URL del repository sorgente).
Nella pratica, il flusso che npm orchestra dietro le quinte segue quattro passaggi. Il runner CI presenta al provider OIDC (GitHub o GitLab) una richiesta di identità legata a quel preciso job. Fulcio verifica il token e rilascia un certificato X.509 valido per pochi minuti, sufficiente a firmare un solo artefatto. npm costruisce la dichiarazione in-toto con i metadati della build e la firma con la chiave temporanea appena ottenuta. Infine la firma, insieme al certificato, viene inviata a Rekor, che la registra in modo permanente e pubblicamente verificabile: chiunque può controllare in un secondo momento che quella firma sia esistita in quel preciso istante, anche se il certificato che l’ha generata è ormai scaduto. È questo il dettaglio che rende il modello keyless più robusto della firma con chiave GPG statica: non c’è nulla di lungo periodo da rubare, solo un’identità momentanea legata a un’esecuzione precisa del workflow. La documentazione ufficiale di npm sull’argomento è disponibile alla pagina Generating provenance statements, mentre GitHub ha raccontato il lancio della funzionalità sul proprio blog di sicurezza in Introducing npm package provenance.
Un punto va chiarito subito, perché è la lezione più importante emersa dal caso TanStack: la provenance dimostra che un pacchetto è uscito da una determinata pipeline, non che quella pipeline stesse eseguendo codice legittimo. Se un attaccante controlla il workflow, la provenance attesterà correttamente una build malevola come se fosse regolare. Per questo la provenance va trattata come un livello di difesa da combinare con l’hardening del workflow stesso, non come una soluzione a sé stante.
Prerequisiti e progetto di esempio
Prerequisiti e versioni
Per seguire la guida ti servono pochi strumenti, ma con versioni precise. Node.js 24.20.0 LTS (nome in codice “Krypton”) è la linea long-term support più recente al momento della scrittura, ma va bene anche Node.js 26.8.1 se preferisci la linea current. Verifica sempre la versione di npm installata: ti serve npm 11.5.1 o superiore se vuoi sfruttare la Trusted Publishing con provenance automatica descritta più avanti, mentre la pubblicazione manuale con --provenance funziona già da npm 9.x. Ti serve inoltre un account npm con autenticazione a due fattori attiva, un repository GitHub pubblico o privato con Actions abilitate, e la CLI jq per ispezionare le risposte JSON.
| Componente | Versione minima | Cosa abilita |
|---|---|---|
| Node.js | 24.20.0 LTS o 26.8.1 current | Runtime aggiornato con npm recente incluso |
| npm CLI | 9.x | Pubblicazione con npm publish --provenance |
| npm CLI | 9.5+ | Verifica attestazioni via CLI e badge sul sito npm |
| npm CLI | 11.5.1+ | Trusted Publishing con provenance automatica opt-out |
| GitHub Actions | N/D | Emissione token OIDC per Fulcio/Sigstore |
Struttura del progetto di esempio
Costruiamo insieme un piccolo pacchetto utility chiamato safe-id-utils, una libreria di funzioni per generare identificatori casuali: abbastanza semplice da pubblicare in pochi minuti, ma realistica quanto basta da avere una pipeline di test, build e publish come un progetto vero. Alla fine della guida avrai un repository completo che puoi clonare e adattare al tuo pacchetto reale.
safe-id-utils/
├── .github/
│ └── workflows/
│ └── publish.yml
├── src/
│ └── index.js
├── test/
│ └── index.test.js
├── scripts/
│ └── verify-provenance.sh
├── package.json
└── README.md
Il contenuto di src/index.js resta volutamente minimale, senza dipendenze esterne, per tenere piccola la superficie di attacco della libreria stessa:
// src/index.js
import { randomBytes } from 'node:crypto';
export function generateSafeId(length = 16) {
if (length < 8) {
throw new RangeError('length deve essere almeno 8 per motivi di sicurezza');
}
return randomBytes(length).toString('hex');
}
export function generatePrefixedId(prefix, length = 16) {
return `${prefix}_${generateSafeId(length)}`;
}
// test/index.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { generateSafeId, generatePrefixedId } from '../src/index.js';
test('genera un id esadecimale della lunghezza attesa', () => {
const id = generateSafeId(16);
assert.match(id, /^[0-9a-f]{32}$/);
});
test('rifiuta lunghezze inferiori a 8', () => {
assert.throws(() => generateSafeId(4), RangeError);
});
test('applica correttamente il prefisso', () => {
const id = generatePrefixedId('user', 8);
assert.ok(id.startsWith('user_'));
});
Step 1-2: Preparare repository e package.json
Step 1. Crea il repository su GitHub e clonalo in locale, poi inizializza il progetto Node.js con npm init -y. Aggiungi un file src/index.js con la logica della libreria e un test minimo in test/index.test.js usando il test runner integrato di Node (node --test), così eviti dipendenze extra nella catena di build. Meno dipendenze di sviluppo hai, meno superficie offri a un eventuale pacchetto compromesso a monte: per un progetto che parla di sicurezza della supply chain, ridurre la propria catena di dipendenze è già una parte della soluzione.
Step 2. Configura package.json con i campi che il registro npm userà per mostrare la provenienza del pacchetto agli utenti: repository, homepage e files devono puntare esattamente al repository che pubblicherai, perché l'attestazione di provenance verrà confrontata con questi metadati. Il campo files, in particolare, merita attenzione: limitarlo esplicitamente alla cartella src evita che finiscano nel tarball pubblicato file di configurazione locale, script di sviluppo o cronologie di test che non servono a chi installa il pacchetto e che aumentano inutilmente la superficie da verificare.
{
"name": "safe-id-utils",
"version": "1.0.0",
"description": "Generatore di ID sicuri con provenance verificabile",
"main": "src/index.js",
"type": "module",
"files": ["src"],
"repository": {
"type": "git",
"url": "git+https://github.com/tuo-utente/safe-id-utils.git"
},
"scripts": {
"test": "node --test test/"
},
"license": "MIT"
}
Step 3-4: Attivare 2FA e token granulari su npm
Step 3. Prima ancora di pensare al workflow CI, blinda l'account npm che userai per pubblicare. Attiva l'autenticazione a due fattori sia per il login sia per le operazioni di pubblicazione dal profilo npm (Account Settings → Two-Factor Authentication → "Authorization and Publishing"). È il controllo che, se fosse stato attivo su tutti i maintainer coinvolti negli incidenti di marzo 2026, avrebbe reso molto più difficile l'hijacking degli account.
Step 4. Evita i classici token NPM_TOKEN a vita lunga salvati come secret GitHub. Se non puoi ancora usare la Trusted Publishing (vedi Step 11), genera almeno un token granulare con scadenza breve e permessi limitati al singolo pacchetto: dalla pagina Access Tokens scegli "Granular Access Token", imposta una scadenza massima di 90 giorni e restringi la pubblicazione al solo pacchetto che stai gestendo, non all'intero account. Segna in calendario la data di scadenza: un token che scade da solo dopo 90 giorni, anche se dimenticato in un secret, smette di essere utile a un eventuale attaccante molto prima di un token senza scadenza dimenticato per anni, come accaduto in diversi degli incidenti censiti da StepSecurity nel 2025-2026.
Step 5-6: Configurare OpenID Connect nel workflow GitHub Actions
Step 5. Crea il file .github/workflows/publish.yml. Il blocco chiave è permissions: id-token: write: è ciò che autorizza il runner a richiedere a GitHub un token OIDC di breve durata, che Sigstore userà per emettere il certificato di firma. Senza questo permesso esplicito, npm publish --provenance fallisce con un errore di autenticazione.
name: Publish package
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
id-token: write # richiesto per il token OIDC di Sigstore
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24.20.0'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm test
Step 6. Aggiungi il trigger corretto. Nell'esempio sopra il workflow parte solo alla pubblicazione di una release su GitHub, non a ogni push: è una scelta deliberata per ridurre la superficie di attacco. Più il trigger è ampio (per esempio su ogni pull request), più cresce il rischio che un contributor esterno riesca a far eseguire codice nel contesto del workflow che pubblica. Abbina questo trigger a una regola di branch protection sul ramo principale che richieda almeno un'approvazione prima del merge: in questo modo, perché una release parta, deve prima passare da una revisione umana sul codice che finirà nel pacchetto pubblicato, non solo dal comando di publish in sé.
Step 7-8: Pubblicare il pacchetto con npm publish --provenance
Step 7. Aggiungi lo step di pubblicazione vero e proprio al workflow, con il flag --provenance esplicito. Anche se sei già su npm 11.5.1+ con Trusted Publishing, vale la pena lasciarlo per chiarezza: se in futuro la pubblicazione avviene da un contesto diverso, il flag fa fallire subito il comando invece di pubblicare silenziosamente senza attestazione.
- name: Publish with provenance
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Step 8. Fai partire il workflow creando una release su GitHub (o eseguendo il comando in locale la prima volta per validare la configurazione). npm, dietro le quinte, scambia il token OIDC con Fulcio, ottiene il certificato temporaneo, firma la dichiarazione in-toto e la invia a Rekor prima di completare l'upload del tarball sul registro. L'intero processo aggiunge in genere pochi secondi al tempo di publish complessivo: non è un ostacolo alla velocità della pipeline, solo un passaggio in più che avviene in modo trasparente prima che il pacchetto diventi scaricabile.
Step 9-10: Verificare le attestazioni e il badge di provenance
Step 9. Una volta pubblicato, controlla che l'attestazione sia effettivamente registrata. Il modo più diretto è interrogare i metadati del pacchetto e filtrare il campo attestations:
npm view safe-id-utils --json | jq '.dist.attestations'
Step 10. In parallelo, apri la pagina del pacchetto sul sito npmjs.com: se l'attestazione è valida vedrai un badge "Provenance" accanto al numero di versione, cliccabile per mostrare repository, commit e workflow di origine. È lo stesso segnale che oggi npm audit e strumenti come pnpm usano per assegnare un livello di fiducia a una versione pubblicata, distinguendo tra pacchetti con provenance verificata e pacchetti pubblicati "alla vecchia maniera".
Questo meccanismo di lettura non riguarda solo l'interfaccia web: pnpm, per esempio, interroga direttamente l'API delle attestazioni del registro per determinare come è stata pubblicata ciascuna versione, e usa la presenza (o assenza) di provenance SLSA e firma Sigstore per assegnare un livello di fiducia alla dipendenza. Significa che, una volta pubblicato con provenance, il tuo pacchetto diventa automaticamente più affidabile agli occhi degli strumenti che i tuoi utenti usano già, senza bisogno che loro configurino nulla di aggiuntivo lato consumo.
Step 11-12: Automatizzare il controllo con npm audit signatures e Trusted Publishing
Step 11. Non fermarti alla pubblicazione: verifica anche lato consumo. Nei tuoi progetti che dipendono da pacchetti terzi, integra npm audit signatures nella pipeline CI per segnalare dipendenze prive di firma o provenance valida prima che finiscano in produzione.
#!/usr/bin/env bash
# scripts/verify-provenance.sh
set -euo pipefail
echo "Verifica firme e provenance delle dipendenze..."
npm audit signatures
echo "Verifica provenance del pacchetto pubblicato..."
ATTESTATION=$(npm view "$1" --json | jq '.dist.attestations // empty')
if [ -z "$ATTESTATION" ]; then
echo "ERRORE: nessuna attestazione di provenance trovata per $1"
exit 1
fi
echo "Provenance presente per $1"
Step 12. Se lavori su npm CLI 11.5.1 o superiore, valuta il passaggio alla Trusted Publishing: configurando il collegamento tra il tuo pacchetto npm e il repository GitHub nelle impostazioni del registro, la provenance viene allegata automaticamente a ogni pubblicazione fatta dal workflow autorizzato, senza bisogno di gestire nessun NPM_TOKEN come secret. Il modello è opt-out anziché opt-in: una volta collegato il repository, il flag --provenance diventa ridondante e il token a lunga durata sparisce del tutto dalla pipeline.
# Con Trusted Publishing configurato (npm 11.5.1+), NODE_AUTH_TOKEN
# non è più necessario: l'autenticazione avviene via OIDC end-to-end.
- name: Publish (Trusted Publishing)
run: npm publish --access public
Output di esempio: cosa restituiscono davvero i comandi
Vale la pena vedere in anticipo cosa aspettarsi in console, così riconosci subito se qualcosa non torna. Un publish riuscito con provenance mostra righe come queste nel log del job GitHub Actions:
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice Bundled attestations:
npm notice - Provenance statement (application/vnd.in-toto+json)
npm notice - Bundle predicate (https://slsa.dev/provenance/v1)
+ [email protected]
La successiva ispezione con jq restituisce l'URL della singola attestazione registrata, non il contenuto completo (per quello serve seguire l'URL o usare npm audit signatures):
[
{
"predicateType": "https://slsa.dev/provenance/v1",
"url": "https://registry.npmjs.org/-/npm/v1/attestations/[email protected]"
}
]
Se invece la pubblicazione manca dei permessi corretti, il fallimento è esplicito e avviene prima ancora dell'upload, non a metà: npm error code EUSAGE seguito da un messaggio che segnala l'assenza del token OIDC. È un comportamento voluto: meglio un publish bloccato che uno pubblicato senza attestazione a causa di un errore silenzioso.
Errori comuni: i pitfall che vanificano la provenance
Nella pratica, configurare la provenance è la parte facile: la maggior parte dei team che ho visto adottarla nel 2026 falliva non sul comando in sé, ma su decisioni di configurazione che, prese di fretta, riaprono esattamente il varco che la provenance dovrebbe chiudere. Ecco i sei errori più frequenti, in ordine di frequenza osservata.
- Dimenticare id-token: write. È l'errore più frequente: senza questo permesso nel job GitHub Actions, il runner non può richiedere il token OIDC e la pubblicazione con
--provenancefallisce. Il problema arriva quando, invece di aggiungere il permesso, qualcuno toglie il flag--provenance"per farla funzionare": il publish riesce, ma perde silenziosamente ogni protezione, e spesso nessuno se ne accorge finché non serve davvero verificare l'origine di quella versione. - Trigger del workflow troppo permissivo. Attivare il job di publish su ogni push o, peggio, su
pull_request_targetda fork esterni apre la porta a chi può proporre modifiche al repository senza avere accesso diretto ai secret. In quello scenario la provenance attesterebbe correttamente una build partita da codice non fidato, dando una falsa sensazione di sicurezza a chi la controlla. - Token npm a vita lunga ancora nei secret. Molti team aggiungono provenance ma lasciano il vecchio
NPM_TOKENsenza scadenza come fallback "nel dubbio". È esattamente il tipo di credenziale che negli incidenti 2025-2026 è stata rubata e riusata per settimane prima di essere revocata, perché un token statico non fa scattare nessun allarme finché non viene usato per pubblicare qualcosa di anomalo. - Repository field disallineato. Se il campo
repositoryinpackage.jsonnon corrisponde al repository reale da cui parte la build (per esempio dopo un trasferimento di organizzazione GitHub non aggiornato ovunque), gli strumenti che leggono la provenance, incluso il badge sul sito npm, possono mostrare incongruenze che confondono proprio le persone che stanno cercando di verificare il pacchetto. - Confondere provenance con integrità del codice. Come mostra il caso TanStack, la provenance certifica la pipeline, non la bontà del codice eseguito al suo interno. Se un attaccante ha già compromesso il workflow prima della firma, l'attestazione risulterà tecnicamente valida pur descrivendo una build malevola: trattarla come garanzia assoluta, anziché come un livello aggiuntivo di difesa, è l'errore concettuale più pericoloso di questa guida.
- Non verificare le dipendenze in ingresso. Pubblicare con provenance il proprio pacchetto e ignorare la provenance delle centinaia di dipendenze transitive installate lascia scoperto il lato più esposto della catena: nella maggior parte degli incidenti 2025-2026 la vittima finale non era l'autore del pacchetto compromesso, ma chi lo aveva installato come dipendenza indiretta senza controlli automatizzati.
Troubleshooting: quando qualcosa non funziona
Anche seguendo tutti gli step alla lettera, capita che il primo tentativo di publish con provenance non vada a buon fine: il flusso OIDC coinvolge più sistemi (GitHub, Fulcio, Rekor, registro npm) e basta una configurazione fuori posto in uno solo di questi per far fallire tutto silenziosamente o con un messaggio poco chiaro. Ecco gli otto problemi più comuni segnalati da chi ha adottato questo setup nel 2025-2026 e come risolverli.
- "npm error 404 Not Found" durante il publish. Controlla che il nome del pacchetto in
package.jsonnon sia già occupato su npm e che--access publicsia presente se il pacchetto non è scoped o è la prima pubblicazione: senza questo flag npm assume per default un pacchetto privato e rifiuta la richiesta con un errore che sembra, a prima vista, un problema di nome. - "Unable to authenticate, need: Basic realm..." nel job CI. Il campo
registry-urlinactions/setup-nodenon è stato impostato, quindi npm non sa dove cercare le credenziali OIDC e ripiega su un metodo di autenticazione legacy che fallisce subito. Aggiungilo esplicitamente nello step di setup, prima di qualunque comandonpm cionpm publish. - Il badge "Provenance" non compare sulla pagina npm. La propagazione dei metadati al sito può richiedere qualche minuto dopo il publish. Se dopo 15-20 minuti non appare, verifica con
npm view <pkg> --json | jq '.dist.attestations'se l'attestazione esiste comunque a livello di registro: spesso il dato è già lì, è solo la UI del sito a essere leggermente indietro rispetto all'API. - "id-token permission not granted" anche con permissions impostati. Controlla che il permesso sia definito a livello di job e non solo a livello di workflow quando usi job multipli o job riutilizzabili (reusable workflows), perché in quel caso i permessi vanno propagati esplicitamente da chi chiama il workflow riutilizzabile, non ereditati automaticamente.
- npm audit signatures segnala pacchetti "unsigned" inaspettati. Molte librerie storiche non hanno ancora adottato provenance o firma Sigstore: non è un errore del tuo setup, è lo stato reale dell'ecosistema nel 2026. Usa il risultato per prioritizzare quali dipendenze sostituire, isolare in sandbox o monitorare con maggiore attenzione, non per bloccare a priori ogni build.
- Trusted Publishing non collega correttamente il repository. Verifica che l'URL del repository configurato lato npm corrisponda esattamente, incluso lo slug organizzazione/repo, a quello da cui parte il workflow: anche una differenza minima, come un trattino o una maiuscola diversa, fa fallire silenziosamente l'handshake OIDC.
- Il workflow fallisce solo sui runner self-hosted. I runner self-hosted spesso non hanno la stessa integrazione OIDC preconfigurata dei runner ospitati da GitHub: per Sigstore serve un runner GitHub-hosted, oppure una configurazione OIDC esplicita e testata sul runner self-hosted, cosa che molti team scoprono solo al primo tentativo di publish fallito.
- La build passa in locale ma fallisce in CI al momento del publish. Nella maggior parte dei casi è perché in locale stai ancora usando un token statico che bypassa completamente il flusso OIDC. Replica esattamente le condizioni del job CI, inclusi i permessi e le variabili d'ambiente, prima di dare per buono un test locale: un publish "che funziona sul mio computer" non garantisce nulla sulla provenance generata in pipeline.
Oltre la provenance: hardening avanzato della pipeline
Una volta che provenance e Trusted Publishing sono attivi, resta il compito più difficile: ridurre la superficie di attacco della pipeline stessa, visto che è lì che si sono concentrati gli incidenti più gravi del 2025-2026. Blocca le versioni delle GitHub Action di terze parti usate nel workflow tramite il commit SHA completo invece del tag mobile (per esempio actions/checkout@8459de8... invece di actions/checkout@v4): un tag può essere spostato da chi controlla quel repository, uno SHA no. Separa nettamente il job che esegue test e build (che può girare su pull request esterne) dal job che pubblica (che deve girare solo su trigger fidati come una release taggata da un maintainer).
Aggiungi un secondo livello di revisione umana per ogni release: un approvatore diverso dall'autore del commit che preme il tasto "pubblica" riduce drasticamente il rischio che una singola credenziale compromessa basti a far uscire un pacchetto malevolo. GitHub Environments con reviewer obbligatori è il modo più semplice per implementarlo senza strumenti aggiuntivi: colleghi il job di publish a un environment protetto e la pipeline si ferma finché una persona autorizzata non approva manualmente il deployment.
Disattiva l'esecuzione automatica degli script di postinstall per le dipendenze che installi in CI, con npm config set ignore-scripts true o l'equivalente flag --ignore-scripts: è proprio questo il meccanismo sfruttato nell'incidente Axios di marzo 2026 per eseguire il dropper RAT al semplice comando npm install, prima ancora che il codice dell'applicazione venisse toccato. Riattivalo selettivamente solo per i pacchetti che sai per certo richiederne uno, dopo averne verificato lo script. Infine, monitora attivamente il log di trasparenza Rekor per il tuo pacchetto: se compare una firma non generata dal tuo workflow, è un segnale di compromissione da investigare subito, prima ancora che qualcuno segnali il pacchetto come sospetto sul registro pubblico.
Per una vista d'insieme sulla postura di sicurezza dell'intero repository, non solo della fase di publish, vale la pena eseguire periodicamente OpenSSF Scorecard: lo strumento assegna un punteggio automatico che tiene conto proprio di fattori come la presenza di provenance, il pinning delle GitHub Action, la revisione obbligatoria delle pull request e la presenza di branch protection, aggregando in un solo numero molte delle pratiche descritte in questa guida. Trovi la metodologia completa e la dashboard pubblica dei risultati su securityscorecards.dev, dove puoi anche confrontare il punteggio del tuo progetto con quello di librerie popolari dello stesso ecosistema.
Il progetto completo pronto da clonare
Struttura finale del repository
Mettendo insieme tutti gli step, il repository safe-id-utils risultante contiene: il codice sorgente in src/index.js, i test in test/index.test.js, il workflow .github/workflows/publish.yml con permessi OIDC e step di provenance, lo script scripts/verify-provenance.sh da lanciare nella pipeline dei consumatori del pacchetto, e un package.json con i metadati di repository allineati. È lo scheletro minimo che puoi copiare in un progetto reale sostituendo nome pacchetto, logica applicativa e organizzazione GitHub. Su un pacchetto vero aggiungerai probabilmente un linter, un bundler o un passo di type-checking, ma la parte che riguarda provenance, permessi OIDC e verifica delle attestazioni resta identica indipendentemente dalla complessità del codice che stai pubblicando.
La tabella seguente riassume come la provenance si posiziona rispetto ad altri due strumenti spesso citati insieme, SBOM e firma classica dei pacchetti, perché non sono alternative ma livelli complementari.
| Meccanismo | Cosa dimostra | Dove si genera | Limite principale |
|---|---|---|---|
| Provenance (npm + Sigstore) | Da quale repository, commit e workflow è nato l'artefatto | Automaticamente al publish, via OIDC | Non certifica che il workflow eseguisse codice onesto |
| SBOM (CycloneDX / SPDX) | Quali componenti e dipendenze compongono il pacchetto | Generato da tool dedicati (es. cyclonedx-npm) | Non prova l'origine della build, solo la composizione |
| Firma classica (GPG/PGP) | Che il pacchetto è firmato da una chiave specifica | Manuale, richiede gestione chiavi a lungo termine | Chiavi statiche più difficili da ruotare e più esposte a furto |
Per una difesa solida, i tre livelli vanno combinati: la provenance copre l'origine della build, l'SBOM copre la composizione delle dipendenze (argomento che abbiamo approfondito nella guida dedicata a SBOM e dependency scan in Node.js), e pratiche come 2FA obbligatoria e revisione delle release coprono il fattore umano che resta, ancora nel 2026, l'anello più debole della catena.
Prima di chiudere il repository e passare al prossimo progetto, usa questa checklist rapida per verificare di non aver saltato nessun passaggio critico della guida:
- 2FA attiva sull'account npm per login e publish (Step 3)
- Nessun token npm a vita lunga salvato come secret, oppure Trusted Publishing configurata (Step 4 e 12)
- Permesso
id-token: writepresente nel job che pubblica, non solo nel workflow (Step 5) - Trigger di publish limitato a eventi fidati come le release taggate, mai a pull request esterne (Step 6)
- Flag
--provenancepresente nel comando di pubblicazione (Step 7) - Badge "Provenance" verificato sulla pagina npm dopo il primo publish (Step 9-10)
npm audit signaturesintegrato nella pipeline CI dei progetti che consumano il pacchetto (Step 11)- GitHub Action di terze parti pinnate allo SHA del commit, non a un tag mobile (hardening avanzato)
Domande frequenti
La provenance npm sostituisce npm audit?
No. npm audit cerca vulnerabilità note nelle dipendenze già installate, mentre la provenance certifica l'origine di un pacchetto pubblicato. Sono controlli complementari e vanno usati entrambi nella pipeline.
Serve una chiave privata da gestire per firmare con Sigstore?
No, ed è proprio il punto di forza del modello keyless: Fulcio emette certificati temporanei legati all'identità OIDC del runner CI, quindi non esiste una chiave a lungo termine da custodire, ruotare o che possa essere rubata da un disco.
Cosa succede se pubblico senza --provenance per errore?
Il pacchetto viene pubblicato normalmente ma senza attestazione: nessun badge sul sito npm e nessun record in dist.attestations. Puoi pubblicare una nuova versione con provenance attiva, ma non puoi aggiungere retroattivamente l'attestazione a una versione già pubblicata.
La provenance funziona anche con GitLab CI o solo con GitHub Actions?
Il meccanismo OIDC alla base della provenance npm è documentato ufficialmente per GitHub Actions e GitLab CI. Altri provider CI possono funzionare se espongono un provider OIDC compatibile, ma vanno verificati caso per caso prima di affidarcisi in produzione.
Come faccio a sapere se una dipendenza che uso ha provenance verificata?
Lancia npm audit signatures nel tuo progetto: il comando controlla firme e attestazioni di tutte le dipendenze installate e segnala quelle prive di verifica, così puoi decidere se sostituirle o monitorarle con attenzione maggiore.
Trusted Publishing elimina completamente il rischio di token rubati?
Elimina il rischio legato a token statici salvati come secret, che è stato il vettore in diversi incidenti 2025-2026, perché non esiste più un token permanente da poter esfiltrare da un secret store o da un log mal configurato. Non elimina però il rischio di un account maintainer compromesso a livello GitHub, o di un workflow modificato da chi ha accesso al repository: per questo va comunque abbinato a 2FA, revisione obbligatoria delle release e pinning delle GitHub Action allo SHA.
Posso applicare questa guida a un monorepo con più pacchetti pubblicati insieme?
Sì: il flag --provenance e i permessi id-token: write funzionano allo stesso modo per pubblicazioni multiple in uno stesso job, a patto che ogni pacchetto abbia il proprio campo repository corretto in package.json.




