Salta al contenuto

Blog ·

Una sonda che non si lascia convincere

Monitorare da fuori la rete di un cliente significa gestire macchine nostre. Ecco come la piattaforma impedisce a una di esse di fare qualsiasi altra cosa.

Di Provibr

Verificare se la macchina di un cliente sia raggiungibile ha senso soltanto da un punto in cui quella macchina non si trova. Un agente dentro la rete può dire che il servizio è in ascolto; non può dire che la strada per arrivarci è interrotta. Quindi si mettono macchine proprie sull’internet pubblico e si lascia che siano loro a stabilire la connessione.

È una funzionalità e un rischio nello stesso respiro. Una sonda è hardware nostro, in un datacenter che condividiamo con estranei, che tiene aperta una connessione di controllo verso la nostra piattaforma. Se qualcosa su quella connessione riesce a convincerla a fare qualcosa di diverso dal misurare, diventa il punto d’appoggio più utile di tutto il prodotto.

Un solo binario, e il token decide chi si è

Una sonda non è un programma separato. È l’agente che i nostri clienti eseguono, avviato con probe enroll o probe run anziché con run, sullo stesso motore di monitoraggio e sulla stessa RPC di sessione. Una build ridotta all’osso non potrebbe eseguire affatto un comando verso un hypervisor, il che suona più sicuro, ma significa una seconda implementazione di ritentativi, backoff, rotazione dei certificati e motore dei controlli, e la seconda riceve ogni mese meno attenzione. Abbiamo tenuto un solo binario e spostato il confine in un punto verificabile.

Il modo ovvio per far arruolare un solo binario come due cose diverse è a quel punto un campo nella richiesta di arruolamento che dica quale delle due. È il modo sbagliato, e vale la pena essere netti sul perché: un client che dichiara la propria natura dichiara i propri privilegi, e ogni verifica successiva poggia su una stringa fornita dalla cosa che si sta verificando.

Quindi quel campo non esiste. Esistono due tabelle di token: enrollment_tokens con una chiave esterna non nulla verso agents, e monitoring_probe_enrollment_tokens con una verso monitoring_probes. Un token esiste esattamente in una delle due, quindi ciò che viene arruolato discende da qualcosa che abbiamo emesso noi anziché da qualcosa che ha detto il chiamante. Entrambi i fallimenti restituiscono lo stesso identico rifiuto, perché messaggi diversi direbbero a un attaccante quale dei due tipi di token ha trovato.

Il certificato che torna indietro porta un common name della forma probe:<id> e, come subject alternative name, un URI privo di organizzazione, mentre l’URI sul certificato di un agente nomina il cliente. Serve a un operatore che legge un certificato ed è esplicitamente non il confine: un common name è una stringa che scrive un’autorità di certificazione. Il confine è che un’impronta si risolve in agents oppure in monitoring_probes, mai in entrambe, perché quelle sono due tabelle.

Un verdetto per ogni messaggio, non un elenco di quelli ammessi

Il protocollo di sessione è fatto di due messaggi busta, ciascuno dei quali contiene una scelta fra alternative. Un elenco di autorizzazione che nomini soltanto ciò che una sonda può mandare resta verde per sempre, anche il giorno in cui qualcuno aggiunge un quinto tipo di messaggio, e quel giorno esiste un tipo di messaggio che nulla nomina e che quindi nulla rifiuta. Perciò le tabelle nel gateway nominano ogni campo di entrambe le buste con un verdetto esplicito: ventidue voci in salita, di cui vengono accettati tre messaggi veri più un numero di sequenza, e quattordici in discesa, di cui vengono accettate un’istruzione vera e tre conferme di sessione. Un test legge il TypeScript generato e fallisce se un campo non ha un verdetto, e fallisce anche nel verso opposto quando un nome nella tabella non esiste più nel protocollo, perché una regola che non vincola nulla si legge esattamente come una copertura. Aggiungere un messaggio di sessione costa adesso una riga, e quella riga è una decisione con sopra il nome di qualcuno nel commit.

