Skip to content

fix(kepler): SSH-poort alleen bereikbaar vanaf de gateway - #101

Open
ericwout-overheid wants to merge 14 commits into
experiment/kepler-ssh-sandboxfrom
fix/kepler-ssh-firewall-gateway
Open

fix(kepler): SSH-poort alleen bereikbaar vanaf de gateway#101
ericwout-overheid wants to merge 14 commits into
experiment/kepler-ssh-sandboxfrom
fix/kepler-ssh-firewall-gateway

Conversation

@ericwout-overheid

@ericwout-overheid ericwout-overheid commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Gestapeld op #92. Sluit het eerste openstaande punt uit ADR 0001 §4.2 over de sshd.

Wat er wijzigt

init-firewall.sh accepteerde inbound vanaf het hele bridge-subnet zonder poortfilter:

iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT

Daardoor bereikt elke andere container op hetzelfde compose-netwerk de SSH-poort rechtstreeks, langs de 127.0.0.1-publish om. Er staat nu een DROP vóór die regel die inbound naar die poort beperkt tot de gateway, alleen bij ENABLE_SSHD=true.

Het poortnummer komt uit de Port-directive in /etc/ssh/sshd_config.d/kepler.conf — dezelfde drop-in die sshd zelf leest — met sshd's eigen default 22 als die directive er niet staat. Eén bron dus, en geen aparte environment-variabele die stil uit de pas kan lopen: viel de firewall terug op 22 terwijl sshd elders bindt, dan stond de echte poort open voor het hele subnet, mét een geruststellende regel in het log.

De smoke-test asserteert positie, -p tcp, de exacte bronbeperking en de overeenkomst met sshd -T; README en ADR beschrijven dit niet langer als open restrisico.

Waarom gestapeld en niet in #92

Dit raakt init-firewall.sh, dat álle sandbox-gebruikers deelt, en de aanname "een gepubliceerde poort komt binnen vanaf de gateway" is platformafhankelijk. Los te testen en los terug te draaien is hier meer waard dan het meenemen in de grote PR.

Wat dit níet dekt

Bron-IP-filtering op een gedeelde bridge is geen authenticatie. Een container met NET_RAW kan de gateway spoofen, een met NET_ADMIN bereikt hetzelfde met een SNAT-regel; beide blijven blind, maar voor het openen van een sessie is dat genoeg. De uitzondering laat bovendien élk pakket toe met het bridge-adres als bron, niet alleen de port-forward. De drempel gaat omhoog, de poort gaat niet dicht — zo staat het ook in de README.

Geverifieerd

  • shellcheck --severity=warning op init-firewall.sh en kepler/smoke-test.sh: schoon
  • De regel echt aangemaakt in een netwerk-namespace (unshare --map-root-user --net) en de assertielogica tegen de gerenderde iptables -S-uitvoer gedraaid: de bron staat vóór -p tcp, de positiecontrole klopt, en een DROP zonder ! -s of op UDP wordt rood
  • De awk-extractie van de Port-directive tegen een drop-in mét en zonder die regel

Expliciet NIET geverifieerd

  • Geen image gebouwd en geen container gedraaid. De dragende aanname is dat de gepubliceerde poort binnenkomt vanaf het gateway-IP. Dat klopt bij Docker met userland-proxy, maar podman of gvproxy op macOS kan de bronadressen anders presenteren. Werkt dat daar anders, dan blokkeert deze regel juist de legitieme Kepler-verbinding; de smoke-test noemt dat als mogelijke oorzaak bij een login die tot de timeout blijft hangen.
  • Te testen op beide platforms voor merge: Linux/Docker én de macOS podman-machine. ./kepler/smoke-test.sh -i ~/.ssh/kepler moet groen blijven, inclusief de nieuwe assertie.
  • Een negatieve test (een tweede container op hetzelfde netwerk die de SSH-poort niet meer mag bereiken) zit niet in de smoke-test.

🤖 Generated with Claude Code

init-firewall.sh accepteerde inbound vanaf het hele bridge-subnet zonder
poortfilter, waardoor elke andere container op datzelfde compose-netwerk poort 22
rechtstreeks bereikte — langs de 127.0.0.1-publish om. Een gepubliceerde poort
komt binnen vanaf de gateway, dus die blijft werken.

De regel staat er alleen bij ENABLE_SSHD=true; zonder sshd luistert er niets.
Smoke-test asserteert de regel, en README en ADR beschrijven het restrisico niet
langer als open.
…n bron

