Salta al contenuto

Blog ·

Contro il port forwarding verso il pannello

Un agente in uscita richiede due regole in uscita e nessuna porta in entrata. Che cosa ottiene, quanto costa e i quattro problemi che sposta senza risolvere.

Di Provibr

Se una piattaforma di gestione deve far girare i suoi hypervisor, qualcosa deve attraversare il confine tra la loro rete e la sua. Le direzioni tra cui scegliere sono due, e la scelta determina quasi tutto il resto del modello di sicurezza.

La direzione tradizionale è verso l’interno. La piattaforma detiene le credenziali del suo cluster Proxmox, della sua macchina DirectAdmin e del suo pannello di gioco, e si connette a ciascuno di essi attraverso internet. Questo significa un’API che risponde a internet, una regola di firewall o una VPN, e un piano di controllo raggiungibile da chiunque riesca a trovarlo.

L’altra direzione è verso l’esterno. Lei fa girare un piccolo agente dentro la sua rete. Apre una connessione verso la piattaforma e la tiene aperta, i comandi scendono lungo quella connessione e i risultati risalgono. Dalla sua parte non è in ascolto nulla.

Questo testo parla di che cosa costa la seconda opzione e di che cosa dà, a partire dall’averne costruita una.

Due regole sul firewall, entrambe in uscita

L’intero requisito di rete di un agente in uscita sta in una tabella:

DirezioneRegolaQuando
In uscitaTCP 50051 al gateway della piattaformaUna volta, durante l’iscrizione
In uscitaTCP 50052 verso lo stesso hostIn continuazione, è la sessione
In entrataNessuna

Due porte anziché una sono una separazione voluta. L’iscrizione avviene prima che l’agente abbia un certificato client, quindi quella porta può offrire solo TLS lato server. La porta di sessione richiede il TLS mutuo e rifiuta qualsiasi cosa senza un certificato client valido. Un unico listener per entrambe significherebbe un listener in cui il certificato client è facoltativo e la decisione di autorizzazione è condizionata. Due lo rendono un confine strutturale: sulla 50052 non esiste alcun percorso di codice in cui un peer non autenticato superi l’handshake.

Se il suo filtraggio in uscita è severo, quelle due regole sono tutto ciò che le viene chiesto. Non c’è una regola in entrata da scrivere, nessuna voce di NAT, nessun concentratore VPN e nulla da rinnovare quando il suo indirizzo pubblico cambia.

Che cosa significa «in uscita» quando la rete è ostile

Una connessione TCP in uscita attraverso un NAT non è gratuita: lo stato che la fa funzionare vive nella tabella di qualcun altro, e quella tabella ha un timeout. Il NAT dell’operatore, un firewall aziendale e un load balancer cloud dimenticano tutti, in silenzio, una connessione inattiva, e la modalità di guasto è la peggiore possibile, perché entrambe le estremità continuano a credere che il socket stia bene.

A questo pensano due meccanismi, e servono entrambi.

Il primo è il keepalive di HTTP/2, a intervalli di 30 secondi con un timeout di 10 secondi. È il livello che batte i timeout dei NAT e dei load balancer, e l’unico che si accorge di una connessione TCP morta in silenzio sotto lo stream.

Il secondo è un heartbeat applicativo ogni 20 secondi, con un watchdog che chiude la sessione se entro 45 secondi non arriva alcuna conferma. Il keepalive dimostra che il socket è vivo; l’heartbeat dimostra che l’altra estremità sta ancora elaborando.

Quando la sessione finisce davvero, l’agente si riconnette con un backoff esponenziale e full jitter: l’attesa viene estratta in modo uniforme tra zero e il limite corrente, che raddoppia da un secondo fino a sessanta. Il jitter non è un ornamento. Senza di esso, ogni agente di ogni cliente si riconnette nello stesso istante dopo un riavvio del gateway, e il suo ripristino si trasforma in una calca autoinflitta.

Un dettaglio che salta fuori solo in produzione: il contatore del backoff si azzera soltanto dopo che una sessione è stata sana per un minuto intero. Senza quella regola, un agente con una connessione mezza rotta si connette, muore, riporta il contatore a un secondo e martella il gateway una volta al secondo per sempre.