In salita una sonda può mandare tre cose: hello, heartbeat e il risultato di un controllo. Il rifiuto che merita una spiegazione è quello dei risultati dei comandi: il dispatch abbina un risultato a un comando tramite identificatore anziché tramite mittente, quindi una sonda che potesse mandarne uno sarebbe in grado di chiudere un comando appartenente a un agente qualsiasi del parco. In discesa vengono rifiutate dieci voci, e quattro di esse sono il motivo per cui esiste l’intero progetto:

MessaggioChe cosa porterebbePerché una sonda non deve mai vederlo
Consegna di credenzialiLe credenziali di pannello e di hypervisor di un clienteUna sonda non ha vault e non ha integrazioni; non esiste uno stato in cui questo significhi qualcosa
Cancellazione di credenzialiL’istruzione di cancellarne unaStessa ragione, al contrario
Direttiva di netbootChe cosa servire a una macchina che si avvia dalla reteUna sonda non effettua alcun provisioning
Apertura di automazioneUna sandbox che esegue script del clienteUna sonda non esegue codice del cliente

Il registro in cui una sonda non compare

La tabella qui sopra è la metà dimostrabile della promessa. La metà portante è che non esiste una strada. Il dispatch dei comandi trova la propria destinazione tramite l’identificatore dell’agente; le sonde vivono in un registro proprio, la cui chiave è l’identificatore della sonda, e non hanno affatto un identificatore di agente. Nessuna ricerca può restituire una sessione di sonda al codice che manda una consegna di credenziali, nemmeno se qualcuno aggiungesse un nuovo tipo di istruzione dimenticando del tutto la tabella.

L’unica funzione che scrive verso una sonda controlla il verdetto alla sorgente anziché al destinatario, perché un messaggio sfuggito arriverebbe a una macchina nostra che esegue lo stesso binario di un agente di un cliente, la quale potrebbe semplicemente eseguirlo. Esegue inoltre un controllo che la tabella non sa esprimere: command è accettato, ma una richiesta di comando è a sua volta una scelta fra ventiquattro alternative, tutte tranne tre una famiglia di istruzioni verso un provider, quindi senza quel controllo «una sonda può ricevere comandi» significherebbe in silenzio «una sonda può ricevere un comando verso un hypervisor». Il payload deve essere esattamente un campo, e quel campo deve essere il controllo di monitoraggio. Il test percorre ogni campo di payload nel codice generato e verifica che tutti tranne uno siano rifiutati.

Scegliere una sonda, e ripiegare sulla successiva

Ogni località può ospitare più di una sonda, quindi qualcosa deve decidere. Un hash modulo il numero di sonde è sbagliato il giorno in cui quel numero cambia: si sposta quasi ogni regola in una volta sola, e grafici di latenza in tutto il prodotto saltano lo stesso giorno senza una ragione visibile. Il rendezvous hashing sposta soltanto ciò che era attaccato alla sonda che se n’è andata. La chiave è la revisione della regola e la località. La revisione è immutabile, quindi una regola che il cliente modifica può legittimamente finire altrove, mentre la scelta dentro una stessa revisione non deriva mai. Lo slot è deliberatamente fuori dalla chiave: con dentro il numero del tick, misurazioni consecutive finirebbero su sonde diverse, e una differenza fra due punti potrebbe essere tanto una differenza fra due macchine. La confrontabilità nel tempo vale più della distribuzione del carico.

Una sonda può deludere in due modi, che si notano in momenti diversi: può essere sparita quando si prova a mandare, oppure può accettare il comando e non rispondere mai, cosa che emerge solo quando è passato il timeout del controllo stesso. Entrambi producono un tentativo in più dentro lo stesso tick, e mai un secondo tick. Un failover non è una seconda misurazione, e se lo fosse una località sfortunata raddoppierebbe in silenzio il numero dei campioni. Il tetto è di due tentativi fisici, perché insieme devono stare dentro la scadenza del tick.

Quando una località non ha alcuna sonda sana, lo slot viene comunque preso e la sorgente viene registrata come non disponibile. Mai come «down»: l’assenza di una località nostra non dice nulla sulla macchina del cliente, e chiamarla un disservizio sarebbe un falso allarme generato dalla nostra stessa infrastruttura.