De regel pinde poort 22 hard. Verplaatst een latere wijziging sshd naar een
andere poort — zoals #103 doet — dan beschermt de DROP een dichte poort en staat
de nieuwe open voor het hele bridge-subnet, zonder dat de smoke-test daarover
valt. SSHD_PORT is nu de enige plek waar het nummer staat; de kepler-override
geeft hem door.

De gate op ENABLE_SSHD vervalt: een DROP op een poort waar niets luistert kost
niets, en default-deny hoort niet van een runtime-vlag af te hangen.

De smoke-test toetste alleen of er ergens een DROP-regel stond. Een regel ná de
subnet-ACCEPT doet niets en een regel zonder bronuitzondering sluit juist
iedereen buiten; beide bleven groen. Nu worden positie en bronbeperking apart
getoetst, met een eigen melding per geval, en de fout van het uitleescommando
wordt niet meer weggegooid.

README en ADR zeggen niet langer dat een buurcontainer er niet bij kan: bron-IP
op een gedeelde bridge is een drempel, geen authenticatie — een container met
NET_RAW kan de gateway spoofen.
De assertie las SSHD_PORT uit dezelfde env die init-firewall.sh gebruikt en
vergeleek de firewall dus met zichzelf. Belangrijker: kepler.conf bevat geen
Port-directive, dus sshd luistert op de OpenSSH-default en SSHD_PORT is nergens
aan gekoppeld. Verplaatst een latere wijziging sshd naar een andere poort, dan
beschermt de regel een dichte poort terwijl de echte openstaat — en de test meldt
groen.

De poort komt nu uit sshd -T, en de test vergelijkt die met SSHD_PORT. Een
verschil is precies het scenario waarvoor de variabele bestaat.

Verder:
- De bronbeperking wordt tegen het echte gateway-adres gehouden. ! -s met een
  heel subnet of een verkeerd adres slaagde eerder ook.
- Een gefaalde exec valt niet meer stil terug op poort 22.
- SSHD_PORT wordt gevalideerd zoals de andere externe waarden in het script; een
  range als 1:65535 zou anders alle inbound TCP dichtzetten.
- De grep koppelt --dport en -j DROP niet meer aan elkaar in één patroon, zodat
  een extra match in de regel de check niet stil laat missen.
De match op de DROP-regel eiste geen -p tcp, dus een UDP-DROP op dezelfde
poort zou geslaagd zijn terwijl TCP open bleef voor het hele bridge-subnet.

Een gefaalde sshd -T gooide zijn foutmelding weg, waardoor de assertie de
reden nooit kon noemen.

De README benoemt dat deze regel Kepler zelf blokkeert als de port-forward
met een ander bronadres binnenkomt — een hangende verbinding is dan het enige
symptoom.
ericwout-overheid added a commit that referenced this pull request Aug 6, 2026
Door het verplaatsen van het authorized_keys-blok naar vóór de sshd-start is de
status op dat punt altijd ready; running wordt pas verderop gezet. De INFO-tak
was daarmee onbereikbaar en elke geslaagde start printte een waarschuwing die
verwees naar een eerdere melding die niet bestaat. Het onderscheid met een echt
mislukte start was daarmee ook weg.

Verder:
- SSHD_PORT=2222 in de kepler-override, gelijk aan de Port-directive. Zonder dat
  beschermt de firewallregel uit #101 na samenvoegen een dode poort terwijl 2222
  open staat voor het hele bridge-subnet.
- De build eist precies één port-regel: Port is cumulatief, dus een tweede regel
  zou sshd op twee poorten laten luisteren terwijl beide controles groen blijven.
- Er is weer een assertie dat een geslaagde login gelogd wordt, met een offset
  zodat een event van een vorige start niet meetelt. Die was met het oude
  logbestand verdwenen, terwijl dat juist de reden voor -e is.
- Het comment over het venster claimt niet meer dat er geen pad is waarop sshd
  zonder sleutel luistert; dat pad bestaat, het is alleen blijvend in plaats van
  een venster.
- ADR noemt de derde prijs: het auth-spoor wordt vervalsbaar, want sshd schrijft
  als claude naar dezelfde stroom als claude zelf.
Met `sshd -T 2>&1 | awk` gaan de foutregels de awk in, die alleen regels
doorlaat die op ^port matchen — de melding bleef dus leeg terwijl de vorige
commit-boodschap beweerde dat de reden zichtbaar werd. De exitcode van een
pipeline is bovendien die van awk en dus altijd 0; alleen de regex ving het.

