fix(kepler): auth-log naar het volume, niet naar de container-laag - #102
Merged
ericwout-overheid merged 19 commits intoAug 7, 2026
Merged
Conversation
/var/log/sshd.log stond op de container-laag: een recreate wiste hem en er was geen rotatie. Met -D -e gaan de auth-events naar de containerlog, waar de rotatie van de logdriver geldt en de operator toch al kijkt. -D betekent dat sshd niet daemoniseert, dus hij gaat met & naar de achtergrond. De exit-code zegt daarmee niets meer over succes; de bestaande wachtlus op /run/sshd.pid blijft het signaal, aangevuld met een kill -0 op het proces. Smoke-test toetst nu op sshd-regels in de containerlog in plaats van op de rechten van een bestand dat niet meer bestaat.
ericwout-overheid
requested review from
jonrust-minbzk,
loek-rijksoverheid and
mreuvekamp
as code owners
August 6, 2026 09:42
ericwout-overheid
marked this pull request as draft
August 6, 2026 09:48
De containerlog was de verkeerde bestemming, om twee redenen die bij het reviewen naar boven kwamen. De aanleiding klopte niet: een containerlog bestaat per container, dus een recreate wist hem net zo goed als een bestand op de container-laag. Het probleem dat deze wijziging moest oplossen bleef dus bestaan. En het maakte het spoor onbetrouwbaar. Na de privilege-drop schrijft claude zelf naar diezelfde stroom, dus een verzonnen Accepted publickey-regel is in docker logs niet te onderscheiden van een echte, en oudere records zijn weg te duwen. De gelogde partij kon daarmee zijn eigen auth-spoor vervalsen. De log staat nu op het claude-home volume als 640 root:claude: overleeft een recreate, en alleen root schrijft. De smoke-test toetst het echte auth-event (Accepted publickey na de login uit sectie 3) in plaats van de startbanner, plus de rechten van het bestand. De README is eerlijk over wat er niet gedekt is: geen rotatie, en een log die de gelogde partij kan lezen is geen bewijsmateriaal zolang het het enige exemplaar in de sandbox is.
De assertie op Accepted publickey stond in sectie 1b, terwijl de eerste login pas in sectie 3 gebeurt. Op een vers volume gaf dat een vals-rood met een misleidende diagnose — en dat is het standaardpad, want ENABLE_SSHD aanzetten vereist een volume-recreate. Op een hergebruikt volume was het omgekeerde erger: het bestand overleeft een recreate en wordt nooit getrunceerd, dus één geslaagde run hield de check voorgoed groen, ook als de logging daarna gesloopt werd. De assertie staat nu ná de login en telt alleen de regels die sinds een offset van vóór die login zijn bijgekomen. prepare_auth_log had een type-guard op de directory maar niet op het bestand. [[ -f ]] dereferencet, dus een symlink op die plek liet het auth-spoor ergens anders belanden en zette root een bestand naar keuze op root:claude 640. claude bezit /home/claude en kan die directory aanmaken voordat sshd voor het eerst start. Nu dezelfde guard als bij de host-key. De faalmelding noemt het pad en de waarschijnlijke oorzaken, en de README claimt niet langer dat claude het spoor niet kan wissen: aanpassen en verwijderen van regels kan hij niet, de directory wegschuiven wel.
Een mislukte meting viel op 0 terug, en 0 betekent tel het hele bestand. Daarmee zou een Accepted publickey van een vorige run de assertie voorgoed groen houden — precies het gat dat de offset moest dichten. Alleen bestand bestaat nog niet levert nu 0 op; een onleesbare meting is een bevinding. De symlink-guard vermeldt dat hij geen hardlink dekt.
De smoke-test toetste alleen het bestand. De 750 op de directory is juist wat claude belet regels te verwijderen, want het bestand staat in zijn eigen home; zakt die ooit terug naar 770 of naar eigenaar claude, dan bleef de check groen terwijl de premisse van deze PR weg is. De oorzakenlijst bij een mislukte start verwees naar de containerlog, een restant van de tussenliggende opzet. sshd schrijft naar SSHD_LOG en daar staat de reden. Het comment claimde dat het spoor niet te verdringen is; de README zegt terecht dat de directory wel weg te schuiven is.
Een pad in /home/claude is root-eigen te maken, maar de ouder is van claude: hij kan de directory hernoemen en er een eigen sshd.log voor in de plaats zetten. sshd schrijft dan door op de open fd, terwijl wie het gedocumenteerde pad leest — inclusief de smoke-test — de vervalsing ziet. Een spoor wegschuiven valt op, een spoor vervangen niet. Het bestand staat nu op een eigen volume onder /var/log, waar alleen root de directory kan hernoemen. OpenSSH schrijft achter -E en -e het formaat "%s%s%.*s\\r\\n": geen datum, geen tijd, geen pid. Een auth-spoor zonder tijd beantwoordt niet wanneer iemand binnenkwam. sshd schrijft daarom naar een fifo en een root-leesluis zet de tijd ervoor; valt die luis weg, dan blokkeert sshd op een volle fifo en is er geen login zonder spoor.
De rechten-assertie toetst nu ook de ouder van de logdirectory: is die van claude, dan kan hij het hele pad omleggen en leest iedereen die het opvraagt zijn versie, terwijl stat op directory en bestand groen blijft. De login-assertie eist een ISO-8601-tijdstempel voor de regel. Valt de leesluis weg, dan komt de kale OpenSSH-regel in het bestand en zou een grep op 'Accepted publickey' groen blijven zonder dat er nog te herleiden is wanneer de login plaatsvond.
Het comment en de README beweerden dat een weggevallen leesluis de fifo laat vollopen en sshd blokkeert. Dat gebeurt niet: schrijven naar een fifo zonder lezer geeft EPIPE, sshd zet SIGPIPE op SIG_IGN en gooit de returnwaarde van write() weg, dus regels van de daemon verdwijnen geruisloos. Nieuwe verbindingen heropenen het logpad en blokkeren wel, vóór authenticatie — SSH valt dan stil zonder melding, en de ingesloten partij kan dat uitlokken door het volume vol te schrijven. Dat staat nu bij de beperkingen in plaats van als garantie. claude-home is external en sshd-log niet, dus docker compose down -v wist het auditspoor en laat de data staan. De README benadrukte alleen de andere kant. Verder: /run is onder Docker geen tmpfs, en een laatste regel zonder afsluitende newline viel uit de leesluis weg.
… rechtzetten De bullet zei dat de sleutel een sshd-sessie niet bereikt. Dat is waar over sshd's env-opbouw, maar elk proces van claude draagt de variabele en een sessie is dezelfde uid. De beschreven trigger klopte niet: claude heeft geen schrijfrecht op de logdirectory en kan het volume niet rechtstreeks vullen. Wat wel geldt en scherper is: één mislukte schrijfactie beëindigt de luis definitief, want hij erft errexit.
Niet elk proces van claude draagt de variabele: de SSH-sessie juist niet, en dat is precies waarom een aanvaller een ander proces moet uitlezen.
# Conflicts: # claude-sandbox/README.md # claude-sandbox/entrypoint-root.sh
sshd schreef met -E naar een fifo waar een bash-leesluis de tijdstempel bijzette. Dat leverde een tijd op, maar met een prijs: één mislukte schrijfactie doodde de luis definitief, waarna regels van de daemon geruisloos verdwenen en nieuwe verbindingen blokkeerden op het openen van de fifo — vóór authenticatie, dus SSH viel stil zonder melding. De keten kon dat bovendien niet afkeuren, want de functie eindigde op een achtergrondjob en gaf altijd 0 terug. busybox syslogd doet waar hij voor bestaat: hij zet de tijd erbij en roteert op grootte, en als hij wegvalt blokkeert hij sshd niet, want syslog() schrijft naar een socket en is best-effort. De start controleert dat /dev/log er is voordat sshd begint, anders zouden de eerste regels alsnog verdwijnen. De padbescherming blijft ongewijzigd: eigen volume onder een root-eigen /var/log, 640 root:claude in een 750 root:claude-directory. Twee dingen die deze opzet niet oplost en die nu bij de beperkingen staan: een weggevallen syslogd is stil, en het syslog-formaat draagt geen jaartal.
De syslog-variant liep op twee dingen vast die los staan van de tijdstempel. busybox syslogd leest zonder -f zijn eigen /etc/syslog.conf, en dat bestand komt mee met het pakket; matcht daar een regel, dan schrijft hij daarheen en slaat de -O-bestemming over, waarmee het spoor op de container-laag belandt in plaats van op het volume. En de socket wordt bij het binden op 0666 gezet, dus elk lokaal proces kan er regels in schrijven met een zelfgekozen tijdstempel — precies wat deze opzet moet uitsluiten. Beide zijn te ondervangen, maar niet zonder het rechtenmodel van syslog te verbouwen, en dat hoort niet in een wijziging over de logbestemming. sshd schrijft daarom rechtstreeks met -E naar het root-eigen bestand: dat houdt de invariant die deze wijziging beoogt, zonder daemon, socket of routeringsconfig. De prijs is dat de regels geen tijd dragen. Dat staat nu expliciet bij de beperkingen, met de reden erbij: OpenSSH laat de tijd aan syslog over. De smoke-test toetst nu de key-fingerprint in plaats van een tijdstempel — dat is de enige identificatie die het spoor draagt, want het bron-IP is door NAT altijd de gateway.
De controle op de key-fingerprint pretendeerde te bewaken dat LogLevel VERBOSE in de drop-in staat. Dat kan hij niet: OpenSSH verhoogt het niveau naar INFO zodra de authenticatie slaagt en plakt de fingerprint hoe dan ook aan die regel, dus de tak vuurt nooit voor het scenario in zijn eigen foutmelding. De directive wordt nu getoetst waar dat wel kan — in de build tegen sshd -T, en in de sectie van de smoke-test die de effectieve config nagaat. De fingerprint-controle blijft, met een melding die zegt wat hij echt aantoont. De ouder van de logdirectory werd op eigenaar getoetst terwijl hernoemen aan de mode hangt: een groep- of other-schrijfbare /var/log laat het hele pad omleggen terwijl stat op directory en bestand klopt. Verder aan de beperkingen toegevoegd: een vol filesystem legt het spoor stil zonder dat sshd dat merkt, en de regels eindigen op CRLF. De ankering van regels via de containerlog gold alleen binnen één containerleven, terwijl het spoor juist een recreate overleeft.
De rechten-assertie toetste alles behalve de eigenschap waar deze wijziging om draait: dat het spoor op een eigen mount staat. Zonder de volume-regel maakt de entrypoint dezelfde directory met dezelfde rechten aan op de container-laag en blijft de hele sectie groen, terwijl het spoor geen recreate meer overleeft. Wat LogLevel VERBOSE toevoegt was te ruim opgeschreven: geweigerde gebruikers en herhaalde mislukkingen staan al op het standaardniveau, en afgebroken verbindingen loggen als fout. Wat er echt bijkomt is de eerste mislukte poging per verbinding van een toegelaten gebruiker, plus de key-probe. De fingerprint-controle is geen guard op de drop-in maar op het formaat; de melding zei het eerste. Het comment bij de ouder-assertie ontkende de helft van wat de code eronder doet — mode en eigenaar tellen allebei. De claim dat /run onder podman een tmpfs is en onder Docker niet, is niet nagemeten en is vervangen door wat wel vaststaat.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gestapeld op #92.
Wat er wijzigt
sshd logde met
-E /var/log/sshd.lognaar de container-laag, en dat bestand is bij een recreate weg. Het auth-spoor staat nu op een eigen volume:/var/log/sshd/sshd.logals640 root:claudein een750 root:claude-directory.Waarom daar, en niet in de containerlog of in
/home/claude--force-recreategeeft een nieuwe — precies het probleem dat deze PR wegneemt. En na de privilege-drop schrijftclaudezelf in die stroom: een verzonnenAccepted publickey for claude …is er niet van een echte te onderscheiden, en oudere records zijn weg te duwen door de log vol te schrijven./home/claude: het bestand is root-eigen te maken, maar hernoemen vereist alleen schrijfrecht op de ouder, en die is vanclaude. Hij kan de directory verplaatsen en er een eigensshd.logvoor in de plaats zetten; sshd schrijft dan door op de open fd, terwijl iedereen die het gedocumenteerde pad leest — inclusief de smoke-test — de vervalsing ziet. Een spoor wegschuiven valt op, een spoor vervangen niet./var/logop een eigen volume: die ouder is niet schrijfbaar voorclaude, dus die omlegging kan daar niet, en het volume zorgt dat het spoor een recreate overleeft. Het staat los vanclaude-home, dus het blijft ook staan als je dat volume weggooit om een environment-variabele te wijzigen.Geen tijdstempel
OpenSSH zet zelf geen tijd in een logregel. De normale weg loopt via
openlog()/syslog(), waarbij alleen de kale boodschap wordt doorgegeven en de syslog-daemon tijd, hostnaam en tag toevoegt; het pad achter-e/-Eschrijft dezelfde boodschap metwrite(), zonder die laag. Er draait hier geen logdaemon, dus de regels dragen geen datum.Een logdaemon toevoegen is geen kwestie van een pakket installeren: een syslog-socket is per ontwerp voor elk lokaal proces schrijfbaar, dus de ingesloten partij zou er zelf regels in kunnen zetten met een zelfgekozen tijd — precies wat deze opzet moet uitsluiten. Dat vraagt een eigen afweging over het rechtenmodel en staat als #110.
Smoke-test
Toetst het auth-event zelf —
Accepted publickey for claudena de geslaagde login uit sectie 3 — en dat de regel een key-fingerprint draagt, want dat is de enige identificatie in het spoor: het bron-IP is door NAT altijd de gateway. Alleen regels die ná de login zijn bijgekomen tellen mee, want het bestand overleeft een recreate.De rechten-assertie dekt bestand, directory én
/var/logzelf, en toetst daar de mode en niet de eigenaar: is die ouder ooit groep- of other-schrijfbaar, dan is het hele pad om te leggen terwijlstatop de eerste twee klopt.LogLevel VERBOSEwordt getoetst waar dat kan — in de build tegensshd -Ten in de config-sectie van de smoke-test. Niet via de fingerprint: OpenSSH verhoogt het niveau naarINFOzodra de authenticatie slaagt en plakt de fingerprint hoe dan ook aan die regel.Wat dit níet dekt
sshd-logvolume, dan is het weg. Let op de asymmetrie metclaude-home: dat volume isexternal,sshd-logniet, dusdocker compose down -vwist juist het spoor en laat de data staan.claudekan dat uitlokken via zijn eigen home, die hetzelfde filesystem deelt.Geverifieerd
bash -nenshellcheck --severity=warningop alle gewijzigde scripts: schoon-E, en dat de fingerprint los staat vanLogLevel, nagelezen in de upstream-bron (log.c,auth.c) — niet uit documentatie afgeleidprepare_auth_log-logica los nagespeeld: verse aanmaak (640), bestaande log blijft behouden, te ruime rechten worden hersteld, en een symlink op de plek van de directory of het bestand wordt geweigerdcompose.override.kepler.ymlmet de volume-toevoeging, en dat de mount niet botst met de bestaande volumes of met de podman-overrideExpliciet NIET geverifieerd
Accepted publickeyis niet tegen echte sshd-output getoetst.claudein de praktijk niet bij/var/log/sshdkan, is beredeneerd uit de rechten en niet in een draaiende container geprobeerd.🤖 Generated with Claude Code