Il confine fra runner ospitati e self-hosted

Un runner è la macchina che preleva un job dalla coda, prepara l’ambiente, esegue gli step e rimanda i log a GitHub. La scelta del runner non è un dettaglio operativo: decide isolamento, chi paga la manutenzione e quanto vale la superficie d’attacco di un workflow.

Con i GitHub-hosted runners la macchina è fornita da GitHub. La documentazione è esplicita: ogni runner ospitato è una nuova macchina virtuale, con l’eccezione dei runner a singola CPU, che girano in un container su una VM condivisa. Le pagine sulla sicurezza rincarano definendoli macchine virtuali effimere, pulite e isolate, dove non esiste modo di lasciare una compromissione persistente. Manutenzione, patch e aggiornamenti del sistema operativo sono a carico di GitHub.

I self-hosted runners ribaltano il quadro. Ti danno controllo su hardware, sistema operativo e strumenti installati, e li registri a livello di repository, organization o enterprise. In cambio, aggiornare l’OS e tutto il software è responsabilità tua, e soprattutto non c’è nessuna garanzia di ambiente pulito fra un job e il successivo: quello che un job scrive su disco resta lì.

Da qui discendono i rischi documentati. GitHub scrive che i self-hosted runner non andrebbero quasi mai usati con repository pubblici, perché chiunque possa aprire una pull request da un fork riesce a compromettere l’ambiente, arrivando ai secret e al GITHUB_TOKEN. La persistenza aggrava tutto: un job che passa un secret come argomento da riga di comando lo espone a un altro job sulla stessa macchina, che può leggerlo con ps x -w. Le contromisure raccomandate sono runner ephemeral o just-in-time (JIT), che eseguono un solo job e poi si de-registrano, e i runner groups, descritti come un confine di sicurezza: ogni runner appartiene a un solo gruppo, i nuovi finiscono nel gruppo default, e il gruppo stabilisce quali organization e repository possono usarlo. Attenzione a non confondere i gruppi con i self-hosted: anche i larger runners — ospitati da GitHub, con più risorse, indirizzi IP statici, immagini personalizzate e autoscaling — si governano tramite runner groups.

Come si punta a un runner nel workflow

La chiave è runs-on. Un’etichetta singola basta per gli ospitati; un array vale come AND logico, cioè il job va solo su runner che hanno tutte le etichette elencate, con self-hosted per primo per convenzione. Con i gruppi si usa la forma a oggetto.

jobs:
  build:
    runs-on: [self-hosted, linux, x64, gpu]
  package:
    runs-on:
      group: build-runners
      labels: linux-large

Le etichette personalizzate si assegnano dall’interfaccia in Settings > Actions > Runners oppure al momento della registrazione con --labels. Nota che GitHub accetta le etichette di sistema così come le dichiari e non verifica che la macchina usi davvero quel sistema operativo o quell’architettura: un runner etichettato male è un job che fallisce in modo confuso.

Che software ci trova sopra

Le immagini dei runner ospitati contengono un corredo di strumenti già installati, aggiornato con cadenza settimanale. La fonte autoritativa non è una tabella nella documentazione, ma il repository actions/runner-images, che ospita anche gli annunci sugli aggiornamenti maggiori del software. Il secondo canale è il run stesso: nel log, espandendo la sezione Set up job, trovi la voce Runner Image con il collegamento Included Software relativo alla macchina esatta che ha eseguito quel job. Per Windows e Ubuntu sono disponibili anche gli SBOM delle immagini. Sui runner ospitati hai privilegi elevati: sudo senza password su Linux e macOS, account amministratore con UAC disabilitato su Windows.

È esattamente qui che l’esame insiste: sapere dove si legge l’elenco del software preinstallato, non impararlo a memoria. Un elenco memorizzato invecchia in settimane, e le etichette -latest si spostano su nuove versioni del sistema operativo, quindi ancorare una pipeline fragile a ubuntu-latest è una scelta, non un default innocuo.

Quando serve qualcosa che non c’è, si installa a runtime. La via preferibile sono le action setup-* (setup-node, setup-python, setup-java), che fissano la versione e spesso espongono un input cache per riusare le dipendenze fra run. In alternativa un normale step run con il package manager della piattaforma: apt-get su Ubuntu — sempre preceduto da sudo apt-get update, perché l’indice preinstallato può essere obsoleto e far fallire l’installazione — brew su macOS, choco su Windows. Sulla cache ricorda il modello di accesso: è condivisa in base al branch o tag del run, i trigger a bassa fiducia come le pull request dai fork hanno accesso in sola lettura allo scope del branch di default, e non ci si mettono mai dati sensibili, perché workflow non fidati possono leggerne il contenuto.

Monitorare e diagnosticare

Per i self-hosted lo stato si legge in Settings > Actions > Runners, con tre valori: Idle (connesso e in attesa), Active (sta eseguendo un job), Offline (macchina spenta, applicazione ferma o problemi di comunicazione). Sulla macchina, i log stanno nella cartella _diag della directory di installazione: i file Runner_ registrano l’avvio dell’applicazione, i file Worker_ il dettaglio di ogni singolo job, i file SelfUpdate gli aggiornamenti automatici. Il servizio si ispeziona con journalctl su Linux (unità nella forma actions.runner.<org>-<repo>.<nome>.service), con launchctl su macOS, con i cmdlet dei servizi e l’Application event log su Windows. Se usi runner effimeri, la documentazione chiede di inoltrare quei log a uno storage esterno: la macchina sparisce dopo il job e con lei le prove.

Ultimo dettaglio che ricorre nelle domande: il runner comunica in uscita su HTTPS porta 443 verso i domini GitHub, non richiede porte in ingresso, e si aggiorna da solo salvo disattivazione esplicita con --disableupdate. Chi risponde “bisogna aprire una porta in ingresso sul firewall” sbaglia.