Blog ·
O sondă care nu poate fi convinsă de nimic
Monitorizarea din afara rețelei unui client înseamnă să rulăm mașini proprii. Iată cum împiedică platforma una dintre ele să facă orice altceva.
De Provibr
A verifica dacă mașina unui client este accesibilă are sens numai dintr-un loc în care mașina nu se află. Un agent din interiorul rețelei vă poate spune că serviciul ascultă; nu vă poate spune că ruta către el este întreruptă. Așa că vă puneți propriile mașini pe internetul public și le lăsați pe ele să facă legătura.
Este, în aceeași răsuflare, o funcționalitate și o vulnerabilitate. O sondă este hardware pe care îl deținem, într-un centru de date pe care îl împărțim cu necunoscuți, ținând o conexiune de control deschisă către platforma noastră. Dacă ceva de pe acea conexiune o poate convinge să facă altceva decât să măsoare, ea devine cel mai util punct de sprijin din tot produsul.
Un singur binar, iar jetonul decide cine sunteți
O sondă nu este un program separat. Este chiar agentul pe care îl rulează clienții noștri, pornit cu probe enroll sau probe run în loc de run, pe același motor de monitorizare și pe același RPC de sesiune. O compilare redusă nu ar putea executa deloc o comandă de hipervizor, ceea ce sună mai sigur, dar înseamnă o a doua implementare pentru reîncercări, backoff, rotația certificatelor și motorul de verificări, iar a doua primește în fiecare lună mai puțină atenție. Am păstrat un singur binar și am mutat granița undeva unde se poate testa.
Modul evident de a lăsa un singur binar să se înroleze drept două lucruri este atunci un câmp în cererea de înrolare care spune care dintre ele. Este modul greșit, iar motivul merită spus pe șleau: un client care își declară propriul fel își declară propriile privilegii, iar fiecare verificare ulterioară se sprijină pe un șir de caractere furnizat chiar de lucrul verificat.
Așa că un asemenea câmp nu există. Există două tabele de jetoane: enrollment_tokens, cu o cheie străină not null către agents, și monitoring_probe_enrollment_tokens, cu una către monitoring_probes. Un jeton există în exact unul dintre ele, așa că ceea ce se înrolează decurge din ceva emis de noi și nu din ceva spus de apelant. Ambele eșecuri întorc exact același refuz, pentru că mesaje diferite i-ar spune unui atacator ce fel de jeton a găsit.
Certificatul care se întoarce poartă un nume comun de forma probe:<id> și, drept nume alternativ al subiectului, un URI care nu conține nicio organizație, acolo unde URI-ul de pe certificatul unui agent numește clientul. Acela este pentru un operator care citește un certificat și în mod explicit nu este granița: un nume comun este un șir scris de o autoritate de certificare. Granița este că o amprentă se rezolvă în agents sau în monitoring_probes, niciodată în amândouă, pentru că acelea sunt două tabele.
Un verdict pentru fiecare mesaj, nu o listă a celor permise
Protocolul de sesiune este format din două mesaje-anvelopă, fiecare purtând una dintr-un set de alternative. O listă de permisiuni care numește numai ce are voie o sondă să trimită rămâne verde la nesfârșit, inclusiv în ziua în care cineva adaugă un al cincilea tip de mesaj, iar în acea zi există un tip de mesaj pe care nimic nu îl pomenește și, prin urmare, nimic nu îl refuză. Așa că tabelele din gateway numesc fiecare câmp al ambelor anvelope cu un verdict explicit: douăzeci și două de intrări în amonte, dintre care sunt acceptate trei mesaje adevărate plus un număr de secvență, și paisprezece în aval, dintre care sunt acceptate o instrucțiune adevărată și trei confirmări de sesiune. Un test citește codul TypeScript generat și eșuează dacă un câmp nu are verdict, și eșuează și în sens invers, atunci când un nume din tabel nu mai există în protocol, pentru că o regulă care nu restrânge nimic arată exact ca acoperirea. Adăugarea unui mesaj de sesiune costă acum o singură linie, iar acea linie este o decizie cu numele cuiva pe commit.
În amonte, o sondă are voie să trimită trei lucruri: salutul, heartbeat-ul și rezultatul unei verificări. Refuzul care merită explicat este cel al rezultatelor de comandă: dispecerul potrivește un rezultat cu o comandă după identificator și nu după expeditor, așa că o sondă care ar putea trimite unul ar putea închide o comandă care aparține oricărui agent din flotă. În aval, zece intrări sunt refuzate, iar patru dintre ele sunt motivul pentru care există tot acest proiect:
| Mesaj | Ce ar transporta | De ce o sondă nu are voie să îl vadă niciodată |
|---|---|---|
| Împingerea datelor de autentificare | Datele de acces la panou și la hipervizor ale unui client | O sondă nu are seif și nu are integrare; nu există nicio stare în care asta să însemne ceva |
| Uitarea datelor de autentificare | O instrucțiune de a șterge una dintre ele | Același motiv, în sens invers |
| Directiva de netboot | Ce anume să i se servească unei mașini care pornește din rețea | O sondă nu aprovizionează nimic |
| Deschiderea unei automatizări | Un spațiu izolat care rulează scripturi ale clientului | O sondă nu execută cod al clientului |
Registrul în care o sondă nu se află
Tabelul de mai sus este jumătatea demonstrabilă a promisiunii. Jumătatea de rezistență este că nu există traseu. Dispecerul de comenzi își găsește destinația după identificatorul de agent; sondele trăiesc în registrul lor propriu, cheiat pe identificatorul de sondă, și nu au deloc un identificator de agent. Nicio căutare nu poate întoarce o sesiune de sondă către codul care trimite date de autentificare, chiar dacă cineva ar adăuga un fel nou de instrucțiune și ar uita cu totul de tabel.
Singura funcție care scrie către o sondă verifică verdictul la sursă și nu la destinatar, pentru că un mesaj strecurat ar ajunge la o mașină de-a noastră care rulează același binar ca un agent de client și care l-ar putea pur și simplu executa. Ea face și o verificare pe care tabelul nu o poate exprima: command este acceptat, dar o cerere de comandă este ea însăși o alegere între douăzeci și patru de alternative, dintre care toate în afară de trei sunt câte o familie de instrucțiuni de furnizor, așa că fără ea „o sondă poate primi comenzi” ar însemna în tăcere „o sondă poate primi o comandă de hipervizor”. Sarcina utilă trebuie să fie exact un câmp, iar acel câmp trebuie să fie verificarea de monitorizare. Testul parcurge fiecare câmp de sarcină utilă din codul generat și impune ca toate în afară de unul să fie refuzate.
Alegerea unei sonde și comutarea către următoarea
Fiecare locație poate ține mai mult de o sondă, așa că ceva trebuie să decidă. Un hash modulo numărul de sonde este greșit în ziua în care acel număr se schimbă: aproape fiecare regulă se mută deodată, iar grafice de latență din tot produsul sar în aceeași zi, fără vreun motiv vizibil. Rendezvous hashing mută numai ce era atașat sondei care a plecat. Cheia este revizia regulii și locația. Revizia este imuabilă, așa că o regulă pe care clientul o modifică poate ajunge în mod legitim în altă parte, în timp ce alegerea din interiorul unei singure revizii nu derivează niciodată. Slotul lipsește în mod deliberat din cheie: cu numărul tactului acolo, măsurători consecutive ar ajunge pe sonde diferite, iar o diferență între două puncte ar putea la fel de bine să fie o diferență între două mașini. Comparabilitatea în timp valorează mai mult decât distribuirea sarcinii.
O sondă vă poate dezamăgi în două feluri, observate în momente diferite: poate să nu mai fie acolo când încercați să trimiteți, sau poate accepta comanda și să nu răspundă niciodată, ceea ce iese la iveală abia după ce a trecut termenul propriu al verificării. Amândouă produc încă o încercare în interiorul aceluiași tact și niciodată un al doilea tact. O comutare nu este o a doua măsurătoare, iar dacă ar fi, o locație ghinionistă ar dubla în tăcere numărul de eșantioane. Plafonul este de două încercări fizice, pentru că împreună trebuie să încapă în termenul tactului.
Când o locație nu are nicio sondă sănătoasă, slotul este totuși revendicat, iar sursa este consemnată drept indisponibilă. Niciodată drept căzută: absența unei locații de-a noastră nu spune nimic despre mașina clientului, iar a o numi o întrerupere ar fi o alarmă falsă generată de propria noastră infrastructură.
Trei locuri care refuză o adresă privată
O regulă de monitorizare este un câmp în care cineva numește o destinație, iar o mașină de-a noastră se conectează la ea în fiecare minut. Fără limite, aceasta este o mașinărie de falsificare a cererilor, cu o adresă de facturare pe ea. Ținta este o alocare de adresă care aparține serverului propriu al clientului și nu un text liber, ceea ce înlătură cea mai mare parte a problemei; restul este o listă de intervale pe care o sondă găzduită nu are voie să le atingă, peste cele închise peste tot:
| Interval | De ce este închis |
|---|---|
10/8, 172.16/12, 192.168/16 | Spațiu privat. Pe o sondă găzduită acela este centrul de date, nu clientul |
100.64/10 | NAT la nivel de operator, acolo unde furnizorii atârnă echilibratoare de sarcină și căi de administrare |
192.0.0/24, 192.0.2/24, 198.18/15, 198.51.100/24, 203.0.113/24, 192.88.99/24 | Atribuiri de protocol, evaluare comparativă, documentație, un anycast de retransmisie învechit. Niciunul dintre ele nu poate fi o mașină de client, așa că orice răspunde se află mai aproape decât ținta |
fc00::/7, 2001:db8::/32 | Primul și al treilea rând de mai sus, în IPv6: spațiu local unic și spațiu de documentație |
Formele IPv4 mapate sunt despachetate mai întâi, altfel fiecare regulă de mai sus are o scriere care trece pe lângă ea.
Verificarea rulează în trei locuri și toate trei sunt necesare. Platforma refuză înainte de trimitere, așa că o țintă greșită nu ajunge niciodată la o mașină care ar putea să o execute. Sonda refuză la execuție, pentru că numai acolo se știe către ce s-a rezolvat de fapt un nume, iar asta poate diferi de la o rulare la alta. Iar platforma citește încă o dată adresa raportată atunci când sosește rezultatul, transformând o sondă care a măsurat în altă parte într-un răspuns nevalid și nu într-un punct de date. O destinație privată este o eroare de configurare și niciodată un eșec: nu s-a măsurat nimic, așa că nu există nimic de spus despre țintă.
Ce am nota
- Nu lăsați niciodată clientul să își numească propriul fel. Deduceți-l din ceva ce ați emis dumneavoastră și păstrați ambele refuzuri identice, ca eroarea să nu fie un oracol.
- Enumerați tot protocolul, nu partea permisă. Lăsați un test să citească codul generat și să ceară un verdict pentru fiecare câmp, în ambele sensuri, astfel încât un tip nou de mesaj să fie un test roșu și nu un gol.
- Puneți granița adevărată acolo unde nu poate fi uitată. Un registru separat, fără identificator de agent, înseamnă că nu există traseu, orice ar spune tabelul.
- Verificați sarcina utilă, nu numai anvelopa. „Comenzile sunt permise” nu este aceeași afirmație cu „această comandă este permisă”.
- Țineți ceasul în afara cheii de atribuire și faceți din comutare o încercare în plus, nu un eșantion în plus, astfel încât o schimbare din date să nu poată fi niciodată confundată cu o schimbare a instrumentului.
Pentru claritate: monitorizarea găzduită stă în spatele unui indicator de lansare care este oprit implicit, sub el nu există niciun strat de drepturi, iar nimic din toate acestea nu se poate cumpăra astăzi. Merită numit și ceea ce rămâne nedovedit. Subcomanda de sondă nu a rulat niciodată cap la cap, pentru că una dintre componentele ei de rețea nu se compilează pe mașina pe care a fost scris acest cod; politicile de locație au fost exersate numai în forma lor cea mai simplă; IPv6 nu a fost măsurat nicăieri. Firul propriu-zis a fost condus cu adevărat, de la înrolare, printr-o sesiune, până la o verificare care s-a întors, iar un mesaj interzis pe acea conexiune a fost refuzat.
Din lucrul la monitorizare de la Provibr. De ce un nume de gazdă trebuie dovedit înainte de a putea fi o țintă se află în proprietatea unui nume de gazdă, fără certificat, iar de ce un incident nu se închide doar pentru că măsurătorile s-au oprit se află în un incident care nu se închide pe tăcere.