Secrets nelle pipeline: come non finire con le chiavi su GitHub - init.d
EN

# Secrets nelle pipeline: come non finire con le chiavi su GitHub

Alessandro Corbelli~7 min
Table of Contents

Una pipeline che pubblica in produzione ha bisogno di credenziali: un token del provider cloud, una chiave SSH, la password di un database. Sono le chiavi di casa, consegnate a una macchina che lavora da sola a ogni modifica del codice.

Il modo in cui quelle chiavi vengono conservate è il punto più delicato di tutta l’automazione, e anche quello che si sbaglia più spesso. Non per ignoranza: si sbaglia di fretta, il venerdì, quando il rilascio deve partire e la strada breve funziona subito. Un token incollato dentro un file di configurazione fa esattamente il suo lavoro, e nessuno se ne accorge finché non è troppo tardi.

Vediamo dove vanno messe davvero, cosa fare quando ne è già uscita una, e qual è oggi la soluzione migliore: smettere del tutto di avere chiavi da conservare.

Un segreto nel repository non si cancella

La prima regola è che una credenziale non va mai scritta in un file versionato. Il motivo non è ovvio: il problema non è il file, è la cronologia.

Git conserva ogni versione precedente di ogni file. Se scrivi un token in un file e domani lo cancelli, il file di oggi è pulito, ma il salvataggio di ieri contiene ancora la credenziale, e resta consultabile da chiunque abbia accesso al repository, per sempre. Chi arriva sei mesi dopo non deve nemmeno cercare: basta scorrere le modifiche.

Da qui discende una conseguenza che sorprende sempre: cancellare il segreto non risolve niente. Se una credenziale è finita in un repository, quella credenziale va considerata compromessa e va sostituita, cioè revocata sul servizio che l’ha emessa e rigenerata. Riscrivere la cronologia è possibile, con strumenti pensati apposta, ma è un’operazione invasiva che rompe le copie di tutti e non ti dà comunque nessuna garanzia su chi ha già letto.

Se il repository è pubblico, il tempo a disposizione è ancora minore: esistono programmi che sorvegliano le pubblicazioni su GitHub in tempo reale cercando credenziali, e i minuti tra la pubblicazione e l’uso sono davvero pochi. GitHub stesso analizza il codice pubblico alla ricerca di chiavi note e avvisa il fornitore che le ha emesse, che spesso le revoca d’ufficio. È una rete di sicurezza, non una soluzione.

Il posto giusto: il magazzino del sistema CI

Ogni sistema di pipeline CI/CD ha un posto dove conservare le credenziali fuori dal codice: si chiamano secrets su GitHub Actions, variabili CI/CD su GitLab. Il valore si inserisce una volta dall’interfaccia, e nella pipeline si usa solo il nome:

- run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Chi legge il file vede DEPLOY_TOKEN, non il suo valore. Ma metterli lì non basta: contano due impostazioni che quasi nessuno cerca.

Il mascheramento impedisce che il valore compaia nei registri di esecuzione. Senza, basta un comando che stampa l’ambiente, o un errore un po’ loquace di un programma, perché la credenziale finisca in chiaro in un registro che di solito è leggibile da molte più persone di quante possano vedere le impostazioni.

La limitazione ai rami protetti decide quali esecuzioni ricevono il segreto. È la protezione più importante e la meno usata: senza, chiunque possa creare un ramo nel progetto può scrivere una pipeline di tre righe che stampa il token e leggerselo con calma. Se il segreto è riservato al ramo principale, e il ramo principale accetta modifiche solo dopo revisione, quella strada si chiude.

Il salto di qualità: nessuna chiave da conservare

La protezione migliore per una chiave è non averla. Da qualche anno i sistemi CI più diffusi permettono di autenticarsi al provider senza credenziali permanenti, con un meccanismo che si chiama OIDC (OpenID Connect).

Funziona così. Al momento dell’esecuzione, il sistema CI genera un documento firmato che attesta chi sta chiedendo: questo repository, questo ramo, questa esecuzione. La pipeline lo presenta al provider cloud, che lo verifica e, se corrisponde a una regola configurata in precedenza, restituisce credenziali temporanee, valide per pochi minuti.

Su GitHub Actions sono due blocchi:

permissions:
id-token: write # la pipeline può chiedere il documento di identità
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy-sito
aws-region: eu-south-1

Non c’è nessuna chiave da nessuna parte. Il ruolo deploy-sito, lato provider, è configurato per fidarsi solo di quel repository e solo di quel ramo: un altro progetto che provasse la stessa richiesta verrebbe rifiutato.

