CI/CD sicuro: proteggere la pipeline da injection e supply chain attack
A marzo 2026 un'azienda del settore servizi finanziari con circa 40 dipendenti - fatturato annuo nell'ordine dei 9 milioni di euro, pipeline DevOps maturo su GitHub Actions con deploy automatici verso cinque VPS di produzione - mi ha commissionato un audit di sicurezza specificamente sulla pipeline CI/CD dopo aver letto un post tecnico su un incidente di supply chain attack avvenuto nel 2025 su un pacchetto NPM ampiamente utilizzato. Il CTO mi aveva detto chiaramente: "abbiamo la certezza che la nostra applicazione sia ben difesa, ma non abbiamo mai valutato la sicurezza del processo con cui la deploiamo. Se qualcuno compromettesse la nostra pipeline, avrebbe accesso a tutto". L'audit ha richiesto due settimane di lavoro su una base di 47 workflow GitHub Actions, 312 step di build distribuiti su 12 repository, e 28 action di terze parti utilizzate. Il risultato è stato preoccupante: 8 vulnerabilità di severity alta identificate, incluse due che avrebbero potenzialmente permesso a un attaccante di ottenere i segreti di produzione compromettendo un singolo repository, e una che avrebbe permesso RCE remoto su un runner self-hosted attraverso una PR maligna da un fork esterno.
Le categorie di vulnerabilità trovate non erano esotiche - erano pattern comuni che l'industria ha documentato negli ultimi anni, ma che in operatività quotidiana sfuggono agli audit tradizionali perché concentrati sull'applicazione, non sulla pipeline. In cinque settimane abbiamo rimediato tutti gli 8 issue critici attraverso una combinazione di hardening tecnico (pinning delle action a commit SHA, restrizione dei permessi, isolamento dei runner) e policy organizzative (review obbligatoria su modifiche ai workflow, audit periodico delle action di terze parti). Al termine del lavoro, il livello di sicurezza della pipeline è allineato con le best practice documentate nell'OpenSSF Scorecard, il framework di riferimento della Open Source Security Foundation per valutare la sicurezza delle pipeline CI/CD, con punteggio medio sopra 8.5/10 contro il 5.2/10 di partenza. Questo articolo descrive le categorie di vulnerabilità più insidiose, i pattern di attacco documentati negli ultimi due anni, e le remediation concrete che applico sistematicamente sui clienti che hanno pipeline CI/CD mature.
Perché le pipeline CI/CD sono diventate bersaglio privilegiato nel 2024-2026
La trasformazione del panorama di minaccia sulle pipeline CI/CD negli ultimi 24 mesi è stata significativa. Tre fattori hanno convergiuto. Primo: la maturazione delle architetture DevOps significa che le pipeline hanno acquisito privilegi sempre più ampi - accesso a segreti di produzione, capacità di deploy diretto verso server live, integrazione con strumenti di observability, connessioni a cloud provider per provisioning infrastruttura. Una pipeline compromessa non è più "un sistema che builda codice", è "il sistema nervoso dell'organizzazione IT". Secondo: il modello di distribuzione delle action (su GitHub) e dei package (NPM, PyPI, Composer) ha creato catene di fiducia complesse dove un singolo punto debole può propagarsi a migliaia di consumer. Terzo: gli attaccanti hanno capito questo e hanno strumentato tecniche specifiche per exploit.
L'incidente più rilevante del biennio è stato quello di tj-actions/changed-files documentato nel GitHub Security Advisory GHSA-mrrh-fwg8-r2c3 del marzo 2025, dove un attaccante ha compromesso una popolare GitHub Action (utilizzata in oltre 20.000 workflow pubblici) e ha modificato silenziosamente il codice per esporre i segreti nei log. Tutti i consumer che referenziavano l'action con tag mutabili (es: @v46, @main) hanno istantaneamente ricevuto la versione maligna al successivo run. I consumer che invece avevano pinnato a un SHA specifico erano protetti. Questo incidente da solo ha convertito al pinning SHA migliaia di team che prima usavano tag - ma il pattern di usare tag mutabili è ancora predominante nel mercato, e rappresenta la prima categoria di vulnerabilità che trovo negli audit.
La MITRE ATT&CK matrix documenta ufficialmente le tecniche di supply chain attack con dettaglio degli step di exploitation, e il pattern generale è prevedibile: l'attaccante identifica un componente della pipeline che molti team usano, trova modo di inserirsi nel processo di pubblicazione di quel componente (compromettendo un maintainer, sfruttando un bug nel sistema di release, o semplicemente acquisendo un pacchetto abbandonato), pubblica una versione modificata, aspetta che i consumer la scarichino automaticamente. Senza pinning, questa catena funziona sempre.
Categoria 1: action pinned a tag mutabili invece che commit SHA
Il pattern più comune che trovo negli audit è l'uso di tag mutabili come riferimento per action di terze parti. Esempio tipico da un workflow reale auditato:
# .github/workflows/deploy.yml - INSICURO
name: Deploy to production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # tag mutabile
- uses: shivammathur/setup-php@v2 # tag mutabile
- uses: tj-actions/changed-files@v46 # tag mutabile (esempio dell'incidente)
- uses: actions/cache@v4 # tag mutabile
- name: Deploy
run: ./deploy.shIl problema: i tag @v4, @v2, @v46 sono puntatori a un commit SHA che l'autore dell'action può cambiare in qualunque momento. Se il repository dell'action viene compromesso (come è successo a tj-actions), l'attaccante sposta il tag su un commit maligno, e tutti i workflow che usano quel tag iniziano immediatamente a eseguire codice malevolo. Non c'è nessun meccanismo di review che intercetti questo cambiamento - il workflow gira e basta.
La fix è di pinnare ogni action a un commit SHA specifico, accompagnato da un commento leggibile con il tag equivalente per futura manutenzione:
# .github/workflows/deploy.yml - SICURO
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
- name: Deploy
run: ./deploy.shIl commit SHA è immutabile per definizione in Git - nessuno può cambiare il codice dietro quel SHA senza inventare una collisione SHA-256 (computazionalmente infeasible). Se l'action viene compromessa, i consumer pinned a SHA specifico continuano a usare la versione vecchia sicura. Il trade-off è che gli aggiornamenti richiedono effort manuale: cambiare SHA, verificare changelog, testare. Questo effort può essere ridotto con Renovate Bot configurato per proporre PR di update automatiche con verifica dei changelog, pattern descritto in dettaglio nel mio articolo sull'aggiornamento automatico delle dipendenze PHP con Dependabot e Renovate - il caso GitHub Actions è analogo al caso Composer.
Sul cliente fintech, la migrazione di 312 step a commit SHA ha richiesto 4 giorni di lavoro distribuiti. L'attività è sistematica ma non complessa: per ogni uses: owner/action@tag, guardare il repository dell'action, identificare il commit SHA corrispondente al tag corrente, aggiornare. Un piccolo script Python ha automatizzato il 90% del lavoro, il restante 10% ha richiesto review manuale su action che avevano release obsolete o incoerenti.
Categoria 2: permessi eccessivi del GITHUB_TOKEN
Il secondo pattern pericoloso è usare il GITHUB_TOKEN con permessi default - che in molti repository ancora significa "write access a tutto". Un workflow compromesso con token write-all può modificare il codice della branch, caricare artifacts malicious, alterare le release del progetto.
La best practice è dichiarare i permessi minimi necessari esplicitamente, a livello di workflow o di singolo job:
# Permessi default a livello workflow: read only
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
# Nessun permesso aggiuntivo per build
steps:
- uses: actions/checkout@...
- run: composer install
deploy:
runs-on: ubuntu-latest
# Deploy ha bisogno di permessi specifici aggiuntivi
permissions:
contents: read
deployments: write
steps:
- run: ./deploy.sh
create-release:
runs-on: ubuntu-latest
# Solo il job di release può creare tag
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/create-release@...Il pattern least privilege applicato al livello granulare dei job minimizza la superficie di attacco. Un job di build che viene compromesso (es: un pacchetto NPM malicious installato tramite npm install) non può modificare il repository perché non ha write permission. Un job di deploy compromesso può eseguire deploy malicious ma non può alterare la storia del repo. Ogni job ha solo i permessi necessari per la sua funzione specifica.
Stai cercando un Consulente Informatico esperto per auditare la sicurezza della tua pipeline CI/CD su GitHub Actions o GitLab CI, identificando vulnerabilità di supply chain attack e implementando hardening calibrato sulla tua criticità operativa? Nel mio profilo professionale trovi l'esperienza concreta su security automation, pipeline hardening, e audit DevSecOps per PMI italiane.
Categoria 3: secret exposure nei log
Il terzo pattern trovo consistentemente è l'esposizione di secret nei log di esecuzione. GitHub Actions maschera automaticamente i valori dei secret se vengono stampati direttamente (sostituendoli con ***), ma questo meccanismo può essere aggirato involontariamente in diversi modi.
Pattern 1 - concatenazione con altri valori. Se lo script concatena un secret con una stringa statica e la stampa, GitHub Actions potrebbe non riconoscere il pattern cambiato. Esempio:
# VULNERABILE: il secret viene modificato prima di stampare
echo "Deploy with token: ${DEPLOY_TOKEN:0:10}..."Pattern 2 - output strutturato che include il secret. Un comando che emette JSON con il secret al suo interno potrebbe non essere correttamente mascherato:
# VULNERABILE: il secret finisce in un JSON che viene loggato
curl -X POST "$API_URL" \
-H "Authorization: Bearer $API_TOKEN" \
-d '{"token": "'$API_TOKEN'"}'Pattern 3 - script di error handling che dumpa environment. Molti script per debugging fanno env | sort quando qualcosa fallisce. Se ci sono secret nell'environment (cosa che succede con GitHub Actions), finiscono nel log.
Le fix sono relativamente semplici ma richiedono disciplina. Mai manipolare secret in modo che GitHub non lo riconosca. Mai mettere secret in parametri URL di curl (usare -H Authorization o file). Mai dumpare environment completo in caso di errore - dumpare solo variabili specifiche già notoriamente sicure.
Uno script di verifica che applico nell'audit cerca pattern sospetti nei workflow:
# Script di audit per pattern secret-exposure
find .github/workflows -name "*.yml" -exec grep -Hn -E \
"(env \|\| printenv|base64 \$\{?SECRETS|echo.*token|set -x)" {} \;Questo script segnala candidati a review manuale. Sul cliente fintech ha trovato 7 workflow con pattern sospetti, di cui 3 erano veri rischi fixati, 4 erano false positive.
Categoria 4: workflow trigger da pull request di fork esterni
Il pattern più insidioso di tutti è l'uso di pull_request come trigger per workflow che hanno accesso a secret o permessi elevati. Il problema: una pull request da un fork esterno può modificare il workflow stesso, e il workflow modificato viene eseguito con i permessi del repository target. Se il workflow ha accesso a secret di produzione, un attaccante può semplicemente aprire una PR che modifica il workflow per leakare quei secret.
La difesa di GitHub è che i workflow eseguiti su PR da fork non hanno accesso ai secret per default (pull_request trigger senza pull_request_target). Ma molti team usano pull_request_target senza capire le implicazioni - questo trigger include i secret ed è pericoloso combinato con execution di codice dal fork.
Il pattern sicuro è: i workflow che testano PR esterne devono girare su pull_request (no secret), mentre quelli che hanno bisogno di secret vanno separati in workflow triggered manualmente da label o approval.
Esempio di pattern insicuro che ho trovato:
# .github/workflows/test-pr.yml - INSICURO
on:
pull_request_target: # con accesso a secret
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@...
with:
ref: ${{ github.event.pull_request.head.sha }}
# scarica codice dal fork!
- run: composer install
- run: composer test
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} # token esposto!Un attaccante fork-a il repo, aggiunge al composer.json uno script post-install che leggi DEPLOY_TOKEN e lo invia a un server esterno, apre PR. Il workflow scarica il codice dal fork, esegue composer install che triggera lo script maligno, e il token viene esfiltrato.
La fix corretta è separare i workflow:
# .github/workflows/test-pr.yml - SICURO (no secret)
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@...
- run: composer install
- run: composer test
# Workflow separato per deploy che richiede secret,
# triggerato solo da label aggiunta da maintainer
on:
pull_request_target:
types: [labeled]
jobs:
deploy:
if: github.event.label.name == 'deploy-preview'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@...
# Nota: NON scarichiamo dal fork, ma dal branch main
with:
ref: main
- name: Deploy preview
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./deploy-preview.shIl secondo workflow gira solo quando un maintainer aggiunge esplicitamente la label deploy-preview alla PR - un atto di autorizzazione esplicita che conferma che il codice della PR è stato review-ato. Inoltre, checkout dalla branch main (non dal fork) evita di eseguire codice potenzialmente malicious.
Categoria 5: runner self-hosted senza isolamento
L'ultima categoria riguarda i runner self-hosted. Molti team li usano per avere controllo su hardware, per accedere a risorse di rete interne, o per evitare il costo dei runner GitHub cloud. Il rischio: un runner self-hosted che esegue workflow da PR esterne diventa un vettore di attacco dove codice arbitrario gira sulla tua infrastruttura.
Le regole di sicurezza per runner self-hosted sono tre. Prima: mai usare runner self-hosted su repository pubblici senza isolamento rigoroso. Seconda: se usi runner self-hosted su repository privati, isola ogni run in container effimeri che vengono distrutti dopo ogni esecuzione. Terza: never allow fork PR to run on self-hosted runners - questa è la regola cardinale. Il GitHub security warning sulla sicurezza dei runner self-hosted è chiaro sul tema.
Per il cliente fintech, abbiamo riorganizzato i runner self-hosted per eseguire solo workflow da branch protette e PR interne (no fork), in container ephemeral orchestrati da un sistema dedicato. Lo stesso pattern è applicabile a GitLab Runner - la logica è la stessa, cambia la sintassi.
Monitoring e detection: come sapere se qualcuno ha tentato qualcosa
Oltre all'hardening preventivo, il monitoring delle pipeline per attività sospette è il secondo livello di difesa. I segnali che monitoro sono cinque. Primo: workflow runs che vengono cancellati prima del completamento - può essere un attaccante che ha capito di essere stato rilevato. Secondo: modifiche ai workflow in branch non-main non rivedute - possibile injection in preparazione. Terzo: accessi anomali ai secret via audit log di GitHub (chi ha letto i secret, quando) - idealmente un secret viene letto solo dai workflow conosciuti. Quarto: creazione di nuove action self-hosted non pianificata. Quinto: cambi di settings repository (team membership, permissions, branch protection).
Tutti questi segnali sono disponibili nell'audit log di GitHub Enterprise e possono essere esportati via API per ingestione in un SIEM. Sul cliente fintech abbiamo configurato export settimanale dell'audit log, parsing per i pattern sopra, e alert su Slack. Nei sei mesi successivi non ci sono stati incident veri (per fortuna), ma il sistema è in piedi per il giorno in cui dovesse capitare. Il pattern si integra con la logica generale di analisi forense di attacchi e ricostruzione della kill chain - avere dati disponibili prima che serva un'investigazione forense è quello che separa un'indagine in 4 ore da una in 4 settimane.
Policy organizzative: la disciplina che rende sostenibile l'hardening
L'hardening tecnico è solo metà del lavoro. L'altra metà è la disciplina organizzativa che impedisce regressioni nel tempo. Tre policy che applico su tutti i clienti.
Primo: CODEOWNERS obbligatorio su .github/workflows/. Il file CODEOWNERS di GitHub dichiara che ogni modifica ai workflow richiede review da parte di un team specifico (tipicamente il team DevOps/Security). Un developer che accidentalmente regressa l'hardening non può mergiare senza approval esplicito.
Secondo: branch protection con "require pull request reviews" su main. Nessuno pusha direttamente su main, tutto passa per PR review. Questo è il basic security hygiene che spesso è disabilitato su team piccoli.
Terzo: audit semestrale delle action di terze parti. Ogni 6 mesi, un tecnico designato rivede la lista delle action usate, verifica che siano ancora attivamente mantenute, aggiorna ai commit SHA più recenti, dismissiona action abbandonate. Questa attività richiede 2-4 ore ogni semestre e previene il problema di accumulare dipendenze su codice non più supportato.
ROI della pipeline sicura: il costo evitato che conta
Il beneficio di questo lavoro è difficile da quantificare finché non succede un incidente. Un singolo incidente di supply chain in una pipeline che tocca produzione può costare alla PMI italiana nell'ordine di 50.000-200.000 euro fra investigation, remediation, downtime, notifica obbligatoria (GDPR/NIS2), eventuale perdita di fiducia dei clienti. Il costo di hardening preventivo è nell'ordine di 15.000-25.000 euro per una pipeline media, e rende quasi impossibile il pattern di attacco più comune. L'aritmetica è favorevole.
Sul cliente fintech, nei 9 mesi successivi al completamento dell'audit e remediation, i numeri sono: zero incident di security sulla pipeline, 4 tentativi di attacco rilevati dal monitoring (tutti da bot automatici che scannerizzano repository pubblici cercando pipeline mal configurate, nessuno ha avuto successo), 28 update di action completati tramite Renovate+review senza regressioni. Il cliente ha inoltre superato con osservazioni positive un audit da parte di un cliente enterprise del settore bancario che aveva incluso la sicurezza DevOps nel questionario di vendor assessment - "processo di pinning rigoroso delle action" è stata citata come pratica sopra la media.
Se gestisci pipeline CI/CD complesse su GitHub Actions, GitLab CI o equivalenti, e non hai mai fatto un audit specifico di sicurezza sulla pipeline stessa (anche se hai audit regolari sull'applicazione), oppure hai dei runner self-hosted dove non sei sicuro di aver isolato correttamente i workflow da codice esterno, contattami per un audit: in una settimana di lavoro rivedo i tuoi workflow, identifico le categorie di vulnerabilità più impattanti, propongo remediation calibrata sulla tua tolleranza al cambiamento (alcune fix sono invasive e richiedono migrazione di approach, altre sono one-line), e ti consegno un piano in priorità per il rollout. L'investimento è contenuto e il ritorno è la tranquillità che la pipeline che deploya il tuo software in produzione non sia essa stessa il vettore di un futuro incidente.