Săriți la conținut

Blog ·

Un URL de webhook care este secretul

Un webhook de intrare poate fi propria credențială dacă URL-ul duce greutatea aceasta. Costul: 160 de biți, o semnătură peste ceas și un corp tăiat devreme.

De Provibr

Cineva vrea ca instrumentul său de monitorizare să pornească unul dintre fluxurile noastre de automatizare. Instrumentul acela știe să trimită JSON către un URL și nimic altceva: fără semnătură, fără antet propriu, fără certificat de client. Este un echipament într-un rack, configurat în 2019, și nu va mai învăța nimic nou.

Puteți refuza clientul acela sau puteți accepta că pentru unele surse însuși URL-ul este credențiala. Am acceptat și apoi am petrecut întreaga rundă asigurându-ne că URL-ul acela duce cu adevărat greutatea aceasta, în loc să fie doar lung.

Treizeci și două de caractere, niciodată derivate din ceva

Identificatorul din /api/hooks/flows/<slug> are treizeci și două de caractere dintr-un alfabet de treizeci și două de simboluri: 160 de biți de entropie brută. Nu este derivat din cont, din flux sau din numele punctului de intrare, fiindcă un identificator care se poate calcula din ceea ce un client a publicat deja se poate calcula de către oricine îi citește pagina de stare.

Alfabetul este base32 al lui Crockford: cifrele și literele mici fără i, l, o și u. Aceeași alegere ca la codurile noastre de activare pentru agenți, din același motiv. Șirul acesta se rescrie în panoul de administrare al altcuiva, iar niciun font nu face l și 1 de deosebit atunci când cineva este obosit.

Secretul se criptează, nu se rezumă

Un punct de intrare poate purta în plus un secret partajat, ca semnătură HMAC sau ca jeton bearer. Amândouă se păstrează criptate, iar asta merită o explicație, fiindcă instinctul clădit de două decenii de sfaturi despre parole este aici exact greșit.

O parolă se poate rezuma fiindcă trebuie răspuns doar dacă este același șir. Un secret HMAC nu: verificarea înseamnă calcularea semnăturii, iar pentru asta este nevoie de textul în clar. Intră așadar sub AES-256-GCM, cu o cheie derivată din secretul platformei și cu date autentificate care numesc punctul de intrare căruia îi aparține, astfel încât un text cifrat luat dintr-un rând să nu se verifice ca secret al altui rând. Este aceeași mecanică ce protejează deja cheile de plată și secretele scripturilor; un al doilea mecanism ar însemna o a doua poveste de rotație și un al doilea loc în care să greșești.

Dacă sursa aceea de cheie se schimbă vreodată, textul cifrat nu se deschide, iar cererea este refuzată în loc să fie acceptată; ecranul spune atunci că secretul trebuie regenerat. Să eșuezi în deschis în fața unui secret ilizibil este genul de rând pe care îl scrii o dată în grabă și îl regăsești trei ani mai târziu.

Clientul vede secretul o singură dată, la creare. Nu fiindcă nu l-am putea arăta din nou, ci fiindcă „notat acum în două locuri” și „vizibil pentru oricine are drepturi de automatizare” sunt riscuri diferite, iar numai primul este inevitabil.

Ceasul intră în materialul semnat

Semnătura este sha256=<hex> într-un antet, calculată peste marca de timp, un punct și corpul brut. Marca de timp călătorește într-un antet propriu și trebuie să cadă la mai puțin de cinci minute de ceasul nostru, de ambele părți.

Punerea mărcii de timp în materialul semnat, și nu alături de el, este întregul mecanism împotriva reluării. Dacă se semnează doar corpul, oricine a interceptat o cerere validă o poate relua la nesfârșit cu un antet de timp proaspăt, iar fereastra rămâne o podoabă. Contează și ambele margini: o verificare care refuză doar ce este prea vechi acceptă o semnătură datată 2099.