Il guadagno è grosso. Non c’è niente da rubare, perché le credenziali durano quanto l’esecuzione. Non c’è niente da ruotare ogni sei mesi, e quindi niente che scada di venerdì pomeriggio. E la fiducia è legata a un’identità verificabile invece che al possesso di una stringa, che è la stessa differenza che passa tra un documento e una chiave trovata per terra. Vale la pena adottarlo appena il provider lo supporta, e oggi lo supportano i principali.

Privilegi minimi, che vale anche per le identità

Chiave o identità temporanea che sia, resta la domanda: quanto può fare quella credenziale? La risposta corretta è: il minimo indispensabile.

Un token che pubblica un sito deve poter pubblicare quel sito. Non amministrare l’account, non leggere le altre applicazioni, non toccare i DNS. È una differenza che si sente solo il giorno dell’incidente, ed è quel giorno che decide se il danno è “hanno ripubblicato il sito” oppure “hanno in mano tutto”.

Lo stesso ragionamento vale dentro la pipeline. Su GitHub Actions il blocco permissions limita cosa può fare l’esecuzione sul repository stesso: dichiararlo esplicitamente, invece di lasciare i valori predefiniti, evita che uno script eseguito nella pipeline possa scrivere dove non deve. Il principio è lo stesso che si applica a un accesso amministrativo ai server o a un’autorizzazione OAuth: un permesso concesso “per comodità” è un permesso che prima o poi verrà usato da qualcun altro.

Le tre trappole che vediamo più spesso

Le proposte di modifica da estranei. Se il progetto accetta contributi esterni, una pipeline che parte in risposta a una proposta di modifica esegue codice scritto da chi l’ha inviata. Su GitHub esiste un tipo di innesco, pull_request_target, che gira con i permessi del repository di destinazione: usarlo insieme al prelievo del codice proposto significa consegnare i propri segreti a uno sconosciuto. È una delle configurazioni sbagliate più sfruttate in assoluto.

Le dipendenze. La pipeline installa librerie, e installarle vuol dire eseguire codice altrui su una macchina che ha in mano le credenziali di produzione. Un pacchetto compromesso, come in tutti gli episodi visti su npm, legge le variabili d’ambiente e le spedisce fuori. Difese pratiche: bloccare gli script di installazione, congelare le versioni, e non passare i segreti agli stadi che non devono pubblicare. Chi esegue i test non ha bisogno del token di produzione.

I registri. Un comando lanciato con la variabile in linea, un curl -v che stampa le intestazioni, un errore che riporta l’URL completo con il token dentro: i registri di esecuzione sono spesso il punto da cui il segreto esce, e sono anche il posto dove nessuno pensa di guardare.

Se un segreto è già uscito

Nell’ordine, senza saltare passaggi. Revocare subito la credenziale sul servizio che l’ha emessa: è l’unica azione che ferma davvero l’emorragia, e va fatta prima di ogni altra cosa. Rigenerarne una nuova, con i permessi minimi che servono davvero, e metterla nel magazzino del sistema CI. Controllare cosa è stato fatto con quella vecchia, guardando i registri di accesso del provider nelle ore precedenti. Solo alla fine, se ha senso, ripulire il file.

E una volta sistemato, vale la pena aggiungere una rete: esistono programmi che analizzano il codice cercando credenziali e che si possono far girare automaticamente prima di ogni salvataggio, così la prossima chiave incollata di fretta viene fermata sul posto invece che scoperta sei mesi dopo.

In sintesi

I segreti di una pipeline non vanno nel repository, perché la cronologia di Git non dimentica e una credenziale finita lì va considerata bruciata: si revoca e si rigenera, non si cancella. Vanno nel magazzino del sistema CI, con il mascheramento nei registri e la limitazione ai rami protetti, che è quella che impedisce a un ramo qualsiasi di leggerseli. Meglio ancora, non vanno conservati affatto: con OIDC la pipeline dimostra chi è e riceve credenziali che durano minuti, quindi non c’è niente da rubare e niente da ruotare. Su tutto vale il principio dei privilegi minimi, che è ciò che decide se un incidente sarà una seccatura o un disastro.

Fonti

Tux versione Gandalf, mascotte del blog init.d

init.d è il team di Alessandro Corbelli, sistemista Linux e sviluppatore backend con oltre vent’anni di esperienza. Progetta e gestisce infrastrutture cloud (Google Cloud, AWS, Azure), server farm e architetture ad alta disponibilità, e sviluppa software su misura in Laravel/PHP e Vue - dalla piattaforma di food delivery Take2Me ai gestionali dei nostri clienti. Su questo blog condividiamo note tecniche su Linux, sistemistica, sviluppo, DevOps ed e-commerce.


Altri articoli