Salta al contenuto

Blog ·

Un segreto errato somiglia al silenzio

Tre protocolli di monitoraggio scritti a mano invece che importati, e un guasto che il protocollo stesso non permette di distinguere da un server morto.

Di Provibr

Aggiungere un controllo di monitoraggio per un nuovo protocollo sembra una questione di dipendenze. Esiste un pacchetto per MongoDB, uno per BSON, uno che parla il protocollo di rete di SQL Server. Tre righe in un manifesto, quaranta righe di collante, fatto.

Tutti e tre i protocolli sono stati scritti a mano, e vale la pena annotare le ragioni perché nessuna di esse è «non inventato qui». Due sono misure, e la terza è una promessa già fatta ai clienti su ciò che questo agente sa fare.

Che cosa porta davvero una dipendenza

Il pacchetto BSON trascina quarantuno pacchetti. È misurabile ed è stato misurato, ma non è stato il numero a decidere.

Il controllo legge quattro campi da una risposta del server. Un tipo di documento generico tiene in memoria l’intera risposta: l’elenco degli host, le etichette, l’istante dell’ultima scrittura, l’ora del cluster, tutto ciò che il server ha voluto inviare. A quel punto «leggiamo solo quattro campi» diventa un accordo fra sviluppatori invece che una proprietà del codice, e gli accordi li rompe il turno successivo che ha bisogno di un quinto campo per una buona ragione.

Esiste quindi un lettore che percorre gli elementi, restituisce i quattro richiesti e salta il resto senza depositarlo da nessuna parte. Non può trattenere la risposta, perciò la promessa è una forma e non un’abitudine. La stessa decisione dell’elenco di permessi sul comando INFO di Redis, e la stessa che ha tenuto fuori dal controllo di PostgreSQL un driver di database completo: un driver porta con sé un costruttore di query, e allora esiste, dentro il binario che gira sulla rete di un cliente, del codice capace di porre una domanda arbitraria al suo database.

Il pacchetto per SQL Server è la versione più tagliente. Compila per il nostro obiettivo di rilascio, che era proprio ciò che ci aspettavamo lo bloccasse. Quello che porta è una seconda pila TLS: una linea uscita dalla manutenzione accanto a quella attuale che già distribuiamo. Il fornitore crittografico dell’intero processo viene installato per la linea attuale e non fa nulla per quella vecchia, quindi il TLS di un controllo userebbe un fornitore diverso da ogni altra connessione di questo agente; una vulnerabilità in quella linea è una che distribuiamo e non possiamo correggere senza attendere il progetto a monte; e la politica sui certificati, che vive in un solo modulo proprio perché non possa divergere, riceverebbe una seconda implementazione contro un’altra interfaccia.

Saltare è il punto in cui sbaglia un lettore scritto a mano

Chi scrive un proprio lettore per un formato binario può commettere un errore che pesa più degli altri, e non sta nell’analisi. Sta nel salto.

Ogni valore BSON ha un byte di tipo e una lunghezza che ne deriva. Un lettore che conosce i tipi in cui si è imbattuto per caso, e che salta un tipo sconosciuto di zero byte, non fallisce: continua a leggere il resto del documento dalla posizione sbagliata. Tutto ciò che segue è spostato. Si ottengono valori plausibili che non sono quelli inviati.

La tabella delle lunghezze è perciò completa, con un rifiuto chiuso per tutto il resto. Un byte di tipo sconosciuto è un errore di protocollo e mai un salto.

La stessa famiglia di errori sta un livello più in alto, in come si leggono i numeri. Un server MongoDB risponde ok: 1 come virgola mobile e un code di errore come intero a 32 bit, nello stesso documento. Un lettore che conosce solo i 32 bit perde il primo in silenzio, e allora ogni accesso fallito si legge come riuscito. Il lettore accetta un numero BSON in tutte e tre le larghezze e restituisce un intero.

Il passo di autenticazione che deve poter fallire

Il controllo accede con SCRAM-SHA-256 e non con SCRAM-SHA-1, per due ragioni che puntano nella stessa direzione.

Il meccanismo più vecchio non immette la password in SCRAM. Immette un MD5 esadecimale del nome utente, di una stringa fissa e della password. Supportarlo significherebbe scrivere codice che produce un MD5 di credenziali, per un meccanismo che nessun server attuale ha bisogno di offrire.

