Săriți la conținut

Blog ·

Cum se refuză singură o actualizare semnată

O sumă de control dovedește că descărcarea a ajuns întreagă, nu că ați trimis-o. Cât costă semnarea unui agent care se actualizează singur.

De Provibr

Orice program care se actualizează singur este execuție de cod la distanță pe care ați invitat-o înăuntru. Acesta nu este un argument împotriva actualizărilor automate, care sunt singura cale realistă de a împiedica o flotă de agenți aflați pe hardware-ul clienților să se degradeze. Este un argument pentru a fi precis în privința a ceea ce promite mecanismul de actualizare, pentru că cele mai multe promit mai puțin decât lasă să se înțeleagă documentația lor.

Noi rulăm un agent pe serverele altora. El vorbește cu platforma noastră, aplică comenzi pe hipervizorul lor și își înlocuiește propriul binar când livrăm o versiune nouă. Iată ce a trebuit să construim înainte să fim dispuși să îl lăsăm să facă asta.

O sumă de control răspunde la întrebarea greșită

Aproape orice unealtă care se actualizează singură publică un SHA-256 lângă fișierul de descărcat, iar multe se opresc acolo. O sumă de control este utilă și nu face treaba pe care oamenii cred că o face.

MăsuraCe dovedește de faptCe nu poate vedea
TLS pe descărcareOcteții au venit de la acea gazdă, pe acea conexiuneNimic despre cine i-a produs
SHA-256 publicat lângă fișierDescărcarea nu este trunchiată și nu este coruptăCine poate schimba fișierul poate schimba și numărul
Semnătură peste octețiAcești octeți au fost produși de deținătorul unei anume chei privateDacă acea cheie vă mai aparține

Rândul din mijloc este toată problema. Suma de control și artefactul stau de obicei în același loc și de obicei pot fi scrise de același proces. Dacă un atacator poate înlocui una, o poate înlocui și pe cealaltă, iar verificarea va trece cu entuziasm.

Așa că suma de control este o probă ieftină făcută înainte de decolare și explicit nu granița de încredere. Ea prinde o descărcare trunchiată dintr-o singură trecere peste octeți pe care oricum îi transmiteam. Ancora este semnătura.

Unde trebuie să stea cheia publică

Iată prima decizie care hotărăște dacă ceva din toate acestea este real: cheia de verificare nu poate fi un fișier livrat alături de binar, pentru că atunci compromiterea traseului de livrare compromite și cheia.

Agentul nostru primește cheia publică Ed25519 a platformei compilată în el, dintr-o variabilă de mediu de la momentul construirii. Într-o versiune de lansare nu există altă sursă. Într-o versiune de depanare, un dezvoltator o poate îndrepta către o cheie locală printr-o variabilă de mediu, iar acel traseu este scos complet la compilare printr-un atribut cfg, pentru că o cheie care poate fi înlocuită în timpul rulării nu este o ancoră, ci o sugestie.

Două amănunte din jurul acestui lucru contează mai mult decât par.

Scriptul de construire declară că trebuie să ruleze din nou atunci când acea variabilă de mediu se schimbă. Fără acea linie, o versiune de lansare care împarte o memorie de construire cu una mai veche refolosește voios cheia din compilarea precedentă, iar nimeni nu observă până când ajunge la un client.

Iar analizorul de chei este strict în privința codificării, nu a lungimii. O cheie publică Ed25519 în formă SPKI este un prefix fix de 12 octeți urmat de 32 de octeți; o cheie X25519 are tot 32 de octeți, cu un alt identificator de algoritm. Acceptând orice lucru lung de 32 de octeți, ați lua o cheie pentru un alt algoritm și ați eșua mai târziu, undeva unde nu are niciun sens.

Refuzați înainte să descărcați

Când platforma îi spune agentului să se actualizeze, agentul parcurge toată lista de precondiții înainte să aducă un singur octet:

  1. Există în instrucțiune un SHA-256 utilizabil?
  2. Există o adresă de descărcare?
  3. Există o semnătură și are exact 64 de octeți?
  4. Rulăm deja această versiune?
  5. Există pe această gazdă scriptul privilegiat de aplicare?
  6. Există fișierul local cu ancora de încredere?
  7. Are în general acest binar o cheie a platformei compilată în el?

