Tutti gli articoli
23 ott 2026 / 15 min di lettura

Arrivo tardi, ma porto il bastion

NIS2 chiede accessi nominali e tracciati. Ho provato a farlo con pezzi open source: Entra ID, Dex, Warpgate e OpenBao, più un audit che risale da ogni comando alla persona. Un proof of concept di un giorno, provato in casa.

# sicurezza# nis2# devops# architettura
Un piccolo forte di pietra su una collina di notte, con il portone illuminato di verde: da lì partono linee luminose verso una fila di server sulle colline e una linea dritta verso un sigillo con una stella nel cielo

Arrivo tardi alla festa degli accessi regolati. Non perché la cosa non mi interessasse: nell'ambito in cui lavoravo prima non era mai servita. Bastavano una VPN, una chiave SSH e la fiducia nei colleghi.

Poi è arrivata NIS2, e la fiducia va messa per iscritto.

Da dove parte

In Italia NIS2 è il D.Lgs. 138/2024, in vigore da ottobre 2024. Le misure concrete le ha scritte ACN nella determinazione 379907/2025, e vanno adottate entro 18 mesi dalla comunicazione di inserimento nell'elenco NIS, per chi ci è entrato nel 2025 (per chi entra nel 2026 la scadenza è il 31 luglio 2027). Le prime comunicazioni sono partite ad aprile 2025: per molti la scadenza è adesso, ottobre 2026. È un obbligo, punto.

Dentro c'è tutto un discorso di tracciatura degli accessi e di sicurezza. Al lavoro avevamo alcuni sistemi da aggiornare e da mettere in sicurezza, anche dove non era strettamente richiesto. E soprattutto ci serviva una cosa semplice e cloud native.

L'obiettivo:

  • una soluzione enterprise che si attacchi a Entra ID, perché al lavoro usiamo quello;
  • che si gestisca in modo semplice;
  • che si possa delegare a un altro team.

Di solito questa roba si fa con un gestore di accessi commerciale: client desktop, spesso solo per Windows, licenze per utente, e un altro team che lo amministra. Funziona, ma è scomodo. Volevo vedere se si poteva ottenere lo stesso risultato con pezzi open source, che girano in container e si configurano dichiarativamente con dell'infrastructure as code.

Cosa chiede la norma

Le misure sono negli allegati della determinazione: l'Allegato 1 per i soggetti importanti, l'Allegato 2 per gli essenziali. Quelle che riguardano gli accessi:

MisuraCosa chiede, in breve
PR.AA-01Utenze censite e individuali, «fatte salve motivate e documentate ragioni tecniche»; verifica periodica di utenze e permessi
PR.AA-03Autenticazione multifattore sui sistemi rilevanti
PR.AA-05Minimo privilegio; utenze amministrative distinte da quelle normali
PR.PS-04«Tutti gli accessi eseguiti da remoto e quelli effettuati con utenze con privilegi amministrativi sono registrati», con log conservati in modo sicuro e possibilmente centralizzato
PR.IR-01Accesso remoto documentato, con l'elenco dei sistemi raggiungibili
DE.CM-01Punti 5 e 6, solo per gli essenziali: monitorare accessi remoti, «eventi amministrativi di rilievo» e accessi con abuso dei privilegi

Già dal 2008, del resto, il Garante privacy chiede di registrare gli accessi logici degli amministratori di sistema, con log inalterabili e conservati almeno sei mesi.

Il problema non sono le chiavi

Le chiavi SSH statiche non hanno niente che non va, se il modo per arrivarci è sicuro e sta dietro una serie di livelli di sicurezza. Il problema è che NIS2 e ACN vogliono sapere chi è entrato.

Su un progetto gestito con l'infrastructure as code, chi ci lavora ha più o meno gli stessi permessi. Spesso si entra con un utente sudoer senza password, che può diventare root. Per avere utenze nominali vere, ogni persona dovrebbe essere censita su ogni macchina, con i suoi permessi e i suoi gruppi, a livello proprio basso. Quindi LDAP integrato con SSH tramite SSSD, il bind, l'utente creato al primo login, la password in cache. Una madonna di casino 😅, che diventa ingestibile quando le macchine sono centinaia.

