Sări la conținut
    Înapoi la blog
    5 min citireStefan

    VanaGreen: o a doua barieră de izolare a datelor, direct în baza de date

    Am adăugat Postgres Row-Level Security ca al doilea strat de izolare între clienți, independent de codul aplicației. Sweep-ul de testare care l-a validat a scos la iveală patru bug-uri reale de producție, ascunse până acum.

    #vanagreen · #securitate · #postgres · #infrastructura

    Într-o aplicație cu mai mulți clienți pe aceeași bază de date, cum e VanaGreen, izolarea datelor între ei stă, în mod normal, într-un singur loc: fiecare query din cod trebuie să includă un filtru după tenant_id. Funcționează, dar are un punct slab structural: depinde de disciplina fiecărei linii de cod, scrisă azi sau peste un an, de fiecare dezvoltator, în fiecare funcție nouă. Un query nou, un refactor grăbit, o rută adăugată în grabă: e suficient un singur filtru uitat ca să expună date de la un client către altul.

    Ce am adăugat

    Am pus Postgres Row-Level Security (RLS) ca al doilea strat de izolare, direct în baza de date, nu doar în cod. Documentat în ADR-0005: fiecare tabel relevant are acum o politică RLS care filtrează automat rândurile după contextul de tenant al sesiunii curente, indiferent ce interoghează query-ul de deasupra. Practic, chiar dacă aplicația uită complet un filtru, baza de date nu returnează și nu lasă să se scrie rânduri din afara tenantului activ. Un backstop, nu prima linie de apărare: codul din aplicație rămâne exact la fel, contractul API nu s-a schimbat cu nimic.

    Contextul de tenant ajunge la Postgres printr-un rol de bază de date dedicat, propagat prin AsyncLocalStorage la fiecare request, plus un mecanism separat de bypass_rls pentru fluxurile administrative care chiar trebuie să vadă peste granițele de tenant (job-uri cron, unelte interne). Sesiunile de tip operator sau mobile, folosite de utilizatorii obișnuiți din teren, nu ating niciodată acest bypass.

    Ce a scos la iveală sweep-ul de testare

    Activarea RLS pe un cod deja existent, scris ani de zile fără ideea că un al doilea strat de izolare va exista vreodată, a însemnat un sweep masiv: aproape 90 de fișiere de test care scriau direct în baza de date, ocolind ciclul normal de request, deci fără niciun context de tenant setat. Corectarea lor n-a fost doar mecanică: pe parcurs au ieșit la iveală patru bug-uri reale de producție, nu goluri de test:

    • Autentificarea prin API key (/api/v1/public/, /api/v1/platform/) căuta cheia fără niciun context de tenant setat. Cu RLS activ, orice request cu bearer token ar fi picat direct cu INVALID_API_KEY.
    • Job-urile cron fleet-maintenance-cron și auth-expiry-cron citeau profilul de tenant fără context. Alertele de expirare (ITP, autorizație de mediu) ar fi rămas tăcute, niciodată trimise nimănui.
    • processDueDeliveries, retry-ul cu backoff din modulul de webhook-uri, livra fiecare webhook fără context de tenant. Orice livrare reîncercată ar fi picat.
    • Migrația 0166: tabelul document_templates amestecă rânduri globale (fallback-ul implicit pentru orice tenant) cu override-uri specifice per client, în același tabel. Politica standard nu lăsa niciun tenant să vadă rândurile globale, deci orice cerere de șablon fără override propriu dădea 404. Citirea acceptă acum explicit și rândurile globale; scrierea rămâne strictă.

    Toate patru ar fi rupt fluxuri reale chiar în momentul activării, nu doar teste. Au fost găsite și reparate înainte.

    Cât de sigur, cât de rapid

    Înainte de activare, mecanismul a trecut prin doi pași de validare separați. Regresia completă (QA-1) a scos inițial 314 eșecuri, aproape toate din fixture-urile de test fără context, nu din aplicația reală; după sweep, 0 eșecuri reale rămase (doar 2 flakiness pre-existente, neconectate la RLS). Un test adversarial dedicat (SEC-1, 7/7 verde) a încercat explicit acces cross-tenant, la citire și scriere, pe trei tabele reale, fără tenant_id în query: toate blocate.

    Pe perf (QA-2), pe rutele cu cel mai mult trafic, timpul mediu de răspuns a crescut moderat: GET /api/collections de la 5.6ms la 10.5ms, GET /api/reports/dashboard de la 9.0ms la 17.6ms. Sub 25ms chiar și la p99. Suficient de puțin cât să nu justifice un fallback de batching per-request, pregătit dar nefolosit deocamdată.

    De ce contează

    Nimic din asta nu se vede vreodată în interfața VanaGreen: niciun endpoint, niciun răspuns, nicio schemă expusă clientului nu s-a schimbat. E genul de lucru care, făcut bine, e complet invizibil. Dar înseamnă că, de acum, izolarea datelor între clienții VanaGreen nu mai depinde exclusiv de un filtru scris corect în fiecare linie de cod nouă. Iar patru bug-uri care ar fi tăcut alerte legale sau blocat integrări API tocmai atunci când conta cel mai mult au fost găsite și închise înainte să apuce să conteze pentru vreun client real.

    Vrei să discutăm cum aplicăm asta pe procesul tău?

    Programează un audit tehnic

    Articole similare