Săriți la conținut

Blog ·

Un secret care călătorește o singură dată

O verificare de monitorizare are nevoie de o parolă la celălalt capăt. Iată cum ajunge la o sondă, pentru o încercare, fără să cadă vreodată într-un jurnal.

De Provibr

Unele verificări de monitorizare au nevoie de acreditări. Nu poți întreba un server PostgreSQL dacă este sănătos fără să te autentifici, iar o instanță Redis cu parolă nu răspunde nimic folositor până nu i-ai trimis-o. O regulă de monitorizare trebuie așadar să ducă un secret de la noi până la sonda sau agentul care va prelua din întâmplare următoarea încercare.

Versiunea ieftină a acestui lucru este o coloană. Pui parola în regulă, o citești la trimitere, o trimiți cu ea. Acea versiune este greșită într-un fel care devine vizibil abia treisprezece luni mai târziu, iar aceasta este forma pe care am construit-o în locul ei.

Cele două căi care existau deja și de ce niciuna nu se potrivește

O comandă de monitorizare ajunge la o sursă prin două locuri, iar amândouă sunt nepotrivite.

Tabelul de revizii al regulii este imuabil prin proiectare, ceea ce este exact potrivit pentru o revizie și exact nepotrivit pentru o acreditare: nimeni nu poate roti o parolă care stă într-un rând ce nu se poate schimba fără a produce o revizie nouă a fiecărei reguli care a folosit-o.

Rândul comenzii este mai rău, fiindcă este un rând de audit și trece prin magistrala de mesaje în drumul lui spre exterior. Propriile noastre note de proiect spun de un an că un rând de audit al unei comenzi conține nume de câmpuri și niciodată valori. O parolă care aterizează acolo o dată rămâne pe toată perioada de păstrare, lizibilă pentru oricine are acces la baza de date și copiată în salvarea care a rulat în noaptea aceea.

Mai există și un canal lateral trecător tocmai pentru asemenea cazuri, folosit când cineva comandă o mașină și o parolă de administrare trebuie să ajungă la agent fără a fi stocată. Nici acela nu se potrivește aici. Acel canal există pentru secrete pe care creatorul unei comenzi le cunoaște deja, iar aici creatorul este un planificator care rulează cu minute înainte și încă nu știe ce sursă va lua sarcina.

De aceea cele patru mesaje de protocol adăugate în această rundă nu au câmp de utilizator și nici câmp de parolă. Nu este vorba că acele câmpuri sunt nefolosite: ele nu există.

Sigilat în clipa trimiterii

Plicul este creat în procesul de sesiune, chiar înainte ca mesajul să fie construit, o dată pentru fiecare încercare. Aceasta nu este ordine de dragul ordinii. Este singurul punct din sistem în care se cunosc în același timp cele patru lucruri pe care trebuie să le lege: ce sursă se află la celălalt capăt al acestei sesiuni, a câta încercare este, cărei revizii a regulii îi aparține și când expiră.

Urmarea este că el nu poate fi nicăieri altundeva, iar asta se verifică în loc să se afirme. Un test plantează o parolă cunoscută, parcurge drumul adevărat și o caută în încărcătura comenzii, în rândurile de evenimente, în configurația de verificare salvată, în magistrala de mesaje, în octeții bruți ai protocolului atât în UTF-8, cât și în base64, și în jurnalul porții. Cere zero apariții în toate cele șase. O listă de afirmații despre unde nu ajunge un secret nu valorează nimic; o căutare valorează ceva.

Textul în clar există cât ține o expresie. O funcție îl produce, următoarea îl consumă, iar apoi nu mai există nicio referință. JavaScript nu poate șterge un tampon, așa că singura opțiune este să nu păstrezi niciunul. La celălalt capăt, în Rust, ajunge într-un tip care se pune singur pe zero la eliberare.

Construcția