Molto meglio avere qualcosa che ti fa entrare con la cosa giusta, nel posto giusto, senza censire le utenze su ogni server. Ci si preoccupa di far arrivare la gente a monte nel modo giusto, e le macchine restano come sono: una golden image con tutto già impostato, la tiri su ed è pronta. Niente preparazioni, niente gruppi da tenere allineati.

Resta un dubbio, e lo dico apertamente. Se sul server entri con un utente condiviso ma a monte sai sempre chi sei, ed è tutto tracciato fino al singolo comando, per ACN va bene? Il testo di PR.AA-01 dice che le utenze sono individuali «fatte salve motivate e documentate ragioni tecniche». Quindi, secondo me, sì: ma la ragione va scritta nero su bianco, e deve tornare con la valutazione del rischio.

I pezzi

Lo schema separa tre domande che una chiave statica mescola: chi sei, dove puoi andare, con quale credenziale entri.

PezzoRisponde aCome
Entra IDChi sei?Login con MFA, gruppi di sicurezza
DexSei tra quelli ammessi?Ponte OIDC davanti a Entra, filtro sui gruppi
WarpgateDove puoi andare?Bastion: ruoli presi dai gruppi, sessioni registrate
OpenBaoCon quale credenziale entri?CA SSH: certificati validi 5 minuti

Dex. Non è un segreto: nella SIGHUP Distribution, la distribuzione Kubernetes open source di cui sono contributor, ci sono Dex e Pomerium. Dex lo usiamo per attaccarci a vari endpoint SSO e avere l'SSO nel cluster. È un identity provider proxy: per le applicazioni è lui l'identity provider, per Entra è un'applicazione. Pomerium ci fa l'identity aware proxy HTTPS: arrivi su Pomerium, ti manda su Dex, ti logghi, torni indietro e usi l'applicativo. In più Dex mette nel token i nomi dei gruppi Entra (infra-admin, non un GUID) e lascia passare solo chi sta nei gruppi giusti, in un posto solo. Se un giorno cambia l'identity provider, cambia un connettore e non quattro configurazioni.

yaml
connectors:
  - type: microsoft
    id: entra
    name: Entra ID
    config:
      tenant: <id del tenant>
      clientID: <id dell'app registrata in Entra>
      clientSecret: $DEX_ENTRA_CLIENT_SECRET
      redirectURI: https://<dex>/callback
      groups: [infra-admin, infra-ops]
      onlySecurityGroups: true
  • type: microsoft: il connettore di Dex per Entra ID;
  • tenant, clientID, clientSecret: l'app registrata in Entra per Dex. Il segreto arriva da una variabile d'ambiente, non sta nel file;
  • groups: chi non è in uno di questi gruppi non fa login, per nessuna applicazione;
  • onlySecurityGroups: nel token finiscono solo i gruppi di sicurezza, non quelli di Microsoft 365.

Warpgate. Per il web c'è già Pomerium. Quello che mi serve regolare sono l'accesso nominale in SSH, sia per la manutenzione sia per lanciare Ansible (che di solito parte da altri strumenti), e l'accesso nominale ai cluster Kubernetes con kubectl. Warpgate è molto semplice, molto dritto, e parla tutti i protocolli che mi servono: SSH, HTTP, Kubernetes, anche MySQL e PostgreSQL. Non c'è un client di Warpgate da installare: si usano ssh e kubectl normali, più il plugin kubelogin per il login di kubectl. In più registra tutte le sessioni.

OpenBao. È il fork open source di Vault, sotto la Linux Foundation. Qui fa una cosa sola, piccolina: firma certificati SSH brevi. Non so ancora quanto sia comodo gestire un OpenBao di produzione. Però è una tecnologia community, conosciuta, che tanti sanno usare, e qualcuno la dovrà gestire comunque, probabilmente il mio team. Si può anche avere un OpenBao dedicato solo a questo sistema, che sta lì tranquillo e fa una cosa sola.

