Blog ·
De ce să nu redirecționați porturi către panou
Un agent care iese din rețea cere două reguli de ieșire și niciun port de intrare. Ce câștigați, ce costă și cele patru probleme pe care doar le mută.
De Provibr
Dacă o platformă de administrare urmează să vă ruleze hipervizoarele, ceva trebuie să traverseze granița dintre rețeaua lor și a dumneavoastră. Sunt două direcții din care puteți alege, iar alegerea determină aproape tot restul modelului de securitate.
Direcția tradițională este spre interior. Platforma deține datele de autentificare pentru clusterul dumneavoastră Proxmox, pentru cutia DirectAdmin și pentru panoul de joc și se conectează la fiecare dintre ele prin internet. Asta înseamnă un API care răspunde la internet, o regulă de firewall sau un VPN și un plan de control accesibil oricui îl poate găsi.
Cealaltă direcție este spre exterior. Rulați un agent mic în propria dumneavoastră rețea. El deschide o conexiune către platformă și o ține deschisă, comenzile coboară pe acea conexiune, iar rezultatele urcă înapoi pe ea. Nimic din partea dumneavoastră nu ascultă.
Acest text este despre ce costă a doua opțiune și ce vă dă, din experiența construirii uneia.
Schimbarea de firewall: două reguli, ambele de ieșire
Toată cerința de rețea a unui agent spre exterior încape într-un tabel:
| Direcție | Regulă | Când |
|---|---|---|
| Ieșire | TCP 50051 către gateway-ul platformei | O dată, la înrolare |
| Ieșire | TCP 50052 către aceeași gazdă | Continuu, aceasta este sesiunea |
| Intrare | Niciuna |
Două porturi în loc de unul reprezintă o separare deliberată. Înrolarea are loc înainte ca agentul să aibă un certificat de client, așa că acel port poate oferi doar TLS pe partea de server. Portul de sesiune cere TLS mutual și respinge orice nu are un certificat de client valid. Un singur ascultător pentru amândouă ar însemna un ascultător la care certificatul de client este opțional, iar decizia de autorizare este condiționată. Două fac granița structurală: pe 50052 nu există nicio cale de cod în care un partener neautentificat să treacă de handshake.
Dacă filtrarea traficului de ieșire este strictă, acele două reguli sunt tot ce se cere. Nu există nicio regulă de intrare de scris, nicio intrare NAT, niciun concentrator VPN și nimic de reînnoit când adresa dumneavoastră publică se schimbă.
Ce înseamnă „spre exterior” când rețeaua este ostilă
O conexiune TCP de ieșire prin NAT nu este gratuită: starea care o face să funcționeze trăiește în tabela altcuiva, iar acea tabelă are un timeout. NAT-ul de operator, un firewall corporativ și un balansor de sarcină din cloud uită toate, în tăcere, o conexiune inactivă, iar modul de eșec este cel mai rău cu putință, pentru că ambele capete cred în continuare că socketul este în regulă.
Două mecanisme se ocupă de asta și aveți nevoie de amândouă.
Primul este keepalive-ul HTTP/2, la intervale de 30 de secunde, cu un timeout de 10 secunde. Acela este stratul care învinge timeout-urile de NAT și de balansor și singurul care observă o conexiune TCP moartă în tăcere sub flux.
Al doilea este un heartbeat de aplicație la fiecare 20 de secunde, cu un watchdog care încheie sesiunea dacă nu sosește nicio confirmare în 45 de secunde. Keepalive-ul dovedește că socketul este viu; heartbeat-ul dovedește că celălalt capăt încă procesează.
Când sesiunea chiar se încheie, agentul se reconectează cu backoff exponențial și jitter complet: așteptarea este extrasă uniform între zero și plafonul curent, dublându-se de la o secundă până la șaizeci. Jitter-ul nu este decor. Fără el, fiecare agent al fiecărui client se reconectează în aceeași clipă după o repornire a gateway-ului, iar redresarea dumneavoastră se transformă într-o busculadă provocată de dumneavoastră înșivă.
Un detaliu care apare doar în producție: contorul de backoff se resetează abia după ce o sesiune a fost sănătoasă un minut întreg. Fără acea regulă, un agent cu o conexiune pe jumătate stricată se conectează, moare, își resetează contorul la o secundă și lovește gateway-ul o dată pe secundă la nesfârșit.
Identitate: un certificat, nu o parolă
Un model spre interior autentifică platforma la panoul dumneavoastră, de obicei cu o cheie API pe care o lipiți într-un formular. Un model spre exterior trebuie să autentifice agentul la platformă și poate face asta cu ceva mai bun decât un secret partajat.
Agentul se înrolează o singură dată cu un jeton de unică folosință: 36 de octeți aleatori, 48 de caractere base64url, stocat pe server doar ca amprentă SHA-256 și valabil 72 de ore. Este consumat într-o tranzacție care reverifică condițiile pe baza cărora a fost selectat, pentru că între citire și scriere este exact locul pe unde intră o înrolare dublă.
Ce se întoarce este un certificat de client. De atunci înainte sesiunea este autentificată mutual, iar gateway-ul fixează exact partenerul: ia amprenta SHA-256 a certificatului prezentat și caută agentul după acea amprentă, înainte ca agentul să fi spus cine crede el că este. Identitatea este fixată de certificat; primul mesaj de pe flux nu poate decât să o confirme.
Rotația rulează pe ceasul propriu al agentului: certificatele trăiesc 90 de zile, iar reînnoirea începe la jumătate, în ziua 45. Asta lasă 45 de zile de reîncercări înainte ca ceva să fie în pericol, iar panoul avertizează un om abia la 21 de zile rămase, așa că reînnoirea trebuie să fi eșuat trei săptămâni înainte să fie cineva deranjat. Fiecare rotație generează o nouă pereche de chei: reînnoirea pe aceeași cheie prelungește viața a ceva ce s-ar putea să fi scăpat deja.
Revocarea merită propria frază, pentru că versiunea sinceră este mai puțin elegantă decât schema. Nu există CRL și nici OCSP. Revocarea unui agent este o actualizare în baza de date, iar o sesiune conectată este verificată față de ea o dată pe minut. Acea verificare este fail-closed, cu un buget de trei eșecuri consecutive, așa că un sughiț al bazei de date nu poate ține în viață, în tăcere, un agent revocat, iar cazul cel mai rău este mărginit și calculabil: șaizeci de secunde ori trei.
Exact o dată, printr-o conexiune care va cădea
Odată ce acceptați că legătura este un flux de lungă durată pe o cale nesigură, semantica livrării încetează să fie o preocupare teoretică. Platforma va livra din nou o comandă după o reconectare, pentru că din partea ei diferența dintre „agentul nu a primit-o” și „rezultatul nu s-a întors” este invizibilă.
Așa că agentul ține minte. Comenzile încheiate sunt păstrate pe disc împreună cu rezultatele lor, indexate după ID-ul comenzii, într-un LRU mărginit. O repetare a unei comenzi încheiate întoarce rezultatul din cache și nu execută nimic. O repetare a unei comenzi care încă rulează este ignorată, pentru că răspunsul este deja pe drum.
Pe disc, mai exact, și acesta este esențialul: un cache în memorie este gol exact atunci când platforma reoferă totul, adică imediat după o repornire.
Direcția inversă se potrivește. Evenimentele pleacă printr-o cutie de ieșire secvențiată; la reconectare platforma declară numărul de secvență pe care îl are, iar agentul reia de acolo. Cutia de ieșire este mărginită, iar când ceva iese din ea agentul o spune la următoarea conectare, declarând cea mai mică secvență pe care o mai deține minus unu, ca platforma să nu mai aștepte evenimente care nu mai există.
Există o fisură sinceră în promisiune, care merită spusă în loc să fie ascunsă: dacă scrierea pe disc a înregistrării de deduplicare eșuează, rezultatul este totuși trimis. Alternativa ar fi să pierdem rezultatul unei comenzi încheiate pentru a proteja o garanție, iar în cel mai rău caz o relivrare ulterioară o execută a doua oară.
Datele de autentificare rămân la dumneavoastră
Aceasta este partea care schimbă forma unei breșe.
În modelul spre interior, platforma stochează date de autentificare care dau controlul asupra infrastructurii dumneavoastră, iar acel depozit este premiul. În modelul spre exterior, datele de autentificare pentru hipervizoarele și panourile dumneavoastră trăiesc într-un seif criptat pe propria dumneavoastră gazdă, împinse acolo o singură dată, când adăugați o integrare.
Seiful nostru folosește XChaCha20-Poly1305 cu o cheie derivată prin HKDF-SHA256 dintr-un fișier de cheie local și ID-ul mașinii, cu ID-ul agentului amestecat în informația de derivare. O consecință care merită spusă pe litere: copierea lui vault.enc pe altă mașină vă dă un fișier care nu se va decripta, pentru că mașina de care a fost legat face parte din cheie.
Iar limita, pe care am scris-o în sursă lângă implementare, este că asta nu protejează împotriva unui root pe gazdă. Nici nu are nevoie. Clientul deține gazda și deține datele de autentificare. Seiful protejează împotriva unui fișier scurs, nu împotriva proprietarului.
Traseul de audit este modelat de aceeași regulă. Fiecare comandă este un rând care nu este niciodată șters definitiv, iar conținutul unei împingeri de date de autentificare este stocat cenzurat. Un tabel de audit care conține secretele pe care le auditează este o a doua copie a problemei.
Patru lucruri pe care asta nu le rezolvă
Un text de arhitectură care enumeră doar avantaje este marketing. Iată ce se mută în loc să dispară.
Software pe gazda dumneavoastră. Schimbul este real: nu mai expuneți un port și, în schimb, rulați un binar care trebuie instalat, actualizat și în care trebuie să aveți încredere. Asta mută întrebarea de la „cine poate ajunge la panoul meu” la „cine semnează codul pe care îl rulez”, care este o întrebare mai bună, dar nu una gratuită.
Privilegii largi. Un agent care administrează un hipervizor deține date de autentificare care administrează un hipervizor. Direcția conexiunii nu schimbă nimic din ce pot face acele date, doar unde trăiesc și cine altcineva mai poate ajunge la ele.
Ieșirea rămâne o ieșire. O conexiune care vă părăsește rețeaua poate scoate date din ea. Destinația este fixă, transportul este autentificat mutual, iar protocolul este îngust, dar „două reguli de ieșire” este o afirmație mai mică decât „nimic nu pleacă”.
Răspunsuri în timp real. Acesta apare imediat în API. Pentru că platforma nu poate apela agentul, o scriere nu poate răspunde cu rezultatul ei: răspunsul corect la „creează această mașină” este o confirmare și ceva de interogat. Citirile nu sunt afectate, pentru că ele vin din baza de date proprie a platformei. Oricine proiectează un API deasupra unui agent spre exterior trebuie să accepte acea asimetrie, în loc să o acopere.
Există și exact un caz în care agentul chiar ascultă, iar a pretinde altceva ar fi necinstit. Instalarea unei mașini fizice prin rețea înseamnă a răspunde la DHCP și a servi fișiere, ceea ce nu se poate face dintr-un socket de ieșire. Așa că agentul leagă acele servicii, pe o singură interfață numită, doar pentru mașinile care au o sarcină în curs și doar cât timp este de lucru. „Activat” înseamnă „permis aici”, nu „ascultă acum”.
Când spre interior este totuși răspunsul corect
Dacă rulați un singur hipervizor și îl administrați singur, nimic din toate acestea nu se aplică: un agent nu vă aduce nimic în plus față de o regulă de firewall și o parolă puternică.
Argumentul se ascute cu distanța și cu numărul. Mai multe locații, rețele care nu vă aparțin, adrese care se schimbă, clienți ale căror firewall-uri nu le puteți edita și un plan de control pe care ați prefera să nu îl aveți deloc pe internetul public. În acel punct agentul nu este o componentă în plus; este componenta care vă lasă să le ștergeți pe celelalte.
Așa funcționează Provibr: o singură conexiune de ieșire per agent, niciun port de intrare și date de autentificare care nu părăsesc niciodată rețeaua clientului. Documentația agentului acoperă instalarea, dimensionarea și revocarea, primii pași parcurg prima conexiune de la un capăt la altul, iar API-ul documentează forma de confirmare și interogare pe care o impune proiectarea spre exterior.