De ruwe uitvoer wordt nu eerst opgevangen en pas daarna gefilterd.
De exitcode van een commando-substitutie met een pipe of een awk is die
van het laatste onderdeel, en awk faalt hier nooit — de guard eromheen
kon dus niet vuren. Het filteren staat nu los van het ophalen, en de
regex-check heeft een eigen tak met de ruwe uitvoer erin.

printenv geeft een lege string en exitcode 1 als de variabele niet gezet
is, en een lege string bij een gefaalde exec; die twee waren niet te
onderscheiden. De fallback op 22 gebeurt nu in de container, gelijk aan
wat init-firewall.sh doet, zodat een lege uitkomst alleen nog een echte
fout kan zijn.
…irewall

De README noemde rootless podman met pasta als runtime die de port-forward met
een ander bronadres doorzet. Dat is niet nagemeten, en pasta zet host-loopback
standaard juist om naar het gateway-adres. De voorwaardelijke formulering
blijft, de runtime-naam gaat eruit.

De smoke-test gebruikte ${SSHD_PORT-22} waar init-firewall.sh ${SSHD_PORT:-22}
gebruikt: bij een lege waarde draaide de firewall op 22 terwijl de test een
afwijking meldde.
SSHD_PORT en de Port-directive waren twee bronnen voor hetzelfde nummer. Liepen
ze uit elkaar — een eigen override, een .env, een afgeleide compose die
ENABLE_SSHD wel zet en SSHD_PORT niet — dan viel de firewall terug op 22, stond
de echte poort open voor het hele bridge-subnet, en meldde het log dat SSH tot
de gateway beperkt was. Fail-open met een succesmelding.

init-firewall.sh leest nu de Port-directive uit dezelfde drop-in die sshd leest,
met sshd's eigen default 22 als die er niet staat. De env-variabele en de
vergelijking ertegen in de smoke-test vervallen; de vergelijking met `sshd -T`
blijft.

De README suggereerde dat NET_RAW de enige spoofing-route is en dat de
bronuitzondering alleen de port-forward toelaat.
Port is cumulatief in sshd_config: elke regel voegt een luisterpoort toe. De
extractie nam alleen de eerste, dus bij twee Port-regels beschermde de firewall
er één en stond de andere open voor het hele bridge-subnet. De smoke-test had
dezelfde beperking en werd daar groen. Beide lopen nu over alle poorten.

sshd accepteert Port 22 en Port=22, hoofdletterongevoelig; de = -vorm werd niet
herkend en viel stil terug op 22. Waarden worden nu ook met 10# genormaliseerd,
zodat Port 0022 net als bij sshd 22 oplevert in plaats van een harde fout die de
container niet laat starten.

De toewijzing van drop_line en subnet_line hing aan een grep die bij afwezigheid
rc 1 geeft; met pipefail legde dat het script neer op precies de regressie die
de takken eronder moesten melden, vóór de resterende secties en zonder
dump_logs.

De INFO-regel bij een geslaagde start noemde poort 22 als vaste waarde, wat weer
een tweede bron voor hetzelfde nummer was.
… SSH

sshd luistert ook op poorten die uit een ListenAddress met expliciete poort
komen, en doet Include over alle drop-ins in sshd_config.d. De extractie keek
alleen naar Port in kepler.conf, dus een ListenAddress 0.0.0.0:2200 leverde een
open poort zonder DROP op — met een groene smoke-test erbij, want die had
dezelfde beperking. Beide lezen nu ook listenaddress; een kaal IPv6-adres telt
niet mee, want dat is een adres en geen poort.

De parse zat buiten elke voorwaarde en liet bij een ongeldige waarde de hele
container niet meer starten, ook met ENABLE_SSHD uit. Hij draait nu alleen als
sshd daadwerkelijk aangezet is.

De README beschreef het restrisico verkeerd: blind spoofen krijgt de TCP-
handshake niet rond en opent dus geen sessie. De aanval die wel werkt is
ARP-poisoning door een buurcontainer met NET_RAW, en die is niet blind.
… ENABLE_SSHD af

Het comment beloofde dat de regels onvoorwaardelijk gezet worden en de code zette
er twee alinea's verderop een ENABLE_SSHD-gate omheen. Beide punten zijn geldig:
default-deny hoort niet van een runtime-vlag af te hangen, en een parse-fout in
een config die niemand gebruikt hoort de container niet onstartbaar te maken.