Comparația se face în timp constant, fiindcă o egalitate obișnuită pe text hexazecimal se oprește la primul caracter diferit, iar timpul acela ajunge pentru a ghici o semnătură octet cu octet. Iar marca de timp este judecată după ce semnătura se potrivește, niciodată înainte. Invers, „expirată” ar fi un oracol gratuit: cineva fără niciun secret valid ar afla că problema era ceasul său și nu semnătura sa. Deosebirea dintre o semnătură greșită și una expirată există, dar există în chitanța pe care o citește proprietarul punctului de intrare, nu în răspunsul dat apelantului.

Un corp tăiat în timp ce sosește

Plafonul corpului este de 64 KiO în mod implicit, iar ceea ce contează nu este cifra, ci locul în care se aplică.

Citirea întregii cereri într-un șir și abia apoi verificarea lungimii este o verificare de după pagubă: un corp de 500 MO ne costă 500 MO înainte de refuz, iar plafonul devine o invitație. Corpul este așadar citit pe bucăți din flux, totalul curent este comparat la fiecare bucată, iar cititorul este anulat de îndată ce trece peste. Octeții înregistrați sunt cei citiți cu adevărat, nu Content-Length declarat de apelant, fiindcă cel care ne atacă este tocmai acela al cărui antet nu se cuvine să fie crezut.

Frâna de debit stă după corp și nu înaintea lui, invers față de sfatul obișnuit, și așa trebuie: semnătura se calculează peste corp, deci o frână care răspunde mai devreme nu poate ști dacă cererea era legitimă. Numără pe punct de intrare și nu pe adresă de origine. O sursă reală de webhook are o adresă; cine atacă are o mie, așa că o frână pe adresă protejează exact partea greșită.

Nouă refuzuri, douăsprezece deznodăminte și niciun corp

Ordinea refuzurilor este proiectul. Întâi forma identificatorului, înainte de a atinge baza de date, fiindcă where slug = <un megaoctet de text> este muncă pe care un necunoscut nu ar trebui să ne-o poată impune. Apoi existența, revocarea, dezactivarea, lista de adrese, plafonul corpului, frâna de debit, semnătura și, la final, dacă JSON-ul este măcar un obiect.

Fiecare refuz de la al treilea pas încolo scrie o chitanță. Tabelul acela este răspunsul la „webhookul meu nu face nimic”, o frază care ascunde două probleme foarte diferite: nu sosește nimic sau sosește ceva și îl respingem. Fără un rând, cele două nu se pot deosebi, iar clientul își petrece după-amiaza cu problema greșită.

Chitanța poartă deznodământul ca cuvânt propriu, alături de starea HTTP, fiindcă pentru asta codurile de stare sunt o limbă slabă: un singur 403 acoperă și „adresa dumneavoastră nu este pe listă” și „acest punct de intrare este oprit”, iar cele două trimit un om la două ecrane diferite. Douăsprezece deznodăminte, doar prin adăugare, cu adresa de origine și octeții citiți.

Chitanța nu poartă niciodată corpul: este sarcina utilă a altcuiva, poate conține orice, iar o comoditate de depanare nu justifică un tabel care adună în tăcere date străine. Chitanțele se păstrează nouăzeci de zile, în mod deliberat mai puțin decât execuțiile cărora le aparțin, fiindcă o adresă de origine este o dată cu caracter personal a unui terț care nu a consimțit la nimic.

Un refuz nu scrie nimic: un identificator necunoscut. Nu există un cont de care să atârne rândul, iar un tabel care crește ori de câte ori un necunoscut ghicește este un mod gratuit de a ne umple baza de date.

De ce 410 și 404 nu sunt același răspuns

Revocarea unui punct de intrare nu îl șterge. Rândul rămâne, identificatorul rămâne ocupat pentru totdeauna, iar o cerere primește 410 în loc de 404. Numele redevine disponibil, fiindcă deosebirea a două puncte de intrare vii era tot rostul unicității aceleia, dar URL-ul nu se mai întoarce niciodată.

