Salta al contenuto

Blog ·

Un segreto che viaggia una volta sola

Un controllo di monitoraggio ha bisogno di una password dall’altra parte. Ecco come raggiunge una sonda, per un tentativo, senza finire mai in un registro.

Di Provibr

Alcuni controlli di monitoraggio hanno bisogno di credenziali. Non si può chiedere a un server PostgreSQL se sta bene senza autenticarsi, e un’istanza Redis con una password non risponde nulla di utile finché non gliela si è inviata. Una regola di monitoraggio deve quindi portare un segreto da noi fino alla sonda o all’agente che per caso prenderà il tentativo successivo.

La versione a buon mercato di tutto ciò è una colonna. Mettere la password nella regola, leggerla al momento dell’invio, spedirla. Quella versione è sbagliata in un modo che diventa visibile solo tredici mesi dopo, ed ecco la forma che abbiamo costruito al suo posto.

Le due strade che già esistevano, e perché nessuna va bene

Un comando di monitoraggio raggiunge una sorgente attraverso due punti, ed entrambi sono inadatti.

La tabella delle revisioni della regola è immutabile per progetto, il che è esattamente giusto per una revisione ed esattamente sbagliato per una credenziale: nessuno può ruotare una password che abita in una riga che non può cambiare senza produrre una nuova revisione di ogni regola che la usava.

La riga del comando è peggio, perché è una riga di controllo e attraversa il bus dei messaggi mentre esce. Le nostre stesse note di progetto dicono da un anno che una riga di controllo di un comando contiene nomi di campo e mai valori. Una password che ci finisce una volta ci resta per l’intero periodo di conservazione, leggibile da chiunque abbia accesso alla banca dati e copiata nel salvataggio girato quella notte.

Esiste inoltre un canale laterale transitorio proprio per questo genere di cose, usato quando qualcuno ordina una macchina e una password di amministrazione deve raggiungere l’agente senza essere memorizzata. Nemmeno quello va bene qui. Quel canale esiste per segreti che il creatore di un comando conosce già, e qui il creatore è un pianificatore che gira minuti prima e non sa ancora quale sorgente prenderà il lavoro.

I quattro messaggi di protocollo aggiunti in questa tornata non hanno perciò né un campo utente né un campo password. Non è che quei campi siano inutilizzati: non esistono.

Sigillato nell’istante dell’invio

La busta viene creata nel processo di sessione, subito prima che il messaggio venga costruito, una volta per tentativo. Non è ordine formale. È l’unico punto del sistema in cui si conoscono nello stesso momento le quattro cose che deve legare: quale sorgente sta all’altro capo di questa sessione, quale tentativo è questo, a quale revisione della regola appartiene e quando scade.

La conseguenza è che non può stare in nessun altro posto, e questo si verifica invece di essere dichiarato. Un test pianta una password nota, percorre il vero cammino e la cerca nel carico del comando, nelle righe degli eventi, nella configurazione di controllo salvata, nel bus dei messaggi, nei byte grezzi del protocollo sia in UTF-8 sia in base64, e nel registro della passerella. Pretende zero occorrenze in tutti e sei. Un elenco di affermazioni su dove un segreto non arriva non vale niente; una ricerca vale qualcosa.

Il testo in chiaro esiste per la durata di un’espressione. Una funzione lo produce, la successiva lo consuma, e poi non c’è più alcun riferimento. JavaScript non sa azzerare un buffer, quindi l’unica scelta è non conservarne nessuno. Dall’altra parte, in Rust, finisce in un tipo che si azzera da sé quando viene rilasciato.

La costruzione

Una coppia di chiavi X25519 effimera per tentativo, un Diffie-Hellman contro la chiave pubblica di esattamente una sorgente, HKDF-SHA256 per derivare la chiave di cifratura e AES-256-GCM sul testo in chiaro. Sul filo passano la chiave pubblica effimera più un numero usato una volta sola, il cifrato e l’etichetta.

Le due chiavi pubbliche entrano nel sale della HKDF, così che la chiave derivata sia legata a quella coppia e non a un segreto condiviso che in linea di principio potrebbe nascere da un’altra coppia. La stessa idea del piano di chiavi di un comune schema ibrido, con la semplificazione qui onesta: c’è una chiave effimera fresca per tentativo, quindi nessuna chiave viene usata due volte.

Quattro campi entrano nei dati autenticati aggiuntivi:

CampoChe cosa impedisce
Identificatore del tentativoChe la stessa sorgente riusi una busta per una misura successiva
Identificatore di revisioneChe una busta venga incollata sotto un altro controllo
ScadenzaChe una busta presa da una cattura o da un dump serva più tardi
FinalitàChe una credenziale PostgreSQL venga data in pasto a un controllo Redis

L’identità della sorgente non figura in quell’elenco perché non serve: il sigillo è fatto verso una sola chiave pubblica, quindi un’altra sorgente non lo apre affatto.

Prefissi di lunghezza, perché un separatore è ambiguo

I dati aggiuntivi sono costruiti con la lunghezza di ogni campo davanti anziché con un separatore fra i campi. Sembra pignoleria finché non si scrive l’alternativa.

Con a:b come forma, la coppia ("x", "y:z") e la coppia ("x:y", "z") producono byte identici. Il legame che questi dati devono stabilire non distingue più due situazioni diverse, e chi controlla parte di un campo può spostare il confine. Quattro campi con quattro byte di lunghezza davanti non hanno quel problema. Entrambi i lati li costruiscono byte per byte allo stesso modo, con un vettore di prova condiviso, così una divergenza è un test rosso e non una busta che nessuno riesce più ad aprire.

