Sari la conținut
Danube Labs

Chei și secrete în dezvoltare: accidente, nu atacuri

Majoritatea cheilor scurse nu au fost ascunse bine: au fost commitate, lipite, logate sau trimise în browser. Regulile practice care le țin afară și cum rotești o cheie care a scăpat.

De Alexandru Secureanu8 min de citit
Un fișier cu chei ieșind din repository spre un chat, un log și bundle-ul din browser

Aproape nicio cheie scursă pe care am văzut-o nu a fost atacată. A fost commitată într-un repository, lipită într-o conversație ca să deblocheze un coleg, tipărită într-un log în timpul unei depanări sau inclusă în bundle-ul JavaScript pentru că numele variabilei părea inofensiv. Nu e nevoie de nicio pricepere ca să găsești așa ceva, iar o cheie scursă nu se anunță singură până când cineva o folosește.

Ce urmează este setul de reguli pe care un dezvoltator le poate ține fără o echipă de securitate. Sunt plictisitoare intenționat. Rostul lor este să reziste și când ai termen.

O scurgere nu este același lucru cu un incident. O cheie commitată într-un repository privat și ștearsă douăzeci de minute mai târziu are o fereastră scurtă; aceeași cheie lăsată un an într-un repository public este altă problemă. Dimensionează reacția după fereastră, nu după panică.

Pe unde se scurg, de fapt, credențialele

Patru canale acoperă majoritatea incidentelor:

  • Controlul de versiuni. Un fișier .env, un șir scris direct într-un modul de configurare, o fixtură de test cu o cheie reală, un notebook commitat în repository.
  • Conversația. O cheie lipită într-un chat, într-un issue, într-un tichet, în descrierea unui pull request, într-o captură de ecran cu panoul de administrare.
  • Ieșirea. O linie de log, un stack trace, logul unui job de CI, o eroare care tipărește un connection string.
  • Browserul. O cheie de server referită din codul de client sau o variabilă numită astfel încât bundler-ul să o includă.

Fiecare este o greșeală diferită cu aceeași rădăcină: cheia a fost tratată ca text, nu ca o permisiune.

Configurarea stă în mediu, nu în cod

Regula de bază e veche și încă corectă. Aplicația twelve-factor ține configurarea în variabile de mediu și dă un test care merită memorat: dacă baza de cod ar putea deveni open source în orice moment, fără să compromită nicio credențială. Dacă răspunsul este nu, secretul este în locul greșit.

Un fișier .env local este modul în care majoritatea aproximează asta pe laptop. Trebuie ignorat de Git înainte de primul commit, pentru că gitignore se aplică doar fișierelor netrasate. Documentația Git spune explicit: fișierele deja urmărite nu sunt afectate, iar ca să oprești urmărirea unuia rulezi git rm --cached, apoi adaugi numele în .gitignore.

Ce commiți este un fișier exemplu: .env.example, cu numele și fără valori, de exemplu DATABASE_URL= și SUPABASE_SECRET_KEY=. Este și documentație: un dezvoltator nou vede ce variabile există fără să vadă vreo valoare.

Prefixul este un comutator de divulgare

Framework-urile decid după nume ce ajunge în browser. În Next.js, variabilele fără prefixul NEXT_PUBLIC_ sunt disponibile doar în mediul Node.js și nu sunt accesibile din browser, iar o variabilă cu prefixul respectiv este inclusă în bundle la build și înghețată acolo. O cheie redenumită cu prefix pentru că ar avea nevoie clientul ajunge într-un fișier pe care oricine îl poate descărca. Parcurge lista și întreabă pentru fiecare variabilă: are nevoie browserul de ea sau serverul?

Ce chei pot sta în browser

Supabase livrează intenționat două familii de chei, ceea ce îl face un exemplu clar.

  • Cheile publishable încep cu sb_publishable_. Supabase le listează ca sigure de expus într-o pagină web, într-o aplicație mobilă sau desktop, într-un CLI sau în codul sursă. Când nu este nimeni autentificat, cheia corespunde rolului anon; când un utilizator se autentifică prin Supabase Auth, corespunde rolului authenticated. Row Level Security decide ce poate citi sau scrie fiecare rol.
  • Cheile secret încep cu sb_secret_. Corespund rolului service_role, care are atributul BYPASSRLS, deci o cheie secret ocolește orice politică Row Level Security. Locul lor este pe un server pe care îl controlezi.