Pasul al treilea merită atenție. O platformă mai veche, care nu completează câmpul semnăturii, trebuie să eșueze aici, zgomotos, în loc să cadă în tăcere înapoi pe verificarea mai slabă pe care o făceam odinioară. Verificarea opțională nu este verificare. Transformarea câmpului lipsă într-o eroare fatală este diferența dintre un mecanism și un obicei.

Și ordinea ultimelor trei este deliberată. Mesajele de tipul „gazdei dumneavoastră îi lipsește pasul privilegiat, rulați din nou programul de instalare” sunt mai ușor de urmat decât „acest binar nu are cheia platformei”, așa că vin primele.

Semnați octeții, nu o descriere a lor

Semnătura acoperă artefactul exact așa cum ajunge pe disc. Ed25519 pur, fără prehașare, fără plic canonic, fără metadate incluse. Motivul nu este puritatea. Motivul este că două implementări cu totul diferite trebuie să ajungă la același verdict.

Pe agent este ed25519-dalek cu verify_strict, care refuză semnăturile ce implică componente de torsiune. Cu o cheie și un fișier ar trebui să existe exact o singură semnătură validă, iar varianta nestrictă nu garantează asta.

Pe gazdă, scriptul privilegiat care instalează efectiv actualizarea verifică aceiași octeți cu openssl pkeyutl -verify -pubin -rawin. Aceeași cheie, aceiași octeți, același răspuns. Dacă am fi prehașat sau dacă am fi semnat un manifest în locul artefactului, verificarea din interpretorul de comenzi ar trebui să reimplementeze împachetarea noastră, iar cele două verificări s-ar îndepărta una de alta.

Aceasta este și verificarea pe care o facem de mână pe server după fiecare lansare, față de cheia publică de producție, înainte să credem că o lansare chiar s-a întâmplat.

Manifestul de lângă artefact poartă versiunea, identificatorul de construire, suma de control, dimensiunea, momentul lansării, felul artefactului și semnătura. Sunt metadate pentru panou și nimic din ele nu este de încredere: semnătura din el este peste binar, nu peste manifest.

Agentul nu își poate instala singur actualizarea

Agentul rulează ca utilizator neprivilegiat. Nu poate scrie în directorul în care stă propriul lui binar și nici nu ar trebui să poată.

Așa că pregătește. Scrie mai întâi semnătura și suma de control într-un director de pregătire și abia apoi mută binarul verificat la locul lui, lângă ele, pentru că scriptul privilegiat folosește prezența acelui binar drept semnal de pornire, iar celelalte două fișiere trebuie să fie deja acolo. Apoi iese curat, iar administratorul de servicii îl repornește, rulând mai întâi un pas privilegiat de dinaintea pornirii.

Acel script merită descris, pentru că presupunerile lui sunt opusul a ceea ce v-ați aștepta.

Iese întotdeauna cu zero. Un pas eșuat dinaintea pornirii ar împiedica serviciul să pornească în general, iar o actualizare defectă care scoate un client din funcțiune este mult mai rea decât o actualizare defectă care pur și simplu nu se aplică.

Directorul de pregătire nu este de încredere. Tot ce se află în el a fost scris de utilizatorul neprivilegiat al agentului: binarul, suma de control, semnătura. Cine preia acel utilizator le poate înlocui pe toate trei. De aceea semnătura, verificată cu cheia aflată de partea privilegiată, este singurul lucru care decide.

Copiază înainte să verifice. Octeții pregătiți sunt copiați exact o dată într-un fișier temporar nou, deținut de root, aflat lângă destinație, iar fiecare verificare de după aceea citește copia. Fișierul care este verificat este exact acel inod care este mutat la locul lui, deci nu există nicio fereastră în care un alt fișier să fie strecurat între verificare și instalare.

