Naar de inhoud

Blog ·

Een canary die tegen echte servers draait

Een stub kan authenticatie niet bewijzen. Twee monitoringdefecten die alleen een echte PostgreSQL en een echte Redis konden tonen, en wat een statuspagina telt.

Door Provibr

Een monitoringcheck die op een database inlogt is een klein stukje protocolcode met een onaangename eigenschap: bijna elke manier om hem te toetsen toetst het verkeerde.

De voor de hand liggende proef is een stub-server die het protocol spreekt. Dat werkt, en het bewijst heel wat: framing, timeouts, wat er gebeurt als de verbinding midden in een handshake wegvalt, of een weigering de juiste foutcode oplevert. Wat hij níet kan bewijzen is authenticatie, en wel om een structurele reden. Een stub die onze eigen scramble narekent gebruikt onze eigen implementatie aan allebei de kanten van de lijn. Verwissel twee argumenten in een hash en beide kanten verwisselen ze, en de test staat groen op code die geen enkele echte server zou accepteren.

Dat gat stond als openstaande post aan het eind van de vorige monitoringronde: SCRAM was alleen ooit gemeten tegen een implementatie die wij ook zelf schreven. Een canary sluit hem.

Wat een canary hier is

Een canary is een kleine verzameling proeven die tegen het echte ding draaien in plaats van tegen een model ervan. Wegwerpcontainers op onze eigen server, gebonden op localhost, met een echte PostgreSQL 17, Redis 7, MySQL 8.4 en MariaDB 11 erin. De adapters worden rechtstreeks aangeroepen en niet via de commando-envelope, want het onderwerp is de draad en niet het leidingwerk eromheen. Elf proeven, standaard allemaal overgeslagen, en elk ervan weigert te draaien als zijn adres niet werkelijk gezet is.

Die tweede grendel is niet dubbelop bij de eerste. Draait iemand de hele suite mét de overslaanvlag opgeheven, dan zou een proef anders stilletjes naar 127.0.0.1:0 verbinden en een foutmelding geven die als een bug in onze code leest in plaats van als een ontbrekende omgevingsvariabele.

Wat de elf dekken:

ServerWat er bewezen wordt
PostgreSQL 17SCRAM met het goede en met een fout wachtwoord, md5, trust, TLS verplicht tegen een server met TLS uit, volledige certificaatcontrole tegen een vreemd anker
Redis 7Wachtwoordauthenticatie en een ACL-gebruiker, plus de INFO-allowlist
MySQL 8.4caching_sha2 op een koude en op een warme cache, en een fout wachtwoord
MariaDB 11.8Dezelfde handshake tegen een andere implementatie ervan

Het onderscheid tussen koude en warme cache doet ertoe omdat het twee verschillende codepaden zijn. MySQL gebruikt bij caching_sha2_password een goedkope scramble zodra de server het account eerder gezien heeft, en vraagt de eerste keer na een herstart om volledige authenticatie. Alleen een echte server herstart.

Twee defecten die geen stub had kunnen tonen

PostgreSQL, fout wachtwoord. Is het wachtwoord fout, dan antwoordt PostgreSQL op de SASL-initiële respons niet met een vervolg. Hij antwoordt met een fout die SQLSTATE 28P01 draagt, ongeldig wachtwoord. De adapter had daar een kale destructurering staan, die van alles wat geen vervolg was een protocolfout maakte: deze dienst antwoordde niet als een PostgreSQL-server. De SQLSTATE werd weggegooid. Een klant van wie de monitoringcredentials verlopen waren las dat zijn database stuk was.

Een stub komt daar nooit langs, want een stub die het geslaagde pad toetst stuurt het vervolg. Beide plekken waar nu een fout kan binnenkomen, na de eerste en na de tweede stap, houden hem een inlogfout.

Redis, een leeg veld. De adapter leest precies twee velden uit INFO: de versie en de rol. Hij vroeg de sectie server, en role staat niet in die sectie. Hij staat in replication. Het veld was leeg sinds de ronde die het invoerde, en niets zei dat, want de stub antwoordde met een INFO-blok dat wij zelf geschreven hadden en waarin role behulpzaam stond waar wij hem verwachtten.

De reparatie is twee losse INFO-aanroepen en niet één aanroep die beide secties noemt, want Redis accepteert meerdere secties in één INFO pas vanaf versie 7.0. Op 6.x is dat een syntaxfout, en omdat een mislukte INFO de check bewust niet rood maakt zou het veld op oudere servers gewoon opnieuw leeg blijven. Een stille degradatie die een stille degradatie vervangt.

Beide fouten hebben dezelfde vorm. Geen van tweeën leverde een fout op. De ene gaf de verkeerde fout en de andere gaf helemaal niets, en allebei waren ze onzichtbaar voor een testsuite die groen stond.