La ragione più silenziosa è peggiore. Un server che offre entrambi finirebbe, ripiegando, su quello più debole senza che nulla diventi rosso, e allora una misura verde sta sopra un accesso più debole di quanto il cliente creda. Se il server non offre SHA-256 per quell’utente, il controllo rifiuta con un codice proprio e lo dichiara.

Scrivere SCRAM a mano era stato rifiutato in un turno precedente per il controllo di PostgreSQL, e l’obiezione era reale: lo scambio ha una firma del server che il client dovrebbe verificare, e un’implementazione che salta quel passo è verde davanti a un server autentico e verde allo stesso modo davanti a uno falsificato. Qui l’obiezione non è stata aggirata ma affrontata. La verifica della firma è una funzione a sé con un proprio codice di errore, perché «il passo manca» non sia la stessa cosa di «il passo è riuscito», ed esiste una prova che presenta una firma falsificata e pretende che l’accesso fallisca. Quella prova è stata dimostrata in rosso togliendo la verifica.

Il pacchetto usato dal controllo di PostgreSQL qui non era utilizzabile, ed è misurato e non supposto. La sua implementazione di SCRAM scrive il primo messaggio del client con il nome utente vuoto, perché PostgreSQL invia il nome nel proprio pacchetto di avvio. MongoDB non ha quel pacchetto e legge il nome proprio da quel campo. Inserirlo dopo cambierebbe gli stessi byte su cui la firma è già stata apposta, e il server rifiuterebbe la nostra prova. Non esiste un’interfaccia per passarlo.

Un guasto che il protocollo non lascia nominare

Ora la parte che nessuno stub avrebbe trovato.

RADIUS viene controllato con una richiesta Status-Server, che chiede al server se è vivo e nient’altro. L’alternativa ovvia, una richiesta di accesso con un account di prova, è stata scartata: è un vero tentativo di accesso sulla rete del cliente, con un account reale che qualcuno potrebbe usare, e scrive una riga nel suo registro di autenticazione in mezzo ai suoi utenti veri. Con un intervallo di un minuto sono millequattrocentoquaranta righe al giorno che non dicono nulla sul suo servizio, e in molte installazioni una password sbagliata è motivo per bloccare un account, per cui il monitoraggio provoca esattamente l’incidente che avrebbe dovuto vedere arrivare.

Poi è stato misurato, contro un server reale, che cosa produce un segreto condiviso sbagliato.

Nulla. Un server che non riesce a verificare l’autenticatore di messaggio di una richiesta scarta il pacchetto in silenzio, senza risposta di errore. Sta nella specifica, e significa che il quadro di guasto di un segreto sbagliato non si distingue da una regola di firewall, da una porta errata o da una macchina morta.

In un adattatore non si corregge, perché è una proprietà del protocollo. Quello che si può fare è smettere di fingere. Il messaggio «nessuna risposta» nomina un segreto condiviso sbagliato come possibile causa, nel testo che il cliente legge, perché un amministratore non passi un pomeriggio nel suo firewall a cercare qualcosa che sta in un file di configurazione. E se il server risponde comunque, l’autenticatore di risposta viene verificato, e una discrepanza lì viene segnalata come fatto di autenticazione invece che come errore di protocollo, perché in quell’assetto lo è davvero.

SituazioneChe cosa vede il clientePerché
Segreto sbagliato, nessuna rispostaNessuna risposta, con il segreto come causa possibileIl server ha scartato il pacchetto; non è tornato nulla da esaminare
Segreto sbagliato, arriva una rispostaUn segreto sbagliato, come esito di autenticazioneL’autenticatore di risposta non corrispondeva
Segreto giusto, nessuna rispostaNessuna rispostaLo stesso silenzio, e merita le stesse parole

Quella riga centrale è l’unico caso in cui la differenza si vede, e non è quello frequente.

Un server che non si può misurare, detto ad alta voce

L’ultima misura è quella che è piaciuta di meno.

SQL Server su Linux genera un certificato al primo avvio. Quel certificato è X.509 versione 1, una forma senza estensioni che da tempo non è più pensata per TLS, e l’analizzatore di certificati sottostante la rifiuta. La modalità cifrata fallisce quindi per una versione di certificato non supportata.