O pereche de chei X25519 efemeră pentru fiecare încercare, un Diffie-Hellman față de cheia publică a exact unei surse, HKDF-SHA256 pentru derivarea cheii de cifrare și AES-256-GCM peste textul în clar. Pe fir trec cheia publică efemeră, plus un număr de unică folosință, criptograma și eticheta.

Cele două chei publice intră în sarea funcției HKDF, astfel încât cheia derivată să fie legată de acea pereche și nu de un secret comun care în principiu ar putea apărea din altă pereche. Aceeași idee ca planul de chei al unei scheme hibride obișnuite, cu simplificarea cinstită aici: există o cheie efemeră proaspătă pentru fiecare încercare, deci nicio cheie nu este folosită de două ori.

Patru câmpuri intră în datele autentificate suplimentare:

CâmpCe împiedică
Identificatorul încercăriiCa aceeași sursă să refolosească un plic pentru o măsurare ulterioară
Identificatorul revizieiCa un plic să fie lipit sub altă verificare
TermenulCa un plic scos dintr-o captură sau dintr-o descărcare de memorie să mai fie bun mai târziu
ScopulCa o acreditare PostgreSQL să fie dată unei verificări Redis

Identitatea sursei nu apare în acea listă fiindcă nu trebuie: sigiliul este făcut către o singură cheie publică, deci altă sursă nu îl deschide deloc.

Prefixe de lungime, fiindcă un separator este ambiguu

Datele suplimentare sunt construite cu lungimea fiecărui câmp în față, nu cu un separator între câmpuri. Sună a pedanterie până când scrii alternativa.

Cu a:b ca formă, perechea ("x", "y:z") și perechea ("x:y", "z") produc octeți identici. Legătura pe care aceste date trebuie să o stabilească nu mai deosebește atunci două situații diferite, iar cine controlează o parte dintr-un câmp poate deplasa granița. Patru câmpuri cu patru octeți de lungime în față nu au acea problemă. Ambele părți le construiesc octet cu octet la fel, cu un vector de test comun, astfel încât o abatere este un test roșu și nu un plic pe care nimeni nu îl mai deschide.

De ce nu cifrul pe care îl folosim deja

Seiful de acreditări al agentului folosește XChaCha20-Poly1305, iar aceeași construcție aici ar da un singur răspuns la întrebarea cum cifrăm un secret în loc de două. Am măsurat, și nu se poate:

text
node v24.20.0   crypto.getCiphers() conține "chacha20-poly1305"
bun  1.4.0      crypto.getCiphers() întoarce 28 de nume, niciunul chacha
                createCipheriv("chacha20-poly1305", ...) -> "Unknown cipher"

Producția rulează pe Node, deci acolo ar funcționa. Dar bancul de test al acestui depozit este bun, iar o cifrare autentificată pe care bancul nu o poate executa este o cifrare al cărei dus-întors nu se dovedește niciodată. Singurul control asupra plicului ar trăi atunci într-un test sărit, iar aceasta este exact aroma de măsurătoare verde în gol pe care ne străduim mult să o evităm.

Alternativa era să scriem de mână piesa lipsă. S-a și făcut, a funcționat față de vectorul de test publicat, iar apoi a fost scoasă. Codul criptografic care există doar ca să ocolească un banc de test nu are ce căuta într-un depozit de produs.

Deci AES-256-GCM, aceeași cifrare autentificată care protejează deja acest secret în repaus, prezentă în ambele medii de execuție și deja în arborele de dependențe al agentului. Numărul de unică folosință de 96 de biți din cauza căruia se apelează de obicei la XChaCha nu este o piedică aici, fiindcă rostul lui XChaCha este să poți alege acele numere la întâmplare sub o cheie de viață lungă. Nu există nicio cheie de viață lungă. Fiecare plic are una efemeră proaspătă, deci fiecare cheie servește exact o dată, iar o repetare nu ar fi un al doilea mesaj sub aceeași cheie.

Unde își ține sursa jumătatea ei