Perché non il cifrario che già usiamo

La cassaforte delle credenziali dell’agente usa XChaCha20-Poly1305, e la stessa costruzione qui darebbe una sola risposta alla domanda su come cifriamo un segreto invece di due. Abbiamo misurato, e non si può:

text
node v24.20.0   crypto.getCiphers() contiene "chacha20-poly1305"
bun  1.4.0      crypto.getCiphers() restituisce 28 nomi, nessun chacha
                createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"

La produzione gira su Node, quindi lì funzionerebbe. Ma il banco di prova di questo deposito è bun, e una cifratura autenticata che il banco non sa eseguire è una cifratura il cui andata e ritorno non viene mai dimostrato. L’unico controllo sulla busta vivrebbe allora in un test saltato, ed è esattamente il sapore di misura verde a vuoto che mettiamo molto impegno a evitare.

L’alternativa era scrivere a mano il pezzo mancante. È stato fatto davvero, funzionava contro il vettore di prova pubblicato, e poi è stato tolto. Il codice crittografico che esiste solo per aggirare un banco di prova non appartiene a un deposito di prodotto.

Quindi AES-256-GCM, la stessa cifratura autenticata che già protegge questo segreto a riposo, presente in entrambi gli ambienti di esecuzione e già nell’albero delle dipendenze dell’agente. Il numero usato una volta sola da 96 bit per cui di solito si sceglie XChaCha non è un problema qui, perché la ragione d’essere di XChaCha è poter estrarre quei numeri a caso sotto una chiave di lunga vita. Non c’è alcuna chiave di lunga vita. Ogni busta ne ha una effimera fresca, quindi ogni chiave serve esattamente una volta e una ripetizione non sarebbe un secondo messaggio sotto la stessa chiave.

Dove la sorgente tiene la sua metà

La chiave privata sta accanto all’identità dell’agente o della sonda, in un file di trentadue byte grezzi con modo 0600, accanto alla chiave privata del certificato client. Non nella cassaforte delle credenziali, e le tre ragioni meritano di essere scritte perché sono state la decisione vera.

La revoca la cancella gratis: la revoca elimina già quella cartella, e una chiave riposta altrove avrebbe bisogno di una propria regola di pulizia, che la prima tornata a dimenticarla lascerebbe su un host revocato. Appartiene a una sola iscrizione, e la piattaforma conserva la metà pubblica accanto al certificato, quindi una nuova iscrizione dovrebbe produrre una nuova coppia, cosa che fa perché la cartella è allora vuota. E la cassaforte non è sempre disponibile: si apre con materiale ricavato dal file di licenza, e una sonda di monitoraggio non ha licenza e dunque nemmeno cassaforte. Metterci la chiave imporrebbe un secondo meccanismo per le sonde, e allora ci sarebbero due risposte a una domanda.

Non viene neppure tenuta in memoria, ed è voluto, per quanto sia allettante per una sonda che non archivia nulla. La piattaforma conserva la metà pubblica, quindi dopo un riavvio non corrisponderebbe più e ogni busta resterebbe chiusa finché un operatore non iscrivesse di nuovo la sonda. Un punto di monitoraggio che dopo un riavvio smette in silenzio di fare controlli autenticati è esattamente il tipo di guasto che nessuno nota.

Mai un ripiego

Se una sorgente non ha chiave, il controllo non parte. Segnala un errore di configurazione che indica l’intermediario come non disponibile, e questo è tutto il comportamento. Non c’è ripiego in chiaro e, cosa più importante, non c’è uno stato di guasto ingannevole per il bersaglio, perché un controllo che non si può misurare non deve mai assomigliare a un servizio rotto. Quello è un allarme che manda qualcuno alla macchina sbagliata alle tre di notte.

Cosa metteremmo per iscritto

  1. Una riga di controllo non è un canale di consegna. Ciò che vi si scrive resta per tutto il periodo di conservazione, in ogni salvataggio.
  2. Non aggiungete il campo. Un messaggio di protocollo senza campo password non ne può far uscire una da un cammino di codice che qualcuno dimentica di rileggere.
  3. Sigillate dove i legami sono noti. Se chi crea un incarico non sa ancora chi lo eseguirà, il segreto non può nascere con l’incarico.
  4. Dimostrate l’assenza cercando, non affermando. Piantate un valore e dategli la caccia in ogni serializzazione, base64 compresa.
  5. Mettete prefissi di lunghezza sui vostri dati autenticati. Un separatore fa produrre gli stessi byte a due coppie diverse.
  6. Scegliete la primitiva che il vostro banco di prova sa eseguire. Una costruzione più forte mai esercitata è in pratica più debole di una buona che lo è.
  7. Una chiave che è un’identità appartiene all’identità. Allora la revoca la ripulisce e una nuova iscrizione la sostituisce, senza una regola che qualcuno debba ricordare.

Per chiarezza su cosa sia tutto questo: il monitoraggio sta dietro un interruttore di rilascio spento per impostazione predefinita, quindi oggi nulla di ciò è acquistabile. Due cose non sono dimostrate. Lo scambio di autenticazione basato su password è stato misurato solo contro l’implementazione di riferimento della libreria del protocollo e non contro un server vero. E il binario della sonda scritto in Rust non è stato esercitato sul filo in questa tornata.

Dal lavoro di piattaforma di Provibr. Perché una sonda tratta come inaffidabile ogni messaggio che riceve è spiegato in una sonda che non si lascia convincere di nulla, e perché le credenziali dei pannelli di un cliente non arrivano mai fino a noi è spiegato in dove devono stare le credenziali di pannello.

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.