Identità: un certificato, non una password

Un modello in entrata autentica la piattaforma verso il suo pannello, di solito con una chiave API che si incolla in un modulo. Un modello in uscita deve autenticare l’agente verso la piattaforma, e può farlo con qualcosa di meglio di un segreto condiviso.

L’agente si iscrive una sola volta con un token monouso: 36 byte casuali, 48 caratteri base64url, conservato lato server solo come hash SHA-256 e valido per 72 ore. Viene consumato dentro una transazione che ricontrolla le condizioni in base alle quali era stato selezionato, perché è tra la lettura e la scrittura che si infila un’iscrizione duplicata.

Ciò che torna indietro è un certificato client. Da quel momento la sessione è autenticata reciprocamente, e il gateway fissa il peer esatto: prende l’impronta SHA-256 del certificato presentato e cerca l’agente in base a quella impronta, prima che l’agente abbia detto chi crede di essere. L’identità è fissata dal certificato; il primo messaggio sullo stream può soltanto confermarla.

La rotazione segue l’orologio dell’agente stesso: i certificati vivono 90 giorni e il rinnovo parte a metà strada, al giorno 45. Restano così 45 giorni di tentativi prima che qualcosa sia a rischio, e il pannello avvisa una persona solo quando mancano 21 giorni, quindi il rinnovo deve essere fallito per tre settimane prima di disturbare qualcuno. Ogni rotazione genera una nuova coppia di chiavi: rinnovare sulla stessa chiave prolunga la vita di qualcosa che potrebbe essere già trapelato.

La revoca merita una frase a sé, perché la versione onesta è meno elegante del diagramma. Non c’è né CRL né OCSP. Revocare un agente è un aggiornamento del database, e una sessione connessa viene confrontata con esso una volta al minuto. Quel controllo è fail-closed, con un margine di tre fallimenti consecutivi, così un singhiozzo del database non può tenere in vita in silenzio un agente revocato, e il caso peggiore è limitato e calcolabile: sessanta secondi per tre.

Esattamente una volta, su una connessione che cadrà

Una volta accettato che il collegamento è uno stream di lunga durata su un percorso inaffidabile, la semantica di consegna smette di essere una preoccupazione teorica. La piattaforma consegnerà di nuovo un comando dopo una riconnessione, perché dal suo lato la differenza tra «l’agente non l’ha ricevuto» e «il risultato non è tornato indietro» è invisibile.

Quindi l’agente ricorda. I comandi completati vengono conservati su disco con i loro risultati, indicizzati per ID del comando, in una LRU limitata. La ripetizione di un comando concluso restituisce il risultato in cache e non esegue nulla. La ripetizione di un comando ancora in corso viene ignorata, perché la risposta è già in viaggio.

Su disco, per la precisione, ed è questo il punto: una cache in memoria è vuota esattamente quando la piattaforma ripropone tutto, cioè subito dopo un riavvio.

La direzione inversa è simmetrica. Gli eventi escono attraverso una outbox sequenziata; alla riconnessione la piattaforma dichiara il numero di sequenza che ha e l’agente riparte da lì. La outbox è limitata, e quando qualcosa ne cade fuori l’agente lo dichiara alla connessione successiva, annunciando la sequenza più bassa che ancora conserva meno uno, così la piattaforma smette di aspettare eventi che non esistono più.

C’è una crepa onesta nella promessa, e vale la pena dirla anziché nasconderla: se la scrittura su disco del record di deduplicazione fallisce, il risultato viene comunque inviato. L’alternativa sarebbe scartare il risultato di un comando completato per proteggere una garanzia, e nel caso peggiore una riconsegna successiva lo esegue una seconda volta.

Le credenziali restano dalla sua parte

Questa è la parte che cambia la forma di una violazione.

