Blog ·
Een probe die zich nergens toe laat overhalen
Meten van buiten het netwerk van een klant betekent dat je eigen machines draait. Zo zorgt het platform dat zo'n machine niets anders kan dan meten.
Door Provibr
Controleren of een machine van een klant bereikbaar is heeft alleen betekenis vanaf een plek waar die machine niet staat. Een agent binnen het netwerk kan je vertellen dat de dienst luistert; hij kan je niet vertellen dat de route ernaartoe stuk is. Dus zet je eigen machines op het publieke internet en laat je die de verbinding maken.
Dat is in een adem een functie en een risico. Een probe is hardware van ons, in een datacenter dat we met vreemden delen, met een open stuurverbinding naar ons platform. Kan iets op die verbinding hem overhalen tot iets anders dan meten, dan is hij het nuttigste bruggenhoofd in het hele product.
Eén binary, en het token bepaalt wie je bent
Een probe is geen apart programma. Het is de agent die onze klanten draaien, gestart met probe enroll of probe run in plaats van run, op dezelfde monitoringmotor en dezelfde sessie-RPC. Een uitgeklede build kan een hypervisorcommando helemaal niet uitvoeren, wat veiliger klinkt, maar het betekent een tweede uitvoering van opnieuw proberen, backoff, certificaatrotatie en de controlemotor, en die tweede krijgt elke maand minder aandacht. We hebben één binary gehouden en de grens verplaatst naar een plek waar hij te toetsen is.
De voor de hand liggende manier om één binary als twee dingen te laten inschrijven is dan een veld in het inschrijvingsverzoek dat zegt welke van de twee het is. Dat is de verkeerde manier, en het is de moeite waard om ronduit te zeggen waarom: een client die zijn eigen soort opgeeft, geeft zijn eigen rechten op, en elke latere controle steunt dan op een tekenreeks die is aangeleverd door precies datgene wat gecontroleerd wordt.
Dat veld bestaat dus niet. Er zijn twee tokentabellen: enrollment_tokens met een not-null-foreign-key naar agents, en monitoring_probe_enrollment_tokens met er een naar monitoring_probes. Een token bestaat in precies een van de twee, dus wat er ingeschreven wordt volgt uit iets dat wij hebben uitgegeven en niet uit iets dat de aanroeper zei. Beide mislukkingen geven dezelfde weigering terug, want verschillende meldingen zouden een aanvaller vertellen wat voor soort token hij gevonden had.
Het certificaat dat terugkomt draagt een common name van de vorm probe:<id> en, als subject alternative name, een URI zonder organisatie erin, waar de URI op het certificaat van een agent juist de klant noemt. Dat is er voor een beheerder die een certificaat leest en het is uitdrukkelijk niet de grens: een common name is een tekenreeks die een certificaatautoriteit erin schrijft. De grens is dat een vingerafdruk oplost in agents of in monitoring_probes, nooit in allebei, want dat zijn twee tabellen.
Een oordeel voor elk bericht, niet een lijst van wat toegestaan is
Het sessieprotocol bestaat uit twee envelopberichten, die elk een keuze uit een verzameling alternatieven dragen. Een toelatingslijst die alleen noemt wat een probe mag sturen blijft eeuwig groen, ook op de dag dat iemand een vijfde berichtsoort toevoegt, en op die dag is er een berichtsoort die nergens genoemd wordt en dus ook nergens geweigerd. De tabellen in de gateway noemen daarom elk veld van beide enveloppen met een uitdrukkelijk oordeel: tweeëntwintig regels stroomopwaarts, waarvan drie echte berichten plus een volgnummer geaccepteerd worden, en veertien stroomafwaarts, waarvan één echte opdracht plus drie sessiebevestigingen geaccepteerd worden. Een test leest de gegenereerde TypeScript en faalt als een veld geen oordeel heeft, en faalt ook de andere kant op wanneer een naam in de tabel niet meer in het protocol bestaat, want een regel die niets begrenst leest precies als dekking. Een sessiebericht toevoegen kost nu één regel, en die regel is een beslissing met iemands naam op de commit.
Stroomopwaarts mag een probe drie dingen sturen: hello, heartbeat en een controleresultaat. De weigering die uitleg verdient is die van commandoresultaten: de dispatcher koppelt een resultaat aan een commando op identificatie en niet op afzender, dus een probe die er een kon sturen zou een commando van willekeurig welke agent in de vloot kunnen afsluiten. Stroomafwaarts worden er tien geweigerd, en vier daarvan zijn waar het hele ontwerp voor bestaat:
| Bericht | Wat het zou dragen | Waarom een probe het nooit mag zien |
|---|---|---|
| Inloggegevens versturen | Paneel- en hypervisorinloggegevens van een klant | Een probe heeft geen kluis en geen integratie; er is geen toestand waarin dit iets betekent |
| Inloggegevens vergeten | Een opdracht om er daar een van te wissen | Dezelfde reden, omgekeerd |
| Netboot-instructie | Wat er geserveerd moet worden aan een machine die van het netwerk opstart | Een probe richt niets in |
| Automatisering openen | Een sandbox die scripts van een klant draait | Een probe voert geen klantcode uit |
Het register waar een probe niet in staat
De tabel hierboven is de bewijsbare helft van de belofte. De dragende helft is dat er geen route is. De commandodispatch vindt zijn bestemming op agent-identificatie; probes leven in hun eigen register, gesleuteld op probe-identificatie, en hebben helemaal geen agent-identificatie. Geen enkele opzoeking kan een probesessie teruggeven aan de code die inloggegevens verstuurt, ook niet als iemand een nieuwe soort instructie toevoegde en de tabel volledig vergat.
De enige functie die naar een probe schrijft toetst het oordeel bij de bron en niet bij de ontvanger, want een bericht dat erdoorheen glipte zou aankomen bij een machine van ons die dezelfde binary draait als een klantagent, en die zou het gewoon kunnen uitvoeren. Hij voert daarnaast een controle uit die de tabel niet kan uitdrukken: command wordt geaccepteerd, maar een commandoverzoek is zelf een keuze uit vierentwintig alternatieven, waarvan er op drie na elk een familie providerinstructies is, dus zonder die controle zou "een probe mag commando's ontvangen" stilletjes betekenen "een probe mag een hypervisorcommando ontvangen". De payload moet precies één veld zijn, en dat veld moet de monitoringcontrole zijn. De test loopt elk payloadveld in de gegenereerde code langs en eist dat alles op één na geweigerd wordt.
Een probe kiezen, en omschakelen naar de volgende
Elke locatie kan meer dan één probe bevatten, dus iets moet kiezen. Een hash modulo het aantal probes is fout op de dag waarop dat aantal verandert: bijna elke regel verhuist tegelijk, en overal in het product springen latentiegrafieken op dezelfde dag zonder zichtbare reden. Rendezvous-hashing verplaatst alleen wat aan de vertrokken probe hing. De sleutel is de revisie van de regel plus de locatie. De revisie is onveranderlijk, dus een regel die de klant bewerkt mag terecht ergens anders landen, terwijl de keuze binnen één revisie nooit verschuift. Het slot zit er met opzet niet in: met het nummer van de meetronde erbij zouden opeenvolgende metingen op verschillende probes landen, en dan kan een verschil tussen twee punten net zo goed een verschil tussen twee machines zijn. Vergelijkbaarheid door de tijd heen is meer waard dan het spreiden van belasting.
Een probe kan je op twee manieren in de steek laten, en je merkt die twee op verschillende momenten: hij kan weg zijn op het moment dat je wilt versturen, of hij kan het commando aannemen en nooit antwoorden, wat pas bovenkomt als de timeout van de controle zelf verstreken is. Allebei leveren ze één extra poging binnen dezelfde meetronde op, en nooit een tweede meetronde. Een omschakeling is geen tweede meting, en als ze dat wel was zou een locatie met pech stilletjes het aantal meetwaarden verdubbelen. Het plafond ligt op twee fysieke pogingen, want samen moeten ze binnen de deadline van de meetronde passen.
Heeft een locatie helemaal geen gezonde probe, dan wordt het slot toch geclaimd en wordt de bron vastgelegd als onbeschikbaar. Nooit als plat: dat een locatie van ons ontbreekt zegt niets over de machine van de klant, en het een storing noemen zou een vals alarm zijn dat door onze eigen infrastructuur is opgewekt.
Drie plaatsen die een privéadres weigeren
Een monitoringregel is een veld waarin iemand een bestemming noemt en waar elke minuut een machine van ons verbinding mee maakt. Zonder grenzen is dat een apparaat om verzoeken mee te vervalsen, met een factuuradres eraan. Het doel is een adrestoewijzing van de eigen server van de klant en geen vrije tekst, en daarmee is het grootste deel van het probleem weg; de rest is een lijst met reeksen die een gehoste probe niet mag aanraken, boven op de reeksen die overal dicht staan:
| Reeks | Waarom hij dicht staat |
|---|---|
10/8, 172.16/12, 192.168/16 | Privéruimte. Op een gehoste probe is dat het datacenter en niet de klant |
100.64/10 | Carrier grade NAT, waar providers loadbalancers en beheerpaden aan hangen |
192.0.0/24, 192.0.2/24, 198.18/15, 198.51.100/24, 203.0.113/24, 192.88.99/24 | Protocoltoewijzingen, benchmarking, documentatie, een afgeschafte relay-anycast. Geen ervan kan een klantmachine zijn, dus alles wat antwoordt zit dichterbij dan het doel |
fc00::/7, 2001:db8::/32 | De eerste en de derde rij hierboven, in IPv6: unieke lokale ruimte en documentatieruimte |
Gemapte IPv4-vormen worden eerst uitgepakt, anders heeft elke regel hierboven een schrijfwijze die eromheen loopt.
De controle draait op drie plaatsen en alle drie zijn nodig. Het platform weigert vóór het versturen, zodat een verkeerd doel nooit een machine bereikt die het zou kunnen uitvoeren. De probe weigert bij de uitvoering, want alleen daar is bekend waar een naam werkelijk naar oploste, en dat kan per keer verschillen. En het platform leest het gerapporteerde adres nog een keer wanneer het resultaat binnenkomt, waarmee een probe die ergens anders gemeten heeft een ongeldig antwoord wordt in plaats van een gegevenspunt. Een privébestemming is een configuratiefout en nooit een mislukking: er is niets gemeten, dus er valt niets over het doel te zeggen.
Wat we zouden opschrijven
- Laat de client nooit zijn eigen soort benoemen. Leid die af uit iets dat je hebt uitgegeven, en houd beide weigeringen gelijk zodat de foutmelding geen orakel is.
- Som het hele protocol op en niet het toegestane deel. Laat een test de gegenereerde code lezen en per veld een oordeel eisen, in beide richtingen, zodat een nieuwe berichtsoort een rode test oplevert en geen gat.
- Leg de echte grens waar hij niet vergeten kan worden. Een apart register zonder agent-identificatie betekent dat er geen route is, wat de tabel ook zegt.
- Controleer de payload net zo goed als de envelop. "Commando's zijn toegestaan" is niet dezelfde uitspraak als "dit commando is toegestaan".
- Houd de klok uit de toewijzingssleutel, en maak van een omschakeling een extra poging in plaats van een extra meetwaarde, zodat een verandering in de gegevens nooit aangezien kan worden voor een verandering in het meetinstrument.
Voor de duidelijkheid: gehoste monitoring zit achter een uitrolvlag die standaard uit staat, er ligt geen entitlementlaag onder, en niets hiervan is vandaag te koop. Wat onbewezen is verdient ook een naam. Het subcommando probe heeft nooit end-to-end gedraaid, omdat een van zijn netwerkonderdelen niet compileert op de machine waarop dit geschreven is; de locatiebeleidsregels zijn alleen in hun eenvoudigste vorm beproefd; IPv6 is nergens gemeten. De draad zelf is echt aangestuurd, van inschrijving via een sessie tot een controle die terugkwam, en een verboden bericht op die verbinding werd geweigerd.
Uit het monitoringwerk aan Provibr. Waarom een hostnaam bewezen moet zijn voordat hij een doel kan worden staat in hostnaameigendom bewijzen, en waarom een incident niet sluit alleen omdat de metingen ophielden staat in een incident dat nooit sluit op stilte.