Restul este deliberat plictisitor: refuzarea legăturilor simbolice, cerința ca suma de control să aibă exact 64 de caractere hexazecimale, cerința ca fișierul să înceapă cu numărul magic ELF, lărgirea permisiunilor la executabil abia după ce trece semnătura, pornirea lui o dată ca utilizator al serviciului și nu ca root și abia apoi redenumirea lui peste binarul viu. O semnătură spune de unde vin octeții. Nu spune că merită root.

Lansarea pe care am semnat-o cu cheia greșită

Mecanismul de mai sus funcționează. Cu toate acestea am livrat o lansare defectă, iar felul în care a eșuat este partea folositoare.

Scriptul nostru de construire primea un indicator pentru „aceasta este o lansare de producție”. La acel moment indicatorul guverna doar publicarea, nu și ce cheie se folosea la semnare. O lansare construită fără a numi și cheia de producție de pe server a fost semnată cu cheia de dezvoltare, avea compilată în ea cheia de licență de dezvoltare și apoi a fost verificată local față de aceeași cheie de dezvoltare. Verificarea a spus că artefactul este în regulă, pentru că era coerent cu el însuși. Doar că nu era al nostru.

Fiecare agent real a refuzat actualizarea, pe bună dreptate, pentru că semnătura nu se potrivea cu cheia compilată în el. Mecanismul a făcut exact lucrul pentru care fusese construit. Greșeala era în amonte de el.

Din asta au ieșit două măsuri, amândouă în cod și nu într-un document. Construirea pentru producție fără a numi cheia de producție este acum o eroare fatală care oprește scriptul. Iar cheia de semnare și cheia de verificare trebuie să fie pe aceeași mașină: verificarea unei semnături cu o cheie adusă din altă parte nu dovedește absolut nimic.

Lecția generală merită spusă pe șleau. Un pas de verificare căruia i se dau ambele jumătăți ale unei perechi potrivite din mediul greșit va trece, și va trece cu încredere. Schemele de semnătură vă apără de atacatori. Nu vă apără de dumneavoastră.

O licență nu este un binar, iar amândouă sunt semnate

Semnăm două lucruri diferite cu aceeași cheie: binare de agent și fișiere de licență. Acesta este un pericol, pentru că o semnătură peste unul nu trebuie să fie acceptată niciodată drept semnătură peste celălalt.

Separarea este structurală, nu o convenție de denumire. Un fișier de licență semnează un plic de text construit din identificatorul de format, sarea, nonce-ul și textul cifrat, unite prin caractere de linie nouă. Un binar semnează octeți ELF bruți. Scriptul de instalare caută numărul magic ELF înainte să verifice orice, așa că o semnătură validă de licență nu poate fi niciodată prezentată drept semnătură validă de binar.

Traseul licenței mai are o proprietate care merită furată: verifică semnătura înainte să derive cheia de decriptare. Argon2id cu un cost de memorie de 64 MiB este scump prin proiectare, iar depunerea acelei munci pentru un fișier care nu a fost niciodată al nostru este un refuz de serviciu oferit gratis. Verificările ieftine de autenticitate stau înaintea criptografiei scumpe.

Ce nu vă cumpără asta

Semnarea leagă un artefact de o cheie. Nu spune nimic despre faptul că acea cheie vă mai aparține și nimic despre faptul că versiunea pe care ați semnat-o ar fi bună. Dacă ne scapă cheia de semnare, fiecare agent acceptă ce construiește atacatorul, iar rotirea ei înseamnă reconstruirea fiecărui binar și înlocuirea ancorei de încredere pe fiecare gazdă, pentru că acea cheie este compilată înăuntru. Acesta este prețul faptului că cheia este o ancoră și nu o sugestie, este compromisul corect, dar este un cost real și își are locul în plan, nu într-o notă de subsol.

Așa funcționează actualizarea agentului la Provibr, unde agentul rulează pe hardware-ul propriu al clientului, se conectează numai spre exterior și ține datele de autentificare pentru hipervizor pe acea gazdă.

Î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ă.