Nel modello in entrata la piattaforma conserva credenziali che danno il controllo della sua infrastruttura, e quel deposito è il bottino. Nel modello in uscita le credenziali dei suoi hypervisor e dei suoi pannelli vivono in un vault cifrato sul suo host, spinte lì una sola volta quando aggiunge un’integrazione.

Il nostro vault usa XChaCha20-Poly1305 con una chiave derivata tramite HKDF-SHA256 da un file di chiave locale e dal machine ID, con l’ID dell’agente mescolato nelle informazioni di derivazione. Una conseguenza che vale la pena esplicitare: copiare vault.enc su un’altra macchina le dà un file che non si decifrerà, perché la macchina a cui era legato è parte della chiave.

E il limite, che abbiamo scritto nel sorgente accanto all’implementazione, è che questo non protegge da root sull’host. Non ne ha bisogno. Il cliente possiede l’host e possiede le credenziali. Il vault protegge da un file trapelato, non dal proprietario.

La traccia di audit ha la stessa forma. Ogni comando è una riga che non viene mai eliminata in modo definitivo, e il contenuto di un push di credenziali viene conservato oscurato. Una tabella di audit che contiene i segreti che sta verificando è una seconda copia del problema.

Quattro cose che questo non risolve

Un testo di architettura che elenca solo vantaggi è marketing. Ecco ciò che si sposta invece di sparire.

Software sul suo host. Lo scambio è reale: non espone più una porta e, in cambio, fa girare un binario che va installato, aggiornato e ritenuto affidabile. Questo sposta la domanda da «chi può raggiungere il mio pannello» a «chi firma il codice che eseguo», che è una domanda migliore ma non gratuita.

Privilegi ampi. Un agente che gestisce un hypervisor detiene credenziali che gestiscono un hypervisor. La direzione della connessione non cambia nulla di ciò che quelle credenziali possono fare, solo dove vivono e chi altro può raggiungerle.

Anche una connessione in uscita è un’uscita. Una connessione che lascia la sua rete può portare dati fuori da essa. La destinazione è fissa, il trasporto è autenticato reciprocamente e il protocollo è ristretto, ma «due regole in uscita» è un’affermazione più modesta di «non esce nulla».

Risposte in tempo reale. Questa si vede subito nell’API. Poiché la piattaforma non può chiamare l’agente, una scrittura non può rispondere con il proprio risultato: la risposta corretta a «crea questa macchina» è una presa in carico e qualcosa da interrogare. Le letture non ne risentono, perché vengono dal database della piattaforma stessa. Chiunque progetti un’API sopra un agente in uscita deve accettare quell’asimmetria invece di nasconderla.

C’è anche esattamente un caso in cui l’agente si mette in ascolto, e fingere il contrario sarebbe disonesto. Installare bare metal via rete significa rispondere al DHCP e servire file, e non si può fare da un socket in uscita. L’agente quindi mette in ascolto quei servizi, su una sola interfaccia nominata, solo per le macchine che hanno un lavoro in corso, e solo finché c’è lavoro da fare. «Abilitato» significa «consentito qui», non «in ascolto adesso».

Quando la direzione in entrata resta giusta

Se ha un solo hypervisor e lo gestisce da sé, niente di tutto questo la riguarda: un agente non le dà nulla in più rispetto a una regola di firewall e a una password robusta.

Il discorso si fa più netto con la distanza e con il numero. Più sedi, reti che non le appartengono, indirizzi che cambiano, clienti i cui firewall non può modificare, e un piano di controllo che preferirebbe non avere affatto su internet pubblico. A quel punto l’agente non è un componente in più; è il componente che le permette di cancellare tutto il resto.

È così che funziona Provibr: una connessione in uscita per agente, nessuna porta in entrata, e credenziali che non lasciano mai la rete del cliente. La documentazione dell’agente copre installazione, dimensionamento e revoca, per iniziare accompagna la prima connessione dall’inizio alla fine, e l’API documenta la forma «prendi in carico e interroga» che il progetto in uscita impone.

Inizi con un host e un agente

Crei un account, installi l’agente con un solo comando e colleghi il suo primo host Proxmox. Il periodo di prova dura 14 giorni e non chiede alcun dato di pagamento.