Naar de inhoud

Blog ·

Waarom je paneel geen open poort nodig heeft

Een agent die uitgaand belt heeft twee uitgaande regels nodig en geen inkomende poort. Wat dat oplevert, wat het kost, en vier problemen die het verplaatst.

Door Provibr

Als een managementplatform je hypervisors gaat draaien, moet er iets over de grens tussen hun netwerk en het jouwe. Er zijn twee richtingen om uit te kiezen, en die keuze bepaalt bijna al het andere aan het beveiligingsmodel.

De traditionele richting is naar binnen. Het platform houdt de inloggegevens van je Proxmox-cluster, je DirectAdmin-machine en je gamepaneel vast, en verbindt met elk daarvan over het internet. Dat betekent een API die aan het internet antwoordt, een firewallregel of een VPN, en een control plane die bereikbaar is voor iedereen die hem weet te vinden.

De andere richting is naar buiten. Je draait een kleine agent binnen je eigen netwerk. Die opent één verbinding naar het platform en houdt die open, commando's reizen over die verbinding naar beneden en resultaten reizen erlangs terug omhoog. Aan jouw kant luistert niets.

Dit stuk gaat over wat die tweede optie kost en wat hij oplevert, vanuit het bouwen ervan.

De firewallwijziging is twee regels, allebei uitgaand

De volledige netwerkeis van een uitgaande agent past in één tabel:

RichtingRegelWanneer
UitgaandTCP 50051 naar de platformgatewayEenmalig, bij de aanmelding
UitgaandTCP 50052 naar dezelfde hostDoorlopend, dit is de sessie
InkomendGeen

Twee poorten in plaats van één is een bewuste splitsing. Aanmelden gebeurt voordat de agent een clientcertificaat heeft, dus die poort kan alleen server-TLS bieden. De sessiepoort eist wederzijdse TLS en weigert alles zonder geldig clientcertificaat. Eén luisteraar voor allebei zou een luisteraar zijn waar het clientcertificaat optioneel is en het autorisatiebesluit voorwaardelijk. Twee maken de grens structureel: op 50052 bestaat er geen codepad waarin een niet-geauthenticeerde tegenpartij voorbij de handshake komt.

Filter je het uitgaande verkeer streng, dan zijn die twee regels de hele vraag. Er is geen inkomende regel om te schrijven, geen NAT-regel, geen VPN-concentrator, en niets om te vernieuwen als je publieke adres verandert.

Wat "uitgaand" betekent op een vijandig netwerk

Een uitgaande TCP-verbinding door NAT heen is niet gratis: de staat die hem laat werken staat in de tabel van iemand anders, en die tabel heeft een timeout. Carrier-NAT, een bedrijfsfirewall en een cloud-loadbalancer vergeten allemaal stilletjes een inactieve verbinding, en dat is het ergste soort storing, want beide kanten geloven nog steeds dat de socket in orde is.

Twee mechanismen vangen dat op, en je hebt ze allebei nodig.

Het eerste is HTTP/2-keepalive, elke 30 seconden met een timeout van 10 seconden. Dat is de laag die de timeouts van NAT en loadbalancers verslaat, en de enige die opmerkt dat een TCP-verbinding stilletjes onder de stroom vandaan gestorven is.

Het tweede is een applicatie-heartbeat elke 20 seconden, met een watchdog die de sessie beëindigt als er binnen 45 seconden geen bevestiging binnenkomt. Keepalive bewijst dat de socket leeft; de heartbeat bewijst dat de andere kant nog aan het verwerken is.

Eindigt de sessie toch, dan verbindt de agent opnieuw met exponentiële backoff en full jitter: de wachttijd wordt uniform getrokken tussen nul en de huidige bovengrens, die verdubbelt van één seconde tot zestig. Die jitter is geen versiering. Zonder dat verbindt elke agent van elke klant op precies hetzelfde moment opnieuw na een herstart van de gateway, en verandert je herstel in een zelfveroorzaakte stormloop.

Eén detail dat pas in productie opduikt: de backoffteller wordt pas teruggezet nadat een sessie een volle minuut gezond is geweest. Zonder die regel verbindt een agent met een halfkapotte verbinding, sterft, zet zijn teller terug op één seconde, en beukt daarna voor eeuwig één keer per seconde op de gateway.