Tre punti che rifiutano un indirizzo privato

Una regola di monitoraggio è un campo in cui qualcuno indica una destinazione e una macchina nostra ci si connette ogni minuto. Senza limiti quello è un motore di falsificazione di richieste con sopra un indirizzo di fatturazione. Il bersaglio è un’assegnazione di indirizzo appartenente al server del cliente stesso anziché testo libero, il che toglie di mezzo la maggior parte del problema; il resto è un elenco di intervalli che una sonda ospitata non può toccare, oltre a quelli chiusi ovunque:

IntervalloPerché è chiuso
10/8, 172.16/12, 192.168/16Spazio privato. Su una sonda ospitata quello è il datacenter, non il cliente
100.64/10NAT di livello carrier, dove i provider appendono bilanciatori e percorsi di gestione
192.0.0/24, 192.0.2/24, 198.18/15, 198.51.100/24, 203.0.113/24, 192.88.99/24Assegnazioni di protocollo, benchmarking, documentazione, un anycast di inoltro deprecato. Nessuno di questi può essere una macchina di un cliente, quindi qualunque cosa risponda è più vicina del bersaglio
fc00::/7, 2001:db8::/32La prima e la terza riga qui sopra, in IPv6: spazio unique local e spazio di documentazione

Le forme IPv4 mappate vengono prima liberate dal loro involucro, altrimenti ogni regola qui sopra ha una grafia che le passa accanto.

Il controllo gira in tre punti e servono tutti e tre. La piattaforma rifiuta prima di mandare, così un bersaglio sbagliato non raggiunge mai una macchina che potrebbe eseguirlo. La sonda rifiuta al momento dell’esecuzione, perché solo lì si sa che cosa un nome abbia davvero risolto, e questo può cambiare da un’esecuzione all’altra. E la piattaforma rilegge l’indirizzo riferito una volta di più quando arriva il risultato, trasformando una sonda che ha misurato altrove in una risposta non valida anziché in un dato. Una destinazione privata è un errore di configurazione e mai un guasto: non è stato misurato nulla, quindi non c’è nulla da dire sul bersaglio.

Che cosa metteremmo per iscritto

  1. Mai lasciare che sia il client a dichiarare la propria natura. La si ricavi da qualcosa che si è emesso, tenendo identici entrambi i rifiuti perché l’errore non faccia da oracolo.
  2. Enumerare tutto il protocollo, non la parte permessa. Un test legga il codice generato e pretenda un verdetto per ogni campo, nei due versi, così che un nuovo tipo di messaggio sia un test rosso anziché una lacuna.
  3. Mettere il confine vero dove non lo si possa dimenticare. Un registro separato senza identificatore di agente significa che non esiste una strada, qualunque cosa dica la tabella.
  4. Controllare il payload oltre alla busta. «I comandi sono ammessi» non è la stessa affermazione di «questo comando è ammesso».
  5. Tenere l’orologio fuori dalla chiave di assegnazione e fare di un failover un tentativo in più anziché un campione in più, così che un cambiamento nei dati non possa mai essere scambiato per un cambiamento nello strumento.

Per chiarezza: il monitoraggio ospitato sta dietro un flag di rollout disattivato per impostazione predefinita, sotto non c’è alcuno strato di entitlement, e niente di tutto questo è acquistabile oggi. Anche ciò che non è dimostrato merita di essere nominato. Il sottocomando probe non ha mai girato da un capo all’altro, perché uno dei suoi componenti di rete non compila sulla macchina su cui è stato scritto; le politiche di località sono state esercitate soltanto nella loro forma più semplice; IPv6 non è stato misurato da nessuna parte. La connessione in sé è stata percorsa davvero, dall’arruolamento fino a una sessione e a un controllo tornato indietro, e su quella connessione un messaggio proibito è stato rifiutato.

Dal lavoro di monitoraggio di Provibr. Perché un nome host debba essere dimostrato prima di poter diventare un bersaglio è in come dimostrare di possedere un nome host, e perché un incidente non si chiuda solo perché le misurazioni sono cessate è in un incidente che non si chiude sul silenzio.

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.