Niciuna nu este un JWT. Supabase notează că o cheie lungă care începe cu eyJ este o cheie veche, ceea ce este util de recunoscut când un tutorial îți spune să copiezi una.

Blocarea cheilor secret în browser nu este o plasă de siguranță. Supabase se uită la antetul User-Agent și întoarce HTTP 401 dacă o cheie secret este folosită din browser, dar documentația spune deschis că un atacator poate totuși folosi cheia din alte unelte. Blocarea oprește un accident, nu o persoană.

Regula generală vine de aici. O cheie publishable este sigură pentru că este făcută să fie ținută de străini și pentru că altceva, în cazul acesta RLS, este granița reală. Scoate politicile și aceeași cheie devine o ușă publică. Dacă îți verifici propriile politici, am scris despre testarea Row Level Security înainte de lansare.

Supabase renunță la cheile vechi anon și service_role până la finalul anului 2026. Crearea cheilor noi nu le revocă pe cele vechi; ambele sisteme funcționează până când cheile vechi sunt dezactivate, iar dezactivarea este reversibilă. Suprapunerea este comodă la migrare și periculoasă la o scurgere, pentru că o cheie de care toată lumea a uitat încă funcționează.

Cheile noi nu sunt JWT-uri, deci se trimit în antetul apikey, nu în Authorization: Bearer, iar Supabase listează varianta bearer printre cauzele frecvente de erori 401.

Ce păstrează controlul de versiuni și de ce ștergerea nu ajunge

Un commit este o fotografie a întregului arbore, iar Git păstrează obiectele la care face referire fotografia. Șterge fișierul într-un commit nou și commitul vechi există în continuare, arată în continuare spre blobul vechi și rămâne accesibil din istoricul ramurii, din orice tag, dintr-un pull request și din fiecare clonă pe care a făcut-o cineva. Scoaterea copiei curente schimbă ce vede următorul checkout. Nu schimbă ce conține repository-ul.

Documentația GitHub recomandă ordinea la care te aștepți: dacă datele sunt un secret, revocă sau rotește cheia mai întâi, pentru că rescrierea istoricului poate să nu fie necesară și oricum nu anulează expunerea. Dacă totuși trebuie să scoți datele din istoric, rescrii local cu git filter-repo, care are nevoie de versiunea 2.47 sau mai nouă pentru opțiunea dedicată datelor sensibile, apoi faci force-push pe fiecare ramură, tag și ref.

Rescrierea schimbă hash-ul commitului care a introdus datele și al fiecărui commit de după el, invalidează semnăturile și perturbă pull request-urile deschise. Ref-urile din refs/pull/ nu pot fi împinse, pentru că GitHub le tratează ca read-only.

GitHub nu poate scoate datele din clonele altor oameni, iar dacă commitul există într-un fork, rămâne accesibil acolo. Vizualizările din cache pot fi atinse direct prin SHA. Suportul GitHub curăță vizualizările din cache doar când decide că riscul nu poate fi atenuat prin rotirea credențialelor și doar după ce repository-ul este curat și niciun fork nu mai ține datele. Protecția la push blochează implicit, pentru utilizatorii de pe GitHub.com, push-urile cu secrete detectate către repository-uri publice, iar o ocolire cere un motiv și rămâne înregistrată.

De aceea rotirea este remedierea, iar rescrierea istoricului este curățenia. Rescrierea face repository-ul prezentabil. Rotirea face valoarea scursă inutilă.

Cum rotești o cheie care s-ar putea să fi scurs

Două cuvinte se folosesc interschimbabil și nu ar trebui. Rotirea emite o credențială nouă și o lasă validă pe cea veche până când o retragi. Revocarea o dezactivează imediat pe cea veche. O rotire care nu ajunge la revocare este o cheie nouă lângă una scursă.