De regels worden nu altijd gezet. Alleen een onbruikbare poortwaarde valt anders
uit: fataal zodra sshd echt aanstaat, want dan is niet vast te stellen welke
poort beschermd moet worden; een waarschuwing met overslaan als sshd uit staat.

sshd_config zelf wordt meegelezen: Port is cumulatief over bestandsgrenzen heen,
dus een directive daar telt op bij die uit de drop-ins.
…wall-gateway

# Conflicts:
#	claude-sandbox/entrypoint-root.sh
#	claude-sandbox/kepler/smoke-test.sh
ericwout-overheid added a commit that referenced this pull request Aug 7, 2026
* experiment(kepler): sshd als claude op poort 2222, geen root-daemon

Poort 2222 vereist geen root, dus sshd start na de privilege-drop als claude. Er
draait daarmee geen root-daemon in de container: een pre-auth-lek in OpenSSH
levert claude op in plaats van container-root, en de setpriv-bounding-set is niet
langer nodig omdat het proces sowieso geen capabilities heeft.

De prijs is dat OpenSSHs eigen privilege separation vervalt; die vereist root om
het pre-auth-proces af te splitsen.

De host-key blijft van root, nu 640 root:claude: sshd moet hem kunnen lezen maar
de ingesloten partij mag hem niet vervangen. Pidfile verhuist mee naar een pad
dat claude kan schrijven.

Smoke-test keert de root-assertie om en toetst een lege effectieve
capability-set; de tunneltest wijst naar de nieuwe poort.

* fix(kepler): UsePAM uit, pidfile naar /run, eerlijk over wat de opzet kost

UsePAM stond nog op de Debian-default yes. sshd_config(5) stelt dat je sshd met
UsePAM aan niet als niet-root kunt draaien: de sessiemodules verwachten root. Dat
is de dragende aanname van deze PR, dus de drop-in zet hem nu expliciet uit en de
build asserteert dat.

De pidfile-directory was 750 root:claude. Groep krijgt daarmee r-x, geen
schrijfrecht, terwijl mijn eigen comment eronder zei dat sshd er zijn pidfile
moest schrijven. Gevolg: sshd luistert, de wachtlus loopt vol, en de operator
leest starten mislukt. De pidfile staat nu in /run/sshd-claude, dat claude bezit;
.ssh-host blijft 700 root:root, want schrijfrecht daar zou betekenen dat de
host-key te unlinken en vervangen is. De opruiming bij een mislukte start is
terug.

Smoke-test: de poortbinding-check keek nog naar container-poort 22, die niet meer
bestaat — de belangrijkste security-assertie faalde dus altijd. Ook de assertie
op het inmiddels dode /var/log/sshd.log is weg, de capability-check toetst weer
permitted naast effective, en een ontbrekend sshd-proces krijgt een eigen melding
in plaats van draait hij toch als root.

README en ADR benoemen nu de tweede prijs van deze opzet: claude kan de host-key
lezen. Dat is genoeg om sshd te doden, zelf op dezelfde poort te binden met
diezelfde sleutel en zonder known_hosts-mismatch door te gaan — waarmee ook
PermitOpen en AllowTcpForwarding onder controle staan van de partij die ze zouden
moeten beperken. Dode chgrp/chmod op de wegwerpsleutel in de Dockerfile zijn weg.

* fix(kepler): host-key-directory doorloopbaar houden voor claude

Ik had de directory bij het oplossen van het pidfile-probleem teruggezet naar
700 root:root. Zonder x-bit voor de groep kan claude er niet doorheen en dus de
host-key niet openen, ongeacht de 640 op het bestand zelf — sshd zou niet
starten.

750 root:claude is de juiste stand: doorlopen en lezen mag, aanmaken en unlinken
niet. Dat laatste is waarom het geen 770 wordt; de pidfile staat daarom in /run.

* fix(kepler): sleutel vóór de sshd-start, rechten kloppend gedocumenteerd

De README beschreef de host-key-directory als 700 root:root. Dat is precies de
stand waarin sshd als claude niet start — nagemeten op debian 13 met OpenSSH
10.0p2: no hostkeys available, geen pidfile. De code zet 750 root:claude; de
README zei het tegenovergestelde en nodigde uit om terug te hardenen naar een
opzet die stil breekt.

authorized_keys wordt nu geschreven vóór sshd start. Beide stappen staan in
hetzelfde script, dus het venster waarin sshd luistert zonder dat er een sleutel
staat is te vermijden in plaats van te documenteren.