Alternativa ispititoare este să răspunzi 404 în ambele cazuri, ca nimeni să nu afle ce identificatori au existat. Ghicirea unuia înseamnă 160 de biți de muncă, deci cine are unul l-a primit, dintr-un rând de jurnal, dintr-o integrare veche sau dintr-o captură de ecran, și știe deja că a existat. Deosebirea nu îi spune nimic.

Jumătății cinstite a lumii îi spune destul de mult. O sursă care trimite mai departe către vechiul URL după o rotație vede din 410 că îi trebuie unul nou; în fața unui 404 conchide că cineva a scris greșit calea, iar atunci un om caută un defect în cale în loc de în rotație.

Primul declanșator fără server

Toate declanșatoarele anterioare lucrau pe o mașină existentă: aprovizionată, reinstalată, ștearsă. O execuție prin webhook nu are mașină, nici plan, nici adresă, nici integrare, nici agent și nici pe cineva autentificat.

Sună mărunt și a fost muchia cea mai ascuțită a rundei. Catalogul de variabile notează la fiecare grup declanșatoarele pentru care este disponibil, iar constanta care însemna „întotdeauna” era literalmente lista tuturor declanșatoarelor. O a cincea valoare ar fi oferit așadar unui flux prin webhook toate variabilele de server, plan, adresă, rețea și agent în selector; validatorul le-ar fi aprobat, iar prima cerere reală ar fi căzut la randare, într-o execuție la care nu se uită nimeni.

Există așadar acum o listă separată care răspunde la altă întrebare: lucrează declanșatorul acesta pe un server? Un al șaselea declanșator va trebui să răspundă mai întâi la ea. Ceea ce lipsește dintr-o execuție prin webhook este tăiat din context în loc să fie lăsat prezent și gol, iar un nod de acțiune într-un asemenea graf este refuzat la salvare.

Corpul aterizează pe un singur nivel, ca text, număr sau valoare logică. Câmpurile necunoscute sunt sărite în loc să fie refuzate, iar acesta este singurul loc în care o sarcină utilă de webhook diferă de una scrisă chiar de client: o sarcină scrisă de mână cu o greșeală de tastare trebuie să eșueze, dar mesajul unui terț conține valori nule, obiecte imbricate și liste fiindcă nu a fost scris niciodată pentru noi, iar refuzul întregii cereri pentru un câmp în plus face inutilizabilă orice sursă reală.

Ce am nota

  1. Dacă URL-ul este credențiala, se cuvine făcut din el o credențială. Aleatoriu, nederivat, imposibil de ghicit, revocabil, niciodată reemis.
  2. Un secret cu care se calculează nu se poate rezuma. Criptați-l, legați-l de proprietarul său și eșuați în închis când nu se deschide.
  3. Semnați ceasul, nu doar corpul. O marcă de timp din afara semnăturii este o apărare care nu apără nimic.
  4. Aplicați plafonul de mărime în timp ce sosesc octeții. O verificare de după citire a plătit deja prețul.
  5. Frânați ce a configurat clientul, nu ce controlează atacatorul.
  6. Jurnalizați refuzurile, nu sarcina utilă. Refuzurile fac din „nu se întâmplă nimic” o întrebare cu răspuns; sarcina utilă este dată străină.
  7. Dacă un caz nou nu are subiect, căutați orice listă care însemna „toate cazurile”. Una dintre ele este sinonimă cu enumerarea și nu o spune.

Pentru claritate: punctele de intrare, schemele, chitanțele și declanșatorul de webhook sunt construite și funcționează. Rămân nedovedite rezerva în memorie a frânei la o cădere reală a memoriei tampon, comportamentul după schimbarea secretului platformei și toate acestea pe domeniul propriu al unui client.

Din munca de platformă la Provibr. Cum se păstrează finit graful din spatele declanșatorului se află în un flux este un graf, iar un graf trebuie să fie finit, iar cum a căpătat pânza firul său se află în un fir pe care îl trageți și exact un declanșator.

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