Procedura Supabase pentru o cheie secret scursă are patru pași: creezi o cheie secret nouă în panou, înlocuiești cheia compromisă peste tot unde o folosește aplicația, confirmi că fiecare componentă folosește cheia nouă, apoi o ștergi pe cea veche. Ștergerea nu poate fi anulată, de aceea confirmarea vine prima. Aceeași pagină recomandă o cheie secret separată pentru fiecare componentă de backend, ca o scurgere într-un serviciu să ceară o singură rotire, nu o oprire coordonată.

Partea grea este pasul doi, pentru că o cheie rareori stă într-un singur loc. Inventarul este: variabilele de mediu din fiecare mediu, depozitul de secrete al sistemului de CI, scripturile de deploy, joburile cron, fișierele .env locale de pe mai multe mașini, un serviciu de staging de care nimeni nu își amintește, un notebook, un tichet și un fir de chat. Fiecare copie rămâne activă până când este înlocuită sau cheia este revocată.

Ambele ordini se susțin. Rotește, apoi revocă păstrează serviciul funcțional cât timp urmărești copiile și se potrivește când expunerea este veche sau teoretică. Revocă întâi se potrivește când crezi că cheia este folosită chiar acum: accepți întreruperea, apoi reconstruiești accesul. Ce nu se susține este să ștergi cheia veche în timp ce o copie încă se autentifică undeva, pentru că eșecul va apărea mai târziu.

Cheile vechi Supabase adaugă o complicație. Pagina lor de troubleshooting spune că regenerarea secretului JWT invalidează toate secretele actuale și taie toate conexiunile, iar pentru proiectele rămase pe cheile vechi nu mai este posibil să rotești cheile anon, service și secretele JWT; drumul este migrarea. Un proiect care nu a migrat are mai puține opțiuni în cel mai prost moment.

Loguri, capturi de ecran și accidentele mici

Cheat sheet-ul OWASP despre logging listează ce nu ar trebui înregistrat direct: parole de autentificare, tokenuri de acces, identificatori de sesiune, chei de criptare și alte secrete primare, connection string-uri de bază de date. Acolo unde datele sunt necesare, ele trebuie mascate, hash-uite sau criptate, iar accesul la loguri trebuie restricționat și revizuit. Un log este o copie a sistemului tău cu permisiuni mai slabe decât sistemul.

În practică, loghezi un fapt despre secret, niciodată secretul. Ghidul Supabase pentru Edge Functions arată tiparul: citești variabila, răspunzi cu { configured: Boolean(secretKey) }, niciodată cu valoarea. Aceeași idee se aplică la verificarea de pornire, la un endpoint de health și la un handler de erori.

Capturile de ecran sunt artefactul cel mai puțin revizuit dintr-o echipă. O captură cu pagina de chei din panou, cu un terminal care rulează env sau cu o comandă curl care are antetul Authorization duce credențiala într-un tichet, într-un slide sau într-un chat. Decupeaz-o sau refă-o.

Build-urile sunt un alt canal discret. Documentația Docker spune că argumentele de build și variabilele de mediu nu sunt potrivite pentru secrete, pentru că persistă în imaginea finală, și recomandă montarea secretelor, disponibile doar pe durata instrucțiunii de build. Dacă o cheie a fost coaptă într-o imagine, imaginea este o copie a cheii.

O listă care încape într-o echipă mică

  • Configurarea vine din mediu. Repository-ul ține un fișier exemplu cu nume și fără valori.
  • Ignoră .env* înainte de primul commit și verifică cu git status, nu presupune. Un fișier adăugat în .gitignore după ce a fost commitat rămâne urmărit.
  • Întreabă pentru fiecare cheie ce poate face, nu dacă pare secretă. Dacă ocolește regulile tale de acces, nu pleacă niciodată de pe server.
  • Numește deliberat variabilele publice și verifică bundle-ul, nu te baza pe nume.
  • Loghează booleeni, nu valori. Estompează capturile de ecran. Nu lipi chei în chat; trimite o intrare din managerul de secrete.
  • Ține o cheie pentru fiecare componentă, ca o scurgere să aibă un blast radius mic, și notează unde stă fiecare înainte să ai nevoie de listă.