Identiteit: een certificaat, geen wachtwoord

Een inkomend model authenticeert het platform bij jouw paneel, meestal met een API-sleutel die je in een formulier plakt. Een uitgaand model moet de agent bij het platform authenticeren, en dat kan met iets beters dan een gedeeld geheim.

De agent meldt zich één keer aan met een token voor eenmalig gebruik: 36 willekeurige bytes, 48 base64url-tekens, serverzijde alleen opgeslagen als SHA-256-hash en 72 uur geldig. Hij wordt verbruikt binnen een transactie die de voorwaarden waarop hij geselecteerd werd opnieuw controleert, want tussen het lezen en het schrijven glipt een dubbele aanmelding naar binnen.

Wat er terugkomt is een clientcertificaat. Vanaf dat moment is de sessie wederzijds geauthenticeerd, en pint de gateway de exacte tegenpartij vast: hij neemt de SHA-256-vingerafdruk van het aangeboden certificaat en zoekt de agent op via die vingerafdruk, vóórdat de agent gezegd heeft wie hij dénkt te zijn. De identiteit ligt vast door het certificaat; het eerste bericht op de stroom kan hem alleen nog bevestigen.

De rotatie loopt op de eigen klok van de agent: certificaten leven 90 dagen en de vernieuwing begint halverwege, op dag 45. Dat laat 45 dagen aan pogingen over voordat er iets in gevaar is, en het paneel waarschuwt een mens pas bij 21 dagen te gaan, dus de vernieuwing moet drie weken lang mislukt zijn voordat iemand wordt lastiggevallen. Elke rotatie maakt een nieuw sleutelpaar: vernieuwen op dezelfde sleutel verlengt het leven van iets dat misschien al gelekt is.

Intrekken verdient zijn eigen zin, want de eerlijke versie is minder elegant dan het plaatje. Er is geen CRL en geen OCSP. Een agent intrekken is een databasewijziging, en een verbonden sessie wordt daar één keer per minuut tegenaan gehouden. Die controle is fail-closed, met een budget van drie mislukkingen op rij, dus een hikje van de database kan een ingetrokken agent niet stilletjes in leven houden, en het slechtste geval is begrensd en uit te rekenen: zestig seconden maal drie.

Precies één keer, over een verbinding die wegvalt

Zodra je aanvaardt dat de verbinding een langlevende stroom over een onbetrouwbaar pad is, houdt de afleverbelofte op een theoretische zorg te zijn. Het platform gaat een commando opnieuw aanbieden na een herverbinding, want van zijn kant is het verschil tussen "de agent heeft hem niet gekregen" en "het resultaat is niet teruggekomen" onzichtbaar.

Dus onthoudt de agent het. Afgeronde commando's blijven met hun resultaat op schijf staan, gesleuteld op commando-ID, in een begrensde LRU. Een herhaling van een afgerond commando geeft het bewaarde resultaat terug en voert niets uit. Een herhaling van een commando dat nog loopt wordt genegeerd, want het antwoord is al onderweg.

Op schijf, nadrukkelijk, en dat is het punt: een cache in het geheugen is precies leeg op het moment dat het platform alles opnieuw aanbiedt, en dat is vlak na een herstart.

De andere richting is hetzelfde gebouwd. Gebeurtenissen vertrekken via een gesequenceerde outbox; bij een herverbinding noemt het platform het volgnummer dat het heeft en speelt de agent vanaf dat punt opnieuw af. Die outbox is begrensd, en valt er iets uit, dan zegt de agent dat bij de volgende verbinding door het laagste volgnummer te noemen dat hij nog heeft, min één, zodat het platform ophoudt te wachten op gebeurtenissen die niet meer bestaan.

Er zit één eerlijke barst in de belofte, en die benoemen we liever dan hem te verbergen: mislukt het wegschrijven van de dedupe-registratie naar schijf, dan wordt het resultaat toch verstuurd. Het alternatief zou zijn om het resultaat van een afgerond commando weg te gooien om een garantie te beschermen, en in het ergste geval voert een latere heraanbieding hem een tweede keer uit.

Credentials blijven aan jouw kant

Dit is het deel dat de vorm van een inbraak verandert.