Cheia privată stă lângă identitatea agentului sau a sondei, într-un fișier de treizeci și doi de octeți bruți cu modul 0600, alături de cheia privată a certificatului de client. Nu în seiful de acreditări, iar cele trei motive merită scrise fiindcă ele au fost hotărârea adevărată.

Revocarea o șterge gratis: revocarea elimină deja acel dosar, iar o cheie păstrată altundeva ar avea nevoie de propria regulă de curățare, pe care prima rundă care o uită ar lăsa-o pe o gazdă revocată. Ea aparține unei singure înscrieri, iar platforma păstrează jumătatea publică lângă certificat, deci o înscriere nouă ar trebui să producă o pereche nouă, ceea ce se și întâmplă fiindcă dosarul este atunci gol. Iar seiful nu este mereu disponibil: se deschide cu material scos din fișierul de licență, iar o sondă de monitorizare nu are licență și deci nici seif. A pune cheia acolo ar impune un al doilea mecanism pentru sonde, și atunci ar exista două răspunsuri la o întrebare.

Nici nu este ținută în memorie, și este intenționat, oricât de ispititor ar fi pentru o sondă care nu stochează nimic. Platforma păstrează jumătatea publică, deci după o repornire aceasta nu ar mai corespunde, iar orice plic ar rămâne închis până când cineva ar înscrie sonda din nou. Un punct de monitorizare care după o repornire încetează în tăcere să facă verificări autentificate este exact felul de defecțiune pe care nu o observă nimeni.

Niciodată o soluție de rezervă

Dacă o sursă nu are cheie, verificarea nu rulează. Ea raportează o eroare de configurare care numește intermediarul ca indisponibil, iar acesta este tot comportamentul. Nu există rezervă în clar și, mai important, nu există o stare de cădere înșelătoare pentru țintă, fiindcă o verificare ce nu se poate măsura nu trebuie să arate niciodată ca un serviciu stricat. Aceea este o alarmă care trimite pe cineva la mașina greșită la trei dimineața.

Ce am nota

  1. Un rând de audit nu este un canal de livrare. Ce se scrie acolo rămâne toată perioada de păstrare, în fiecare salvare.
  2. Nu adăugați câmpul. Un mesaj de protocol fără câmp de parolă nu poate scăpa vreuna pe un drum de cod pe care cineva uită să îl recitească.
  3. Sigilați acolo unde legăturile sunt cunoscute. Dacă cel care creează o sarcină încă nu știe cine o va executa, secretul nu poate lua naștere odată cu sarcina.
  4. Dovediți absența căutând, nu afirmând. Plantați o valoare și vânați-o în fiecare serializare, base64 inclusă.
  5. Puneți prefixe de lungime pe datele voastre autentificate. Un separator face ca două perechi diferite să dea aceiași octeți.
  6. Alegeți primitiva pe care bancul vostru de test o poate executa. O construcție mai puternică niciodată exersată este în practică mai slabă decât una bună care este.
  7. O cheie care este o identitate aparține identității. Atunci revocarea o curăță, iar o înscriere nouă o înlocuiește, fără o regulă pe care cineva trebuie să o țină minte.

Pentru claritate despre ce este vorba: monitorizarea se află în spatele unui comutator de lansare oprit implicit, deci nimic din toate acestea nu se poate cumpăra astăzi. Două lucruri nu sunt dovedite. Schimbul de autentificare bazat pe parolă a fost măsurat doar față de implementarea de referință a bibliotecii de protocol, nu față de un server adevărat. Iar binarul sondei scris în Rust nu a fost exersat pe fir în această rundă.

Din munca de platformă a Provibr. De ce o sondă tratează ca nesigur fiecare mesaj pe care îl primește se află în o sondă pe care nu o poți convinge de nimic, iar de ce acreditările panourilor proprii ale unui client nu ajung niciodată la noi se află în unde trebuie să stea acreditările de panou.

Începeți cu o gazdă și un agent

Creați un cont, instalați agentul cu o singură comandă și conectați primul dumneavoastră host Proxmox. Perioada de probă durează 14 zile și nu cere date de plată.