Nimic din toate acestea nu cere o echipă de securitate, un instrument sau un buget. Cere obiceiul de a trata o cheie ca pe o permisiune care poate fi revocată și memoria faptului că un repository ține minte tot ce ai pus vreodată în el. Două dintre reguli, fișierul de mediu și fișierul exemplu, previn majoritatea accidentelor pe care le vedem. Restul decid cât de rea este ziua când una trece.

Întrebări frecvente

Ar trebui să commitez fișierul .env în Git?
Nu. Ține valorile reale în mediu sau într-un fișier .env local pe care Git îl ignoră și commite un .env.example cu numele variabilelor și valori goale. Ignoră fișierul înainte de primul commit: gitignore nu afectează fișierele deja urmărite de Git, așa că un .env deja commitat trebuie scos din index cu git rm --cached.
Sunt cheile publishable Supabase sigure de expus în browser?
Da, când Row Level Security este activ și politicile sunt corecte. O cheie publishable începe cu sb_publishable_ și corespunde rolului anon, sau authenticated când un utilizator este autentificat, deci ajunge doar la ce permit politicile tale. O cheie secret începe cu sb_secret_ și ocolește orice politică, deci nu trebuie să plece niciodată de pe server.
Ștergerea unui secret într-un commit nou îl scoate din istoricul Git?
Nu. Commitul vechi referă în continuare conținutul vechi al fișierului și rămâne accesibil din ramură, din tag-uri, din pull request-uri și din fiecare clonă. Ca să îl scoți cu adevărat trebuie să rescrii istoricul cu git filter-repo și să faci force-push, ceea ce schimbă hash-urile commiturilor și nu ajunge la clonele sau fork-urile altor oameni. Rotește cheia întâi; tratează rescrierea ca pe curățenie.
Cum rotești o cheie API expusă?
Creezi o cheie nouă, o înlocuiești pe cea veche peste tot unde o folosește aplicația, confirmi că fiecare componentă o folosește pe cea nouă, apoi o retragi pe cea veche. În Supabase, ștergerea unei chei secret nu poate fi anulată, deci confirmarea vine prima. Folosește o cheie separată pentru fiecare componentă, ca o scurgere să ceară o singură rotire, nu o întrerupere în toate serviciile.
De ce nu ajunge să rotești cheia într-un singur loc?
Pentru că o cheie este copiată în secretele din CI, în scripturi de deploy, în joburi cron, în fișiere .env locale, în tichete și în fire de chat. Până când cheia veche este revocată, fiecare copie se autentifică în continuare. Rotirea emite o credențială nouă; revocarea o dezactivează pe cea veche. O rotire care nu ajunge la revocare lasă cheia scursă să funcționeze lângă cea nouă.
Este sigur să tipărești o cheie API într-un log pentru depanare?
Nu. Ghidul OWASP despre logging spune că tokenurile de acces, identificatorii de sesiune, cheile de criptare și connection string-urile nu trebuie înregistrate direct; ele trebuie eliminate, mascate sau hash-uite. Loghează un fapt, de exemplu dacă variabila este setată. Logurile sunt copiate, trimise către unelte terțe și păstrate mai mult decât codul pe care îl descriu.
Distribuie

Articole similare

  • 6 min de citit

    Conturile clienților nu au ce căuta în browserul principal

    Un profil de browser comun pune toate sesiunile clienților în același borcan de cookie-uri, sub extensii care le pot citi pe toate. Iată ce oferă o sesiune furată și ce înseamnă izolare reală.

    • Securitate
    • Confidențialitate
    • Unelte pentru dezvoltatori
  • 7 min de citit

    Mai multe conturi Vercel Netlify: cum le gestionezi fără logout

    Dă fiecărui cont Vercel, Netlify, Cloudflare, Supabase sau Gmail propria sesiune izolată și poți comuta între ele fără logout. Iată variantele, riscurile de securitate și regulile de verificat.

    • Unelte pentru dezvoltatori
    • Productivitate
    • Securitate
  • 8 min de citit

    Hosted sau self-hosted pentru un proiect de client: cum decizi

    Pentru un proiect de client, hosted sau self-hosted este o decizie despre responsabilități, nu despre preferințe. Șase întrebări despre locația datelor, restore, patch-uri, predare și ieșire, cu cazurile sincere de fiecare parte.

    • Afaceri
    • Unelte pentru dezvoltatori