Tre livelli e una sola regola di precedenza

Un secret di Actions si può definire a tre livelli: repository, organization e environment. In tutti i casi il valore viene cifrato prima di arrivare a GitHub e non è più leggibile dalla console: un amministratore può sovrascriverlo o cancellarlo, non rileggerlo. Percorso da conoscere: per repository e organizzazione si va in Settings > Security > Secrets and variables > Actions, scheda Secrets, con i pulsanti New repository secret e New organization secret; i segreti di environment stanno invece in Settings > Environments, dentro la sezione Environment secrets.

Quando lo stesso nome esiste a più livelli non c’è un errore né un merge: vince il livello più vicino al job. L’ordine è environment, poi repository, poi organization. Tradotto in pratica: un segreto di organizzazione è un default che il team di un repository può silenziosamente sostituire, e un environment può sostituirlo di nuovo. È esattamente il tipo di dettaglio su cui l’esame costruisce le domande a scenario, del tipo “il workflow usa credenziali di staging in produzione, perché?”. La risposta quasi sempre è che qualcuno ha ridefinito il nome a un livello più basso.

I nomi seguono regole rigide: solo caratteri alfanumerici e underscore, non possono iniziare con una cifra né con il prefisso GITHUB_, e il riferimento è case-insensitive anche se il valore viene memorizzato in maiuscolo. Un altro comportamento da ricordare è che, con la sola eccezione di GITHUB_TOKEN, i segreti non vengono passati al runner quando il workflow è innescato da un fork. Allo stesso modo, un reusable workflow non eredita nulla automaticamente: o si elencano i segreti con la chiave secrets: nel job chiamante, o si usa la scorciatoia secrets: inherit.

jobs:
  deploy:
    uses: octo-org/shared/.github/workflows/deploy.yml@main
    secrets: inherit

Restringere un segreto di organizzazione ai repository giusti

Quando si crea un segreto a livello di organizzazione compare il campo Repository access, con tre opzioni: All repositories, Private repositories e Selected repositories. L’ultima è quella che un amministratore dovrebbe usare come impostazione predefinita mentale: si condivide il segreto solo con l’elenco di repository che ne hanno davvero bisogno. La stessa policy si gestisce via REST, con gli endpoint che elencano e impostano i repository selezionati per un dato segreto, utili quando l’assegnazione va mantenuta da automazione anziché a mano.

Chi può fare cosa è parte del dominio di governo. Per creare un segreto di repository serve accesso in scrittura al repository; per un segreto di environment serve accesso di tipo admin sul repository; per un segreto di organizzazione servono i permessi di organization owner, oppure un custom organization role che includa il permesso Manage organization Actions secrets. Delegare con un ruolo personalizzato è preferibile a distribuire la proprietà dell’organizzazione.

Qui si annida la confusione più costosa. La policy di accesso decide quali repository possono referenziare il segreto, non quali persone possono vederne il valore. Chiunque riesca a far girare un workflow in un repository autorizzato può, con qualche accortezza, esfiltrare quel valore: la redazione automatica nei log riduce il rischio, ma la documentazione è esplicita nel dire che non è garantita. Da qui due indicazioni di hardening che conviene trasformare in policy interna: non incapsulare mai più valori sensibili in un blob JSON, XML o YAML dentro un unico segreto, perché la mascheratura diventa inaffidabile, e ruotare periodicamente i segreti riducendo la finestra di validità di una credenziale compromessa.

Environment secrets e il gate di approvazione

Un environment è l’unico strumento che lega un segreto a un’autorizzazione umana. I segreti memorizzati in un environment sono disponibili solo ai job che dichiarano quell’environment, e se sull’environment sono configurati i required reviewers il job non può accedere ai segreti finché uno dei revisori non approva. Non è un semplice avviso: il valore non viene proprio consegnato al runner prima dell’approvazione. Accanto ai revisori si configurano wait timer, deployment branch and tag policies e le custom deployment protection rules basate su GitHub App, per integrare sistemi esterni di change management.

Due avvertenze da amministratore. La disponibilità delle protection rules sui repository privati dipende dal piano, quindi va verificata prima di progettarci sopra un processo di rilascio. E i self-hosted runner non isolano i workflow fra loro: mettere segreti di produzione in un environment che gira su runner self-hosted condivisi vanifica buona parte del controllo.

Vault esterni e identità federata

L’alternativa strategica è non tenere il segreto su GitHub. Con OpenID Connect il workflow riceve un token JWT firmato, unico per ogni job, e lo scambia con il provider esterno per una credenziale a vita breve valida solo per la durata del job. Serve il permesso id-token: write nel workflow, e sul lato esterno si configura la fiducia: con HashiCorp Vault si abilita il metodo di autenticazione JWT indicando bound_issuer e oidc_discovery_url di GitHub, si crea una policy in sola lettura sui path necessari e un ruolo con bound_claims che vincola almeno il repository chiamante. Il recupero avviene poi con l’action ufficiale di Vault. Lo stesso schema vale per AWS, Azure, Google Cloud e per registry come JFrog.

Perché un amministratore preferisce questa strada: spariscono le credenziali di lunga durata duplicate in GitHub, l’autorizzazione torna nel sistema di identità del provider dove esistono già condizioni e audit, e la rotazione diventa automatica perché il token scade da solo. Attenzione al caso GHES: l’issuer OIDC è quello della propria istanza e non l’endpoint pubblico, quindi va sostituito nella configurazione di trust, e il dettaglio del comportamento va confermato sulla documentazione della versione in esercizio.