Salta al contenuto

Blog ·

Un canarino che gira contro server veri

Uno stub non prova l’autenticazione. Due difetti di monitoraggio che solo server veri potevano mostrare, e che cosa una pagina di stato può contare.

Di Provibr

Un controllo di monitoraggio che accede a un database è un piccolo pezzo di codice di protocollo con una proprietà sgradevole: quasi ogni modo di collaudarlo collauda la cosa sbagliata.

Il collaudo ovvio è un server stub che parla il protocollo. Funziona, e dimostra parecchio: la formazione dei frame, i tempi di attesa, che cosa succede se la connessione cade a metà di una stretta di mano, se un rifiuto produce il codice di errore giusto. Quel che non può dimostrare è l’autenticazione, e per un motivo strutturale. Uno stub che ricalcola il nostro stesso rimescolamento usa la nostra stessa implementazione a entrambi i capi del filo. Se scambia due argomenti in un digest, entrambi i lati li scambiano, e il test resta verde su codice che nessun server vero accetterebbe.

Quella lacuna era annotata come punto aperto alla fine del giro di monitoraggio precedente: SCRAM era stato misurato solo contro un’implementazione scritta anch’essa da noi. Un canarino la chiude.

Che cos’è qui un canarino

Un canarino è un piccolo insieme di prove che girano contro la cosa vera anziché contro un suo modello. Container usa e getta sul nostro server, legati a localhost, con dentro un vero PostgreSQL 17, un Redis 7, un MySQL 8.4 e un MariaDB 11. Gli adattatori vengono chiamati direttamente e non attraverso la busta dei comandi, perché il tema è il filo e non l’idraulica che lo circonda. Undici prove, tutte saltate per impostazione predefinita, e ciascuna si rifiuta di girare finché il suo indirizzo non è davvero impostato.

Quel secondo chiavistello non è ridondante rispetto al primo. Se qualcuno lancia l’intera raccolta togliendo il contrassegno di salto, una prova si collegherebbe altrimenti in silenzio a 127.0.0.1:0 e produrrebbe un fallimento che si legge come un difetto del nostro codice invece che come una variabile d’ambiente assente.

Che cosa coprono le undici:

ServerChe cosa viene dimostrato
PostgreSQL 17SCRAM con la password giusta e con una sbagliata, md5, trust, TLS richiesto contro un server con TLS spento, verifica completa del certificato contro un’ancora estranea
Redis 7Autenticazione con password e un utente con ACL, più la lista consentita di INFO
MySQL 8.4caching_sha2 con cache fredda e con cache calda, e una password sbagliata
MariaDB 11.8La stessa stretta di mano contro un’altra implementazione di essa

La distinzione tra cache fredda e calda conta perché sono due percorsi di codice diversi. MySQL usa in caching_sha2_password un rimescolamento a buon mercato appena il server ha già visto l’account, e chiede l’autenticazione completa la prima volta dopo un riavvio. Solo un server vero si riavvia.

Due difetti che nessuno stub avrebbe potuto mostrare

PostgreSQL, password sbagliata. Con la password sbagliata, PostgreSQL non risponde alla prima risposta SASL con una continuazione. Risponde con un errore che porta lo SQLSTATE 28P01, password non valida. L’adattatore aveva lì una destrutturazione nuda, che trasformava tutto ciò che non fosse una continuazione in un errore di protocollo: questo servizio non ha risposto come un server PostgreSQL. Lo SQLSTATE veniva buttato via. Una cliente le cui credenziali di monitoraggio erano scadute leggeva che il suo database era rotto.

Uno stub non passa mai per quel ramo, perché uno stub che collauda il percorso felice invia la continuazione. Entrambi i punti in cui ora può arrivare un errore, dopo il primo e dopo il secondo passo, lo mantengono un fallimento di autenticazione.

Redis, un campo vuoto. L’adattatore legge esattamente due campi da INFO: la versione e il ruolo. Chiedeva la sezione server, e role non sta in quella sezione. Sta in replication. Il campo era vuoto dal giro che lo aveva introdotto, e nulla lo diceva, perché lo stub rispondeva con un blocco INFO scritto da noi stessi in cui role compariva cortesemente dove ce lo aspettavamo.

La correzione sono due chiamate INFO separate e non una sola che nomini entrambe le sezioni, perché Redis accetta più sezioni in una INFO solo dalla versione 7.0. Su 6.x è un errore di sintassi, e poiché una INFO fallita di proposito non porta il controllo al rosso, il campo sarebbe semplicemente tornato vuoto sui server più vecchi. Un degrado silenzioso che sostituisce un degrado silenzioso.

Entrambi i difetti hanno la stessa forma. Nessuno dei due ha prodotto un errore. Uno ha prodotto quello sbagliato e l’altro niente del tutto, ed entrambi erano invisibili a una raccolta di test che era verde.