MySQL, en de crate die er niet bij kwam

Dezelfde ronde voegde MySQL en MariaDB als checksoort toe. Er zit geen clientbibliotheek achter, en dat is een bewuste herhaling van een eerder besluit en geen nieuw besluit.

Voor PostgreSQL was de keuze om geen eigen SCRAM te schrijven, want SCRAM kent een stap waarin de server iets ondertekent dat de client hoort te verifiëren, en een implementatie die die stap overslaat is groen tegen een echte server en even groen tegen een vervalste. Dat is de gevaarlijke soort kortsluiting: hij faalt nooit luidruchtig.

MySQL kent zo'n stap niet. Native wachtwoordauthenticatie is een keten van SHA1-hashes en het snelle pad van caching_sha2_password is een keten van SHA256-hashes, en in geen van beide ondertekent de server iets dat wij zouden kunnen nalaten te controleren. Wat overblijft is framing plus twee hashes, en beide hashcrates stonden al in de boom. Een querybouwer was ook niet nodig: er is één query, SELECT 1, hij staat als constante, en zijn verwachte antwoord staat er ook als constante bij.

Dat laatste leest als een ontbrekende functie. Boven een vaste query is een instelbare verwachting een knop waarvan elke stand behalve de standaard de check laat falen, en zo'n knop leert een beheerder dat knoppen niets betekenen. Hij hoort er te zijn op de dag dat er een tweede vaste query bestaat en geen dag eerder. De versie is ook geen tweede query: MySQL stuurt hem ongevraagd mee in het handshakepakket, en de tweede query is altijd degene waar over een jaar een vrij tekstveld aan groeit.

Twee dingen worden ronduit geweigerd. De clientcapability waarmee een server de client kan opdragen een bestand van diens schijf te lezen en op te sturen wordt niet aangevraagd, en de bijbehorende antwoordbyte is een protocolfout in plaats van een tak die iets doet. En volledige caching_sha2-authenticatie gebeurt uitsluitend over TLS: het alternatief op een kale lijn is de publieke sleutel van de server ophalen over diezelfde ongeauthenticeerde lijn, en wie ertussen zit beantwoordt dat met zijn eigen sleutel. De weigering noemt de reparatie, en die is TLS aanzetten.

Een statuspagina telt ticks, geen resultaten

Dezelfde ronde zette een publieke statuspagina voor dit alles, en het rekenwerk daarop vroeg één besluit dat makkelijk fout gaat.

Elke verschuldigde uitvoering van een regel is een tick. Een tick kan eindigen als up, degraded of down, en hij kan ook eindigen als source_unavailable (onze eigen meetbron was er niet), configuration_error (de regel deugde niet), unsupported (de agent kende dat checktype niet) of gemist. Die laatste vier zeggen niets over de dienst van de klant.

Zou de noemer van een dagbeschikbaarheid élke verschuldigde tick zijn, dan verlaagde een storing van ons het cijfer op de statuspagina van de klant. De noemer is daarom up plus degraded plus down: de ticks waarin er werkelijk naar de dienst gekeken is. Blijft daar niets van over, dan is het antwoord voor die dag dat er geen antwoord is, nadrukkelijk niet nul en nadrukkelijk niet honderd procent. Op de pagina zelf worden die vier standen als onbekend getoond en nooit als down.

Degraded telt half mee. Dat is een keuze en geen natuurwet: een dienst die de hele dag antwoordt maar boven zijn latentiedrempel zit is niet uit de lucht en niet in orde, en hem naar één van beide kanten tellen maakt een van de twee betekenisloos.

Gaten worden als gaten getoond. Een regel die vorige week is aangemaakt heeft geen negentig dagen historie, en een balk die dat opvult met de waarde van de buurdag beweert iets dat nooit gemeten is.

Wat een groene testsuite ons gekost heeft

Elk van de defecten hierboven zat onder een geslaagde test. De stub-suite was niet fout; hij beantwoordde een andere vraag dan iedereen dacht. De waarde van de canary is niet dat hij een betere proef is. Het is dat hij een proef is waarbij de andere kant van de lijn geschreven is door iemand die onze aannames niet deelt.

Dat is zes wegwerpcontainers waard, en het is de moeite waard om op te schrijven dát ze wegwerp zijn: ze draaien op onze eigen machine, ze worden verwijderd zodra ze stoppen, en de proeven weigeren te draaien als de adressen er niet zijn.

Uit het monitoringwerk van Provibr. Waarom een gehoste probe precies één soort bericht aanneemt staat in een probe die zich nergens toe laat overhalen, en waarom een monitoringgeheim maar één keer reist staat in een geheim dat één keer reist.

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.