Verder:
- De smoke-test toetste eigenaar en mode maar niet de groep, terwijl de groep
  juist het dragende mechanisme is: 750 root:root zou geslaagd zijn en sshd
  alsnog breken. Nu %U %G %a.
- usepam no staat in de runtime-directive-lijst. Met UsePAM aan bindt sshd de
  poort gewoon en breekt hij pas bij de sessie-opzet, dus dit is de enige plek
  waar zo n regressie zichtbaar wordt.
- De pass-tekst zei nog 0700 waar de assertie 750 toetst.
- Vier verwijzingen naar de root-fase-opzet rechtgezet, en het comment over /run
  als verse tmpfs: dat geldt onder podman, niet onder Docker — de rm in
  entrypoint.sh doet het werk.
- De diagnose bij een mislukte start noemt ook de directory-rechten.
- ADR-sectie staat niet langer onder de rootfase, en de PR-coördinatie is uit de
  README gehaald: die beschrijft de eindtoestand.

* fix(kepler): geslaagde start meldde ten onrechte dat sshd niet luistert

Door het verplaatsen van het authorized_keys-blok naar vóór de sshd-start is de
status op dat punt altijd ready; running wordt pas verderop gezet. De INFO-tak
was daarmee onbereikbaar en elke geslaagde start printte een waarschuwing die
verwees naar een eerdere melding die niet bestaat. Het onderscheid met een echt
mislukte start was daarmee ook weg.

Verder:
- SSHD_PORT=2222 in de kepler-override, gelijk aan de Port-directive. Zonder dat
  beschermt de firewallregel uit #101 na samenvoegen een dode poort terwijl 2222
  open staat voor het hele bridge-subnet.
- De build eist precies één port-regel: Port is cumulatief, dus een tweede regel
  zou sshd op twee poorten laten luisteren terwijl beide controles groen blijven.
- Er is weer een assertie dat een geslaagde login gelogd wordt, met een offset
  zodat een event van een vorige start niet meetelt. Die was met het oude
  logbestand verdwenen, terwijl dat juist de reden voor -e is.
- Het comment over het venster claimt niet meer dat er geen pad is waarop sshd
  zonder sleutel luistert; dat pad bestaat, het is alleen blijvend in plaats van
  een venster.
- ADR noemt de derde prijs: het auth-spoor wordt vervalsbaar, want sshd schrijft
  als claude naar dezelfde stroom als claude zelf.

* fix(kepler): offset-guard kon niet vuren, dode statuswaarde opgeruimd

De guard rond de offsetmeting hing aan de exitcode van een pipeline, en die is
die van wc — altijd 0. De fail-tak en de lege-offset-afhandeling waren daarmee
dood, precies de klasse fout die de vorige commit in de INFO-tak repareerde. De
logs-aanroep staat nu buiten de pipe.

running is op dit punt onbereikbaar: de root-fase geeft alleen disabled,
absent, invalid, failed of ready door, en running wordt pas verderop gezet. Uit
de case en de conditie gehaald zodat er geen tak overblijft die niet kan vuren.

Twee ADR-verwijzingen stonden nog op 2.3.4, en het comment in de override
verwees naar een PR-nummer in plaats van naar het script dat het gedrag levert.

* fix(kepler): tail-tak weghalen die niet kon vuren

tail op een herestring faalt niet, dus de fail-tak eromheen was onbereikbaar.

* fix(kepler): dode statustoekenningen weg, comments gelijk aan de branch

SSHD_STATUS wordt na de start nergens meer gelezen; de twee toekenningen in dat
blok waren dode stores.

Het comment in de override beschreef een firewallregel die op deze branch niet
bestaat, terwijl README en ADR in dezelfde branch het tegenovergestelde zeggen.
De variabele stuurt hier alleen de poortkeuze.

UsePAM no ontbrak in de hardening-opsomming, terwijl die directive de
voorwaarde is voor de hele niet-root-opzet en de smoke-test hem bewaakt.

* fix(kepler): prijs van de niet-root-opzet volledig opschrijven

De prijs van het wegvallen van privilege separation stond als "de scheiding
tussen pre- en post-auth vervalt". Concreet betekent het dat pre-auth-code
ongechroot als claude draait, met de host-bindmount, de login-credentials
en de container-env binnen bereik, in plaats van in een lege chroot als de
user sshd. De ruil is tweezijdig en de verliesrichting stond er niet.