In het inkomende model bewaart het platform inloggegevens die controle geven over jouw infrastructuur, en die opslag is de hoofdprijs. In het uitgaande model leven de inloggegevens van je hypervisors en panelen in een versleutelde kluis op je eigen host, daar één keer heen gestuurd toen je de integratie toevoegde.

Onze kluis gebruikt XChaCha20-Poly1305 met een sleutel die via HKDF-SHA256 afgeleid wordt uit een lokaal sleutelbestand en de machine-id, met het agent-ID meegemengd in de afleidingsinformatie. Een gevolg dat het uitschrijven waard is: vault.enc naar een andere machine kopiëren levert je een bestand op dat niet ontsleutelt, want de machine waaraan hij gebonden was is onderdeel van de sleutel.

En de grens, die we in de broncode naast de implementatie hebben geschreven, is dat dit niet beschermt tegen root op de host. Dat hoeft ook niet. De klant is eigenaar van de host en eigenaar van de inloggegevens. De kluis beschermt tegen een gelekt bestand, niet tegen de eigenaar.

Het auditspoor is naar dezelfde regel gevormd. Elk commando is een rij die nooit hard verwijderd wordt, en de inhoud van een credential-push wordt geschoond opgeslagen. Een audittabel die de geheimen bevat die hij aan het controleren is, is een tweede kopie van het probleem.

Vier dingen die dit niet oplost

Een architectuurstuk dat alleen voordelen opsomt is marketing. Dit is wat er verschuift in plaats van verdwijnt.

Software op jouw host. De ruil is echt: je zet geen poort meer open, en in ruil daarvoor draai je een binary die geïnstalleerd, bijgewerkt en vertrouwd moet worden. Dat verplaatst de vraag van "wie kan bij mijn paneel" naar "wie tekent de code die ik draai", en dat is een betere vraag maar geen gratis vraag.

Brede rechten. Een agent die een hypervisor beheert, houdt inloggegevens vast die een hypervisor beheren. De richting van de verbinding verandert niets aan wat die inloggegevens kunnen, alleen aan waar ze staan en wie er verder bij kan.

Uitgaand is nog steeds een uitgang. Een verbinding die je netwerk verlaat kan er data uit meenemen. De bestemming ligt vast, het transport is wederzijds geauthenticeerd en het protocol is smal, maar "twee uitgaande regels" is een kleinere bewering dan "er gaat niets naar buiten".

Antwoorden in realtime. Deze duikt meteen op in de API. Omdat het platform de agent niet kan bellen, kan een schrijfactie niet met zijn resultaat antwoorden: het juiste antwoord op "maak deze machine aan" is een bevestiging en iets om op te pollen. Leesacties hebben er geen last van, want die komen uit de eigen database van het platform. Wie een API bovenop een uitgaande agent ontwerpt, moet die asymmetrie aanvaarden in plaats van hem weg te poetsen.

Er is ook precies één geval waarin de agent wél luistert, en doen alsof dat niet zo is zou oneerlijk zijn. Bare metal over het netwerk installeren betekent DHCP beantwoorden en bestanden serveren, en dat kan niet vanaf een uitgaande socket. Dus bindt de agent die diensten, op één genoemde interface, alleen voor machines met een lopende opdracht, en alleen zolang er werk is. "Ingeschakeld" betekent "hier toegestaan", niet "luistert nu".

Wanneer inkomend tóch het juiste antwoord is

Draai je één hypervisor en beheer je die zelf, dan geldt niets hiervan: een agent levert je niets op boven een firewallregel en een sterk wachtwoord.

Het argument wordt scherper met afstand en met aantallen. Meerdere locaties, netwerken die niet van jou zijn, adressen die veranderen, klanten van wie je de firewall niet kunt aanpassen, en een control plane die je liever helemaal niet op het publieke internet hebt. Op dat punt is de agent geen extra onderdeel; hij is het onderdeel waarmee je de rest kunt weggooien.

Zo werkt Provibr: één uitgaande verbinding per agent, geen inkomende poort, en inloggegevens die het netwerk van de klant nooit verlaten. De agentdocumentatie behandelt installatie, capaciteit en intrekken, aan de slag loopt de eerste verbinding van begin tot eind door, en de API beschrijft de bevestig-en-poll-vorm die het uitgaande ontwerp afdwingt.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.