Vale la pena essere precisi sul perché quella modalità verifichi ancora qualcosa. L’identità lì non viene pesata: il nome nel certificato non viene controllato. La firma sì, perché senza di essa non resta una stretta di mano ma uno scambio di byte con chiunque abbia risposto. Per pesare una firma occorre leggere la chiave pubblica dal certificato, ed è proprio il passo che rifiuta.

L’esito è dunque: un SQL Server con il proprio certificato generato non è misurabile con questo controllo. Il cliente legge il vero errore dell’analizzatore come dettaglio, citato invece che parafrasato, e il rimedio è un certificato vero nella configurazione del server, cosa che un amministratore che rende monitorabile il proprio server fa comunque.

Due cose non sono state fatte di proposito. La verifica della firma non viene saltata in quella modalità, perché romperebbe la promessa proprio sull’impostazione che la maggior parte sceglie e trasformerebbe un rifiuto visibile in una debolezza invisibile. E non esiste ancora un codice di errore dedicato a questo caso, perché l’unico modo per riconoscerlo sarebbe confrontare il testo leggibile di una libreria, e questa è un’ipotesi sul codice altrui che può spostarsi in silenzio. Merita un turno a sé, con una propria prova.

Non esiste neppure una modalità del tutto senza cifratura, per una ragione propria di questo protocollo: la password nel pacchetto di accesso viene offuscata con un o esclusivo fisso e uno scambio di quartetti. È una codifica, non una cifratura. Chi vede i byte legge la password, quindi una modalità senza cifratura invierebbe in chiaro la password di un cliente sulla sua rete, sotto un nome che suggerisce altro.

Che cosa ha trovato il canarino

Tutto è girato contro server reali e non stub: quattordici controlli contro MongoDB nudo e con TLS, un vero server RADIUS e un vero SQL Server. Il risultato più utile è noioso. Una password sbagliata è dappertutto un errore di autenticazione e mai un errore di protocollo. È la distinzione che dice a un cliente se guardare le proprie credenziali o la propria rete, e un turno precedente l’aveva sbagliata nel controllo di PostgreSQL: un errore durante il passo di autenticazione veniva tradotto in «questo non è un server PostgreSQL».

Che cosa annoteremmo

  1. Conti che cosa trattiene una dipendenza, non solo quanto costa. Un tipo di documento generico trasforma «leggiamo quattro campi» in un accordo invece che in una proprietà.
  2. Un driver porta con sé un costruttore di query. Se la promessa è che nessuna query arbitraria possa partire, il codice per inviarne una non dovrebbe esistere nel binario.
  3. Due pile TLS in un binario sono un problema di politica, non di dimensione. Quella che non ha scelto è quella che non può correggere.
  4. In un lettore scritto a mano la tabella dei salti è la parte pericolosa. Un tipo sconosciuto deve essere un errore; saltarlo di zero byte sposta tutto ciò che segue.
  5. Accetti un numero in ogni larghezza permessa dal protocollo. Un campo che arriva in virgola mobile basta a leggere ogni fallimento come un successo.
  6. Un ripiego su un meccanismo più debole deve essere un rifiuto, non un ripiego. Il verde sopra un accesso più debole è peggio del rosso.
  7. Se il protocollo rende un guasto indistinguibile, lo dica nel messaggio. La formulazione è l’unico posto in cui quella distinzione resta possibile.
  8. Alcuni server non sono misurabili, e questo va nella documentazione e non in un aggiramento. L’aggiramento qui sarebbe stato smettere di verificare le firme.

Per chiarezza su che cosa sia tutto questo: i tre controlli sono costruiti, provati contro server reali e dietro un interruttore di distribuzione. Restano non dimostrati: il viaggio di tutti e tre sul filo verso un vero agente di un cliente, la contabilità RADIUS oltre la richiesta di stato e SQL Server in assetti diversi da un’unica istanza su Linux.

Dal lavoro di piattaforma su Provibr. Come lo stesso approccio con il canarino abbia scoperto due difetti nei controlli di database precedenti si trova in un canarino che gira contro server reali, e come giungano all’agente le credenziali usate da questi controlli in un segreto che viaggia una volta sola.

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.