Het auth-spoor staat niet alleen onder controle van sshd zelf: elk proces
van claude kan via /proc/<pid>/fd/1 in dezelfde stroom schrijven. Het ADR
noemde beide restrisico's niet, en claimde bij de host-key een controle die
alleen rotatie afdekt, geen geheimhouding.

rm -f op het pidfile faalt als daar een directory staat, en claude mag in
die tmpfs-loze directory schrijven; met errexit werd dat een herstartlus.

* docs(kepler): claims gelijktrekken met wat de code waarmaakt

De build-guard op meerdere Port-regels motiveerde zich met een firewallregel die
op deze branch niet bestaat. De guard blijft terecht: Port is cumulatief, dus
een tweede regel laat sshd ook op de niet-bedoelde poort luisteren.

De pidfile-route is niet de enige faalroute die de container meeneemt: een fifo
op het publieke host-key-pad blokkeert de root-fase net zo goed.

De host-key-garantie zat toegeschreven aan de directory-mode. Wie schrijfrecht
op /home/claude heeft kan de hele directory hernoemen; wat dat afvangt is de
controle op type en eigenaar bij elke start.

Het comment over de taakverdeling tussen de fasen beschreef nog de opzet waarin
de root-fase sshd zelf startte.

* fix(kepler): API-sleutel uit sshd, bounding set weer bewaakt, gaten benoemd

De pre-auth-code draait hier ongechroot als claude; zonder env-scrub staat de
API-sleutel in de omgeving van precies dat proces.

De capability-assertie toetste alleen permitted en effective. In de root-variant
verkleint setpriv de bounding set van sshd; hier gebeurt dat niet, en sinds de
assertie CapBnd losliet bewaakte niets meer wat sshd daaraan meekrijgt. Hij
wordt nu vergeleken met die van PID 1, zodat een verruiming opvalt zonder een
vaste waarde vast te leggen.

Twee dingen die in de beperkingen ontbraken: de root-variant weigert sshd te
starten als het auth-logbestand niet aan te maken is, en die eis bestaat hier
niet. En het overnemen van de host-identiteit reikt verder dan de eigen
container: in een zelf opgezette sshd staat AllowAgentForwarding aan, dus met
ForwardAgent aan de andere kant komt de ingesloten partij bij de SSH-agent op de
host.

* fix(kepler): assertie weghalen die alleen op hardening kan vuren

De CapBnd-vergelijking met PID 1 kon niet falen zoals bedoeld: een bounding set
kan niet groeien, hij wordt bij fork geërfd en blijft over execve staan, en sshd
start hier als plain child zonder setpriv ertussen. De enige manier om hem te
laten afwijken is sshd juist verkleinen — precies de hardening die de
root-variant heeft. Een guard die de verbetering afstraft is erger dan geen
guard. De controle op permitted en effective blijft; die vangt wel iets.

De API-sleutel-bullet zei dat de sleutel een sshd-sessie niet bereikt. In deze
opzet draait de pre-auth-code ongechroot als claude, dus de env-scrub verplaatst
het gat één /proc-lezing verderop in plaats van het te sluiten.

* docs(kepler): premisse bij de capability-assertie kloppend maken

Een bounding set kan wel degelijk groeien, namelijk in een nieuwe
user-namespace — en dat is precies wat rootless podman in deze image doet. Voor
sshd geldt het niet, want die unshared er geen; de conclusie blijft dus staan,
de onderbouwing niet.

* docs(adr): niet-root sshd uitgewerkt en verworpen, implementatie eruit

Deze branch bevatte de uitwerking van een sshd die ná de privilege-drop als
claude op poort 2222 draait, om het punt 'de daemon draait als root' uit ADR
0001 par. 4.2 te adresseren. Bij review viel de ruil de verkeerde kant op:
OpenSSH splitst zijn pre-auth-proces alleen af als het als root start, dus
zonder root komt pre-auth-code direct uit als claude — met de host-bindmount en
de credentials binnen bereik, in plaats van in een lege chroot. In een sandbox
die juist claude moet insluiten weegt dat zwaarder dan geen root-daemon hebben.

De zorg achter het oorspronkelijke punt was bovendien al afgedekt: de bounding
set van de daemon is verkleind, dus een lek levert geen container-root op die de
firewall kan flushen.

Wat overblijft is de afweging zelf, vastgelegd bij de verworpen alternatieven,
zodat navolgbaar blijft waarom de daemon als root draait.
@ericwout-overheid
ericwout-overheid marked this pull request as ready for review August 7, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant