Direct naar inhoud

Twee beweringen die ik heb ingetrokken

Wat er gebeurt als je elke claim in je documentatie aan een draaiende test koppelt: je verliest de claims die niet kloppen.

Nick Aldewereld
it-securityit-toolsdigitale-soevereiniteit

sovereign-nix is een set NixOS-modules die ik open source heb gezet onder de Europese EUPL-1.2. Ze harden een machine, geven elke host een reproduceerbaar andere configuratie, en gooien de systeemroot bij elke boot weg.

Dat is niet het interessante deel. Het interessante deel is dat elke bewering in de documentatie vastzit aan een test die je zelf kunt draaien — en dat de documentatie er net zo expliciet bij zet wat die tests niet bewijzen.

Drie mensen van buiten zijn erin gedoken: Rick Okkersen, Daan Breur en Ryan Theunissen. Twee van mijn eigen beweringen hebben dat niet overleefd. Ik heb ze publiek ingetrokken in plaats van ze stilletjes weg te poetsen, en dit stuk gaat over wat dat kost en waarom het de moeite waard is.

De eerste bewering

Een eerdere versie van de README zei dat dns0.eu “did not answer on port 853 from a major Dutch ISP”. Ik had gemeten, ik had niets teruggekregen, en daar had ik een netwerkconclusie aan gehangen.

Die conclusie droeg de meting niet. De dienst is simpelweg weg. zero.dns0.eu resolvet niet meer en het domein wijst inmiddels naar een parkeerhost. Ik had een dood endpoint gelezen als een netwerkprobleem — en het verschil tussen die twee is precies het verschil tussen een uitspraak over een provider en een uitspraak over een dienst die niet meer bestaat.

Ryan Theunissen ving het. Hij stelde vast dat dns0.eu sinds oktober 2025 gewoon verdwenen is. Zijn naam staat in de README, want dat is de norm in open source en het maakt het punt sterker, niet zwakker.

Wat er nu staat is smaller en beter onderbouwd. Op het moment van schrijven voltooien beide defaults een TLS-handshake op poort 853 met een geldig certificaat, en staat protective.joindns4.eu in de SAN-lijst van het DNS4EU-certificaat — en dat is waar systemd-resolved tegenaan controleert. Daar hoort één zin bij die de hele correctie draagt:

Dit is een feit over één dag, niet een eigenschap van de code.

Een test kan alleen bevestigen wat er geconfigureerd is. Of een endpoint vandaag nog antwoordt, is geen eigenschap van mijn modules. Dat handmatig nagemeten stukje hoort dus in de sectie met wat níet bewezen is, niet in de tabel met claims.

De tweede bewering

De diversiteitsmodule maakt van een per-host zaadje (“seed”) reproduceerbare waarden: de SSH-poort en wat onschuldige kerneltuning. Zelfde zaadje, zelfde waarden. Dat determinisme is het punt — uniciteit die je niet kunt reconstrueren, kun je ook niet auditen.

Wat ik daarnaast aannam: verschillende zaadjes geven verschillende machines. Dat is niet waar, en het is nooit waar geweest.

Die bewering is in twee stukken uit elkaar gehaald, door twee verschillende mensen.

Daan Breur nam het eerste stuk. Zijn punt: een test die aantoont dat twee configuraties van elkaar verschillen, is geen test die een beveiligingseigenschap aantoont. Dat is de fundamentelere observatie van de twee, en precies het soort onderscheid waar dit hele stuk over gaat — mijn test deed exact wat hij beloofde, alleen bewees hij iets anders dan ik ermee suggereerde. De claim staat nu gelabeld naar wat hij daadwerkelijk demonstreert: twee zaadjes, twee configuraties. Niet: twee aanvalsoppervlakken.

Ryan Theunissen nam de andere helft, en die ging over de aanname zelf. De afgeleide waarden leven in een kleine ruimte: 40.000 poorten, 41 swappiness-waarden, 6.145 somaxconn-waarden. Bij zo’n 235 machines is de kans dat érgens in die groep twee hosts dezelfde SSH-poort delen ongeveer 50% — een verjaardagsprobleem, over de hele groep, niet over twee specifieke hosts. Voor twee gegeven machines is die kans ongeveer één op de veertigduizend (de README zelf had hier dezelfde onnauwkeurige formulering staan; die wordt met dit stuk meteen aangescherpt). En volledige botsingen over alle drie de waarden bestaan gewoon: john-balls-14333 en john-balls-94081 geven allebei poort 33670, swappiness 42 en somaxconn 3377.

