Blog ·
Een geheim dat één keer reist
Een monitoringcheck heeft aan de overkant een wachtwoord nodig. Zo bereikt het één probe, voor één poging, zonder ooit in een wachtrij of log te belanden.
Door Provibr
Sommige monitoringchecks hebben inloggegevens nodig. Je kunt een PostgreSQL-server niet vragen of hij gezond is zonder in te loggen, en een Redis-instantie met een wachtwoord antwoordt niets bruikbaars totdat je het gestuurd hebt. Een monitoringregel moet dus een geheim van ons naar de probe of agent brengen die de volgende poging toevallig doet.
De goedkope versie hiervan is een kolom. Zet het wachtwoord in de regel, lees het bij het verzenden, stuur het mee. Die versie is fout op een manier die pas dertien maanden later zichtbaar wordt, en dit is de vorm die wij in plaats daarvan gebouwd hebben.
De twee wegen die al bestonden, en waarom geen van beide werkt
Een monitoringcommando bereikt een bron langs twee plekken, en allebei zijn ze ongeschikt.
De revisietabel van de regel is met opzet onveranderlijk, en dat is precies goed voor een revisie en precies fout voor een inloggegeven: niemand kan een wachtwoord roteren dat in een rij staat die niet kan veranderen zonder van elke regel die hem gebruikte een nieuwe revisie te maken.
De commandorij is erger, want dat is een auditrij en hij reist onderweg naar buiten over de berichtenbus. Onze eigen projectnotities zeggen al een jaar dat een commando-auditrij veldnamen bevat en nooit waarden. Een wachtwoord dat daar één keer in belandt staat er de hele bewaartermijn, leesbaar voor iedereen met databasetoegang, en gekopieerd naar elke backup die die nacht draaide.
Er bestaat ook een transient zijkanaal voor precies dit soort dingen, gebruikt wanneer iemand een machine bestelt en een rootwachtwoord de agent moet bereiken zonder opgeslagen te worden. Dat past hier ook niet. Dat kanaal bestaat voor geheimen die de maker van een commando al kent, en hier is de maker een planner die minuten eerder draait en nog niet weet welke bron de klus neemt.
De vier protocolberichten die deze ronde erbij kwamen hebben daarom geen gebruikersnaamveld en geen wachtwoordveld. Het is niet dat die velden ongebruikt zijn; ze bestaan niet.
Verzegeld op het moment van verzenden
De envelope wordt gemaakt in de sessieworker, vlak voordat het bericht gebouwd wordt, één keer per poging. Dat is geen netheid. Het is de enige plek in het systeem waar alle vier de dingen tegelijk bekend zijn die hij moet binden: welke bron aan de andere kant van deze sessie zit, welke poging dit is, bij welke revisie van de regel hij hoort, en wanneer hij verloopt.
Het gevolg is dat hij nergens anders kán zijn, en dat is toetsbaar in plaats van beweerbaar. Een test plant een bekend wachtwoord, draait het echte pad, en zoekt ernaar in de commandopayload, in de gebeurtenisrijen, in de opgeslagen checkconfiguratie, in de berichtenbus, in de rauwe protocolbytes zowel als UTF-8 als als base64, en in de gatewaylog. Hij eist nul treffers in alle zes. Een lijst met beweringen over waar een geheim niet komt is niets waard; een zoektocht is iets waard.
De platte tekst bestaat één uitdrukking lang. De ene functie levert hem, de volgende verbruikt hem, en daarna is er geen verwijzing meer. JavaScript kan geen buffer wissen, dus het enige beschikbare is er nooit een bewaren. Aan de andere kant, in Rust, gaat hij in een type dat zichzelf nulmaakt zodra het opgeruimd wordt.
De constructie
Een ephemeer X25519-sleutelpaar per poging, een Diffie-Hellman tegen de publieke sleutel van precies één bron, HKDF-SHA256 om de versleutelsleutel af te leiden, en AES-256-GCM over de platte tekst. Over de lijn gaan de ephemere publieke sleutel plus een nonce, ciphertext en tag.
De twee publieke sleutels gaan in het zout van de HKDF, zodat de afgeleide sleutel aan dat paar gebonden is en niet aan een gedeeld geheim dat in principe uit een ander paar kan ontstaan. Dezelfde gedachte als het sleutelschema van een gangbaar hybride versleutelingsschema, met de vereenvoudiging die hier eerlijk is: er is per poging een verse ephemere sleutel, dus geen enkele sleutel wordt twee keer gebruikt.
Vier velden gaan in de aanvullende geauthenticeerde gegevens:
| Veld | Wat het tegenhoudt |
|---|---|
| Poging-id | Dezelfde bron die een envelope hergebruikt voor een latere meting |
| Regelrevisie-id | Een envelope die onder een andere check geplakt wordt |
| Deadline | Een envelope uit een pakketopname of een core dump die later nog bruikbaar is |
| Doeltype | Een PostgreSQL-inloggegeven dat aan een Redis-check gevoerd wordt |
De identiteit van de bron staat niet in die lijst omdat dat niet hoeft: er wordt naar één publieke sleutel verzegeld, dus een andere bron krijgt hem sowieso niet open.
Lengteprefixen, want een scheidingsteken is dubbelzinnig
De aanvullende gegevens worden gebouwd met de lengte van elk veld ervoor in plaats van met een scheidingsteken ertussen. Dat leest als muggenzifterij tot je het alternatief uitschrijft.
Met a:b als vorm leveren het paar ("x", "y:z") en het paar ("x:y", "z") identieke bytes op. De binding die deze gegevens moeten leggen onderscheidt dan twee verschillende situaties niet, en iemand die een deel van één veld beheerst kan de grens verschuiven. Vier velden met een lengte van vier bytes ervoor kennen dat probleem niet. Beide kanten bouwen hem byte voor byte hetzelfde, met een gedeelde testvector zodat een verschil een rode test is en geen envelope die niemand meer open krijgt.
Waarom niet de cipher die wij al gebruiken
De credentialvault van de agent gebruikt XChaCha20-Poly1305, en dezelfde constructie hier zou één antwoord opleveren op de vraag "hoe versleutelen wij een geheim" in plaats van twee. Wij hebben gemeten, en het gaat niet:
node v24.20.0 crypto.getCiphers() bevat "chacha20-poly1305"
bun 1.4.0 crypto.getCiphers() geeft 28 namen, geen chacha
createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"Productie draait op Node, dus daar zou het werken. Maar de testharnas van deze repository is bun, en een AEAD die de harnas niet kan uitvoeren is een AEAD waarvan de heen-en-weer nooit bewezen wordt. De enige controle op de envelope zou dan in een test staan die overgeslagen wordt, en dat is precies de smaak vacuüm groene meting waar wij veel moeite in steken om hem te vermijden.
Het alternatief was het ontbrekende stuk met de hand erbij schrijven. Dat is werkelijk gedaan, en het werkte tegen de gepubliceerde testvector, en daarna is het weggehaald. Cryptografische code die alleen bestaat om een testharnas te omzeilen hoort niet in een productrepository.
Dus AES-256-GCM, dezelfde AEAD die dit geheim al in rust versleutelt, aanwezig in beide runtimes, en al in de dependencyboom van de agent. De nonce van 96 bits waarom mensen naar XChaCha grijpen is hier geen bezwaar, want de reden dat XChaCha bestaat is dat je nonces willekeurig mag kiezen onder een sleutel die lang meegaat. Er is geen langlevende sleutel. Elke envelope heeft een verse ephemere, dus elke AEAD-sleutel wordt precies één keer gebruikt en een herhaalde nonce zou geen tweede bericht onder dezelfde sleutel zijn.
Waar de bron zijn helft bewaart
De private sleutel staat naast de identiteit van de agent of de probe, in een bestand van tweeëndertig rauwe bytes met modus 0600, naast de private sleutel van het clientcertificaat. Niet in de credentialvault, en de drie redenen zijn het opschrijven waard omdat zij de eigenlijke beslissing waren.
Intrekken wist hem gratis: het intrekken verwijdert die map al, en een sleutel die ergens anders staat zou een eigen opruimregel nodig hebben, die de eerste ronde die hem vergeet op een ingetrokken host achterlaat. Hij hoort bij één inschrijving, en het platform bewaart de publieke helft naast het certificaat, dus een nieuwe inschrijving hoort een nieuw paar op te leveren, wat hij doet omdat de map dan leeg is. En de vault is niet altijd beschikbaar: hij wordt ontgrendeld met materiaal dat uit het licentiebestand komt, en een monitoringprobe heeft geen licentie en dus geen vault. De sleutel daar zetten zou een tweede mechanisme voor probes afdwingen, en dan bestaan er twee antwoorden op één vraag.
Hij wordt met opzet ook niet in het geheugen gehouden, wat verleidelijk is voor een probe die niets opslaat. Het platform bewaart de publieke helft, dus na een herstart zou die niet meer kloppen en zou elke envelope onopenbaar zijn tot een beheerder opnieuw inschrijft. Een monitoringlocatie die na een herstart stil geen geauthenticeerde checks meer doet is precies het soort storing dat niemand ziet.
Nooit een val-terug
Heeft een bron geen sleutel, dan draait de check niet. Hij meldt een configuratiefout die de broker als onbeschikbaar noemt, en dat is het hele gedrag. Er is geen val-terug naar platte tekst en, belangrijker, geen misleidende downstatus voor het doel, want een check die niet gemeten kan worden mag er nooit uitzien als een kapotte dienst. Dat is een alarm dat iemand om drie uur 's nachts naar de verkeerde machine stuurt.
Wat wij zouden opschrijven
- Een auditrij is geen afleverkanaal. Wat daarin geschreven wordt staat er de hele bewaartermijn, in elke backup.
- Voeg het veld niet toe. Een protocolbericht zonder wachtwoordveld kan er geen lekken langs een codepad dat iemand vergeet na te kijken.
- Verzegel waar de bindingen bekend zijn. Weet de maker van een taak nog niet wie hem uitvoert, dan kan het geheim niet met de taak mee gemaakt worden.
- Bewijs afwezigheid door te zoeken, niet door te beweren. Plant een waarde en jaag erop in elke serialisatie, base64 inbegrepen.
- Zet lengteprefixen op je geauthenticeerde gegevens. Een scheidingsteken laat twee verschillende tupels dezelfde bytes opleveren.
- Kies de primitieve die je testharnas kan uitvoeren. Een sterkere constructie die nooit gedraaid wordt is in de praktijk zwakker dan een goede die dat wel wordt.
- Een sleutel die een identiteit is hoort bij de identiteit. Dan ruimt het intrekken hem op en vervangt een nieuwe inschrijving hem, zonder een regel die iemand moet onthouden.
Voor de duidelijkheid over wat dit is: monitoring zit achter een rolloutschakelaar die standaard uitstaat, dus hiervan is vandaag niets te koop. Twee dingen zijn onbewezen. De wachtwoordgebaseerde authenticatiewisseling is alleen gemeten tegen de referentie-implementatie van de protocolbibliotheek en niet tegen een echte server. En de Rust-probe-binary is deze ronde niet over de lijn gedraaid.
Uit het platformwerk van Provibr. Waarom een probe elk bericht dat hij ontvangt als onvertrouwd behandelt staat in een probe die zich nergens toe laat overhalen, en waarom de inloggegevens van de eigen panelen van een klant ons helemaal niet bereiken staat in waar paneelwachtwoorden thuishoren.