MySQL, e la libreria che non è stata aggiunta

Lo stesso giro ha aggiunto MySQL e MariaDB come tipo di controllo. Dietro non c’è nessuna libreria client, ed è una ripetizione voluta di una decisione precedente e non una decisione nuova.

Per PostgreSQL la scelta era stata di non scrivere uno SCRAM proprio, perché SCRAM ha un passo in cui il server firma qualcosa che il client dovrebbe verificare, e un’implementazione che salta quel passo è verde contro un server vero e altrettanto verde contro uno contraffatto. È la scorciatoia pericolosa: non fallisce mai a voce alta.

MySQL non ha un passo del genere. L’autenticazione nativa con password è una catena di digest SHA1 e il percorso veloce di caching_sha2_password è una catena di digest SHA256, e in nessuna delle due il server firma qualcosa la cui verifica potremmo tralasciare. Restano la formazione dei frame e due digest, ed entrambe le librerie di digest erano già nell’albero. Nemmeno un costruttore di interrogazioni serviva: c’è una interrogazione, SELECT 1, sta come costante, e la sua risposta attesa pure.

Quest’ultima si legge come una funzione mancante. Sopra una interrogazione fissa, un valore atteso configurabile è una manopola in cui ogni posizione diversa da quella predefinita fa fallire il controllo, e una manopola simile insegna a chi amministra che le manopole non significano nulla. Il suo posto sarà il giorno in cui esisterà una seconda interrogazione fissa, e non un giorno prima. Nemmeno la versione è una seconda interrogazione: MySQL la manda senza che gliela si chieda nel pacchetto di stretta di mano, e la seconda interrogazione è sempre quella a cui tra un anno cresce un campo di testo libero.

Due cose vengono rifiutate di netto. La capacità client che consente a un server di ordinare al client di leggere un file dal suo disco e caricarlo non viene richiesta, e il byte di risposta corrispondente è un errore di protocollo invece che un ramo che fa qualcosa. E l’autenticazione caching_sha2 completa avviene soltanto su TLS: l’alternativa su una linea nuda è recuperare la chiave pubblica del server su quella stessa linea non autenticata, e chi sta in mezzo risponde con la propria. Il rifiuto nomina la riparazione, ossia accendere TLS.

Una pagina di stato conta tick, non risultati

Lo stesso giro ha messo una pagina di stato pubblica davanti a tutto questo, e l’aritmetica di quella pagina richiedeva una decisione che sbaglia facilmente.

Ogni esecuzione dovuta di una regola è un tick. Un tick può finire come up, degraded o down, e può anche finire come source_unavailable (la nostra fonte di misura non c’era), configuration_error (la regola non aveva senso), unsupported (l’agente non conosceva quel tipo di controllo) oppure mancato. Questi ultimi quattro non dicono nulla sul servizio della clientela.

Se il denominatore di una disponibilità giornaliera fosse ogni tick dovuto, un guasto nostro abbasserebbe la cifra sulla pagina di stato della clientela. Il denominatore è perciò up più degraded più down: i tick in cui qualcuno ha davvero guardato il servizio. Se di quello non resta nulla, la risposta di quel giorno è che non c’è risposta, esplicitamente né zero né cento per cento. Sulla pagina stessa quei quattro stati compaiono come sconosciuti e mai come down.

Degraded conta per metà. È una scelta e non una legge di natura: un servizio che risponde tutto il giorno ma sta sopra la sua soglia di latenza non è né fuori servizio né a posto, e contarlo verso uno dei due lati rende privo di senso uno dei due.

I buchi vengono mostrati come buchi. Una regola creata la settimana scorsa non ha novanta giorni di storia, e una barra che riempie quel vuoto con il valore del giorno vicino afferma qualcosa che non è mai stato misurato.

Che cosa ci è costata una raccolta di test verde

Ognuno dei difetti qui sopra stava sotto un test superato. La raccolta di stub non era sbagliata; rispondeva a una domanda diversa da quella che tutti credevano. Il valore del canarino non è di essere una prova migliore. È di essere una prova in cui l’altro capo del filo è stato scritto da qualcuno che non condivide le nostre supposizioni.

Questo vale sei container usa e getta, e vale la pena scrivere che sono usa e getta: girano sulla nostra macchina, vengono rimossi quando terminano, e le prove si rifiutano di girare se gli indirizzi non ci sono.

Dal lavoro di monitoraggio di Provibr. Perché una sonda ospitata accetta esattamente un tipo di messaggio sta in una sonda che non si lascia convincere di nulla, e perché un segreto di monitoraggio viaggia una volta sola sta in un segreto che viaggia una volta.

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.