Hij vond dat paar met brute force. Hij las de code in plaats van de README, wat precies de reden is dat hij het vond.

En dan de zin waar het echt om draait: twee hosts die een poort delen is geen kwetsbaarheid. De verkeerde bewering was het probleem.

Daar zit het hele verschil. Er is niets kapot aan de module. Wat kapot was, was mijn beschrijving ervan — en een beschrijving die meer belooft dan de code waarmaakt is in beveiliging het gevaarlijkste product dat je kunt leveren. Iemand baseert er een keuze op.

Het botsende paar staat nu vastgezet in checks/diversity-lib.nix. De test asserteert dat de botsing er nog steeds is. Niemand kan de oude claim stilletjes terugzetten, ik ook niet.

Beweringen die een build kunnen laten falen

Dat is het mechanisme dat dit herhaalbaar maakt, en eigenlijk het hele punt van dit stuk.

De limieten in sovereign-nix zijn niet alleen proza. Meerdere ervan zijn uitvoerbaar. checks/limits.nix, checks/diversity-lib.nix en checks/impermanence.nix asserteren dat het vervelende ding nog steeds gebeurt. De dag dat een limiet niet meer waar is, faalt de build — in plaats van dat de README stilletjes verouderd raakt.

De meeste projecten laten hun documentatie jarenlang wegdrijven van de werkelijkheid, simpelweg omdat niets die documentatie ooit tegenspreekt. Proza vergaat geruisloos. Hier kan de documentatie een build laten omvallen.

De derde correctie, omdat de techniek te mooi is om weg te laten

Rick Okkersen zag dat het bereik van de afgeleide SSH-poort (20000-59999) overlapt met het ephemeral source-port-bereik van de kernel (32768-60999) — de kernel zou de poort waar sshd op leeft dus nooit zelf moeten uitdelen. Hij wees ook op /home als voor de hand liggend persistentiedoelwit dat de wipe nooit aanraakt. Sindsdien wordt de poort toegevoegd aan net.ipv4.ip_local_reserved_ports.

Waar ik daarna in trapte: ik ging ervan uit dat die reservering ook squatten tegenhield. Dat doet ze niet.

Gemeten op een testhost: een ESTABLISHED uitgaande verbinding op de sshd-poort houdt sshd níet tegen bij een herstart, omdat sshd SO_REUSEADDR zet. Wat sshd wél tegenhoudt, is een lokaal, ongeprivilegieerd proces dat op die poort gaat LUISTEREN terwijl sshd even weg is. Dat kan precies omdat de poort boven 1023 ligt, en ip_local_reserved_ports verhindert het niet: die sluit automatische toewijzing uit, niet een expliciete bind.

Het failure-scenario is bovendien onaangenaam beleefd. Sshd faalt dan op 0.0.0.0, komt alleen op :: omhoog, en systemd rapporteert de unit gewoon als active. Een half-luisterende sshd ziet eruit als een gezonde sshd.

Wat het venster wél sluit is socketactivatie, en die staat nu aan: PID 1 bindt de poort bij boot en overhandigt sshd een kant-en-klare socket. Er valt niets meer te pakken. Het kost een fork per verbinding. checks/limits.nix boot één machine op elke manier en laat een ongeprivilegieerde gebruiker de poort pakken op de oude opzet, en falen op de nieuwe.

Ook dat is een limiet die beide kanten op uitvoerbaar blijft. Niet omdat het netjes staat, maar omdat ik mezelf anders niet vertrouw.

Wat het kost

Een bewering publiceren met de test die haar draagt is een ander vak dan een bewering publiceren. Het is langzamer. Het levert zwakkere zinnen op, want elke zin moet overleven wat de test er daadwerkelijk van maakt. En het kost je claims waar je aan gehecht was — ik vond “elk zaadje een andere machine” een prima zin.

De code staat er open bij: github.com/NickAldewereld/sovereign-nix. Je kunt nix flake check draaien en zelf zien welke uitspraken standhouden en welke ik expliciet niet doe.

Voor wie iemand inhuurt is dit denk ik de bruikbaarste vraag die je kunt stellen: is deze uitspraak ooit tegen een meting aan gehouden, en wat gebeurde er toen? Iemand die zijn eigen bewering intrekt als de meting hem tegenspreekt, heeft een track record. Iemand wiens beweringen nog nooit getoetst zijn, heeft alleen beweringen.

Ik heb er twee verloren. Dat is de prijs van de enige versie die overleeft dat iemand het natrekt.

Wilt u hierover sparren?

Plan een vrijblijvend kennismakingsgesprek en ontdek wat ik voor u kan betekenen.

Plan een gesprek →