Tutto insieme, compreso l'audit di cui parlo più avanti:

mermaid
flowchart TD
    me["person<br/>ssh · kubectl · browser"] -->|1. access| wg["Warpgate<br/>bastion"]
    wg -->|2. login| dex["Dex<br/>OIDC bridge"]
    dex -->|3. MFA and groups| entra["Entra ID"]
    wg -->|4. sign a 5-minute certificate| bao["OpenBao<br/>SSH CA"]
    wg -->|5. SSH with the certificate| srv["servers<br/>trust the CA"]
    wg -->|kubectl| k8s["Kubernetes cluster"]
    srv -->|auditd: who ran what| fb["Fluent Bit<br/>on every node"]
    wg -->|sessions and certificates| loki
    fb --> loki["Loki"]
    loki --> grafana["Grafana<br/>audit by person"]

Come funziona un accesso

  1. Apri Warpgate, che ti manda a Dex.
  2. Dex ti manda su Entra: login con MFA.
  3. Dex controlla che tu sia in un gruppo ammesso (infra-admin o infra-ops) e torna a Warpgate, che trasforma i gruppi in ruoli.
  4. Warpgate chiede a OpenBao un certificato per l'utente del server, valido 5 minuti, con dentro il tuo nome.
  5. Warpgate apre l'SSH verso il server, che accetta il certificato perché si fida della CA.

Lo stesso giro, visto da una sessione SSH da terminale:

mermaid
sequenceDiagram
    autonumber
    actor P as Person
    participant W as Warpgate
    participant D as Dex + Entra ID
    participant B as OpenBao
    participant S as Server
    participant A as Audit<br/>Fluent Bit + Loki
    P->>W: ssh -p 2222 to the target
    W-->>P: link to approve in the browser
    P->>D: login with MFA, unless already done
    D-->>W: identity and groups
    W->>W: groups become roles, is the target allowed?
    W->>B: sign for the target user (AppRole)
    B-->>W: 5-minute certificate, key id with the person
    W->>S: SSH with the certificate
    S->>S: sshd checks the CA and logs the key id
    S-->>W: session open
    W-->>P: terminal (session recorded)
    P->>S: commands, through Warpgate
    S->>A: auditd logs every command with its session
    A->>A: command tied to the person

Sul server non c'è nessuna chiave personale. C'è solo una riga di configurazione:

bash
# /etc/ssh/sshd_config.d/50-openbao-ca.conf
TrustedUserCAKeys /etc/ssh/openbao-ca.pub

TrustedUserCAKeys dice a sshd: accetta qualunque certificato firmato dalla CA la cui chiave pubblica sta in quel file. La chiave pubblica della CA la dà OpenBao, ed è uguale per tutti i server: è l'unica cosa da mettere nella golden image.

E nel log di sshd resta scritto chi era:

text
Accepted publickey for deploy ... ID warpgate:<persona>:<id sessione>

deploy è l'utente con cui si entra sul server, magari lo stesso per tutti. ID è il key id del certificato: Warpgate ci scrive dentro chi sei e in quale sessione, OpenBao lo firma, e sshd lo copia nel log. È il filo che lega il server alla persona.

Togliere l'accesso a qualcuno non richiede di toccare i server: lo si toglie dal gruppo in Entra, e al login successivo non entra più da nessuna parte.

Da terminale si entra con ssh -p 2222 '<utente>:<target>@<bastion>'. Al primo accesso Warpgate stampa un link da approvare nel browser, dove sei già loggato con Entra. Un dettaglio: Warpgate usa : e # per separare utente e target, quindi i nomi utente di Entra devono funzionare da riga di comando. Di solito è così, tranne per gli account ospite, che hanno #EXT# nel nome.

Il momento wow è stato proprio la prima connessione SSH tramite Warpgate: voleva dire che tutto il giro e tutta la baracca funzionavano. Poi in Warpgate ho aperto la registrazione della sessione, che si riguarda come un video, ed era bellissimo. 🤩

kubectl dal bastion

Il secondo momento wow è stato kubectl. Per kubectl Warpgate è un API server: è Warpgate che ti dà il kubeconfig, e il login apre il browser su Entra, come per il web (con il plugin kubelogin). Warpgate parla poi con il cluster con una sua credenziale, e le persone non ricevono mai credenziali del cluster.

C'è un limite da sapere: per il cluster tutte le azioni sono del service account di Warpgate. La persona resta nei log di Warpgate, che registrano anche exec, attach e port-forward. Per dare permessi diversi a gruppi diversi servono più target sullo stesso cluster, ognuno con il suo service account.

Chi era davvero quel root

Warpgate sa chi è entrato, dove e quando. Ma non vede cosa succede dentro un tunnel: VS Code remoto, un port forward verso un database. Per sapere sempre chi ha fatto cosa serve l'audit sui server.

Il pezzo che lo rende possibile è vecchio e già presente su ogni Linux: auditd. Al login PAM assegna al processo l'utente di login (auid) e un numero di sessione (ses). Tutti i processi figli li ereditano, e non cambiano con sudo: chi diventa root resta, per l'audit, la persona che era. Due regole, una per i programmi a 64 bit e una per quelli a 32, registrano ogni comando lanciato da chi ha fatto login:

text
-a always,exit -F arch=b64 -S execve,execveat -F auid!=4294967295 -k commands
-a always,exit -F arch=b32 -S execve,execveat -F auid!=4294967295 -k commands

Pezzo per pezzo:

  • -a always,exit: valuta la regola alla fine di ogni chiamata al kernel, e se corrisponde scrivi sempre l'evento;
  • -S execve,execveat: solo le chiamate che avviano un programma, cioè ogni comando lanciato (ls, htop, sudo…);
  • -F auid!=4294967295: solo i processi con dietro una persona che ha fatto login. 4294967295 è -1, «nessun login»: demoni, timer e container restano fuori, e con loro quasi tutto il rumore;
  • -F arch=b64 e arch=b32: i numeri delle chiamate al kernel cambiano tra programmi a 64 e a 32 bit, quindi serve una riga per ciascuno;
  • -k commands: un'etichetta, per ritrovare questi eventi con ausearch -k commands.

In una frase: registra ogni programma avviato da chi ha fatto login, e nient'altro.

Da lì la catena si risale tutta, senza aprire Warpgate:

  1. il comando, con il suo ses;
  2. il login che ha aperto quella sessione, con il pid di sshd;
  3. la riga di sshd di quel pid, con il key id warpgate:<persona>:<sessione>;
  4. la sessione in Warpgate, con la persona e la registrazione;
  5. il login su Entra, con l'MFA.

Sul server sono tre comandi:

bash
sudo ausearch -i -k commands -c htop          # il comando, con il suo ses=
sudo ausearch -i -m USER_START --session 10   # il login di quella sessione, con il pid di sshd
journalctl -u ssh | grep 'ID warpgate:'       # chi era, in Warpgate
  • il primo cerca gli eventi con l'etichetta commands per il programma htop; -i traduce numeri e utenti in nomi leggibili. Nel risultato c'è ses=10;
  • il secondo cerca l'apertura della sessione 10 (USER_START), che dice quale processo sshd l'ha aperta;
  • il terzo cerca nel log di sshd la riga di quel processo, con il key id warpgate:<persona>:<sessione>. Su alcune distribuzioni il servizio si chiama sshd invece di ssh.

Se lanci ausearch via ssh host '…' senza terminale, aggiungi --input-logs: altrimenti legge da stdin e risponde <no matches>.

La prova l'ho fatta così: senza guardare Warpgate, solo dai log di un server, che comando ho lanciato e chi ero? Risposta: htop, e il mio nome.

Il terzo momento wow è stato mettere tutto in un posto solo. Su ogni nodo gira un agente Fluent Bit, che legge auditd e i login di sshd. Un piccolo script Lua fa il collegamento alla persona già sul nodo, prima di spedire. Al centro ci sono Loki e Grafana, con una dashboard «Audit by person»: scegli una persona e vedi tutte le sue briciole, cioè login, certificati, sessioni e comandi su tutti i nodi. I log lasciano subito il server: chi diventa root lì può ancora spegnere l'audit, ma non cancellare quello che è già partito.

Ho scelto Loki invece di un SIEM vero come Wazuh perché pesa pochissimo. Se al lavoro c'è già un SIEM, gli agenti possono scrivere direttamente lì: il collegamento alla persona fatto sul nodo funziona con qualunque destinazione.

Privacy e Statuto dei lavoratori

C'è un pezzo che non si risolve con la tecnologia. Registrare chi accede da remoto e con privilegi amministrativi è un obbligo per NIS2, e il Garante chiede già da anni i log di accesso degli amministratori di sistema. Per i soggetti essenziali ci sono anche i punti 5 e 6 della misura DE.CM-01, che chiedono di monitorare gli eventi amministrativi e gli accessi con abuso dei privilegi: auditd e la dashboard per persona aiutano a coprirli. La registrazione delle sessioni, invece, va oltre quello che la norma chiede. È comoda per le indagini, ma resta un di più.

Tutto quello che registra l'attività delle persone, però, è uno strumento da cui deriva anche la possibilità di un controllo a distanza (art. 4 dello Statuto dei lavoratori). L'eccezione per gli strumenti di lavoro il Garante la interpreta in modo stretto, e un obbligo normativo, secondo l'Ispettorato del lavoro in casi simili, non basta a uscirne. Quindi serve un accordo sindacale o, se manca, l'autorizzazione dell'Ispettorato. Poi servono:

  • un'informativa;
  • una valutazione d'impatto (DPIA);
  • una retention motivata.

E io aggiungerei:

  • una finalità scritta: sicurezza, mai produttività;
  • un accesso alle registrazioni solo in caso di incidente.

Quindi al lavoro si accende dopo aver parlato con DPO, HR e rappresentanze. Non è un parere legale: è quello che ho capito leggendo, e va verificato con chi lo fa di mestiere.

Dal proof of concept alla produzione

L'ho provato in casa, su una macchina sola. È un proof of concept.

In produzione farei un cluster Kubernetes dedicato solo a questo. Dentro ci sarebbero Warpgate bilanciato, un cluster Postgres, OpenBao in alta affidabilità con lo sblocco automatico, e i log su uno storage che non si possa cancellare. L'alternativa sono macchine dedicate con Docker Compose, ma propendo per il cluster.

E quel cluster non lo gestirei entrandoci da Warpgate. Si potrebbe, ma servono anche credenziali super admin custodite da qualche parte, da usare solo in emergenza. Se si rompe quello si rompe tutto, e ci vuole il famoso bottone rosso. Il bastion e la CA sono i gioielli della corona: chi li controlla controlla l'accesso a tutto, quindi vanno tenuti lontani dai server che proteggono.

Fatto con l'AI

Tutto il proof of concept l'ho fatto supportato dall'AI, e non è un problema. Anzi, senza non ci sarei riuscito così in fretta: tutto lo studio mi ha preso un giorno. Come ho già scritto in Chi preme il bottone, è uno strumento, e non c'è motivo di farne un tabù.

Uno stregone non è mai in ritardo

«Uno stregone non è mai in ritardo, Frodo Baggins. Né in anticipo. Arriva precisamente quando intende farlo.»

Gandalf il Bianco emerge dalla luce a Fangorn

Sono arrivato tardi alla festa, ma almeno sono arrivato con qualcosa in mano: un'architettura, tutta open source, che fa entrare la persona giusta nel posto giusto, e che si ricorda chi era. 🙂