Skip to content

docs: handleiding maximale isolatie op Linux (eigen kernel) (#44) - #99

Draft
ericwout-overheid wants to merge 1 commit into
mainfrom
docs/maximale-isolatie-linux
Draft

docs: handleiding maximale isolatie op Linux (eigen kernel) (#44)#99
ericwout-overheid wants to merge 1 commit into
mainfrom
docs/maximale-isolatie-linux

Conversation

@ericwout-overheid

Copy link
Copy Markdown
Collaborator

Wat

Handleiding om de sandbox op Linux in een VM te draaien (podman in Lima of Multipass), met Kata en gVisor als alternatieven. Eén nieuw bestand, geen wijziging aan bestaande.

Waarom apart

Losgetrokken uit #46. De podman-opzet daar deelt de host-kernel, dus een kernel-escape blijft een restrisico — dit document beschrijft hoe je die laag óók sluit. Maar het is een extra stap die niemand nodig heeft om podman te gebruiken, en #46 hoeft er niet op te wachten.

Niet geverifieerd

Geen van de beschreven routes is opgezet of gedraaid. Behandel dit als een startpunt voor wie het uitzoekt, niet als een werkend recept. Dat is ook de reden dat het niet in #46 thuishoort: die PR bevat wel geteste stappen.

De podman-opzet uit #46 deelt de host-kernel, dus een kernel-escape blijft
een restrisico. Dit document beschrijft hoe je die laag óók sluit door de
sandbox op Linux in een VM te draaien (podman in Lima of Multipass), met
Kata en gVisor als alternatieven.

Losgetrokken uit #46 omdat het ongetest is en een extra stap beschrijft die
niemand nodig heeft om podman te gebruiken. Die PR hoeft er niet op te
wachten.

Niet geverifieerd: geen van de beschreven routes is opgezet of gedraaid.
Behandel dit als een startpunt voor iemand die het uitzoekt, niet als een
werkend recept.
ericwout-overheid added a commit that referenced this pull request Aug 5, 2026
#44)

Vier punten uit de tweede review op #46:

- Het aa-complain/dmesg/aa-enforce-recept stond in de kop van het
  AppArmor-profiel. Dat beschrijft wat je doet als het profiel in de weg
  zit, niet wat het afdwingt. Verhuisd naar ADR 0001 §2.5.4 "Als het profiel
  te strak blijkt". In het profiel blijft staan dat je niet mag terugvallen
  op flags=(unconfined) — dat is een eigenschap van het profiel zelf, en
  staat op de plek waar iemand die vlag zou zetten.
- De drie installatievarianten (Linux, podman machine, Rancher) zijn
  subkoppen onder één "Installeren" geworden. Het waren losse ##-secties op
  hetzelfde niveau als "Multi-uid" en "Fallbacks", terwijl het alternatieven
  van dezelfde stap zijn. De wegwijzertabel linkt er nu naartoe.
- Het uitstel van de eigen-kernel-route verwees nergens naar. Beide plekken
  (§4.5 en §5) wijzen nu naar #99, met de kanttekening dat die routes
  ongetest zijn.
- OPEN_HTTPS had in .env.sample geen enkele toelichting en ALLOWED_DOMAINS
  alleen "Alleen nodig als OPEN_HTTPS niet true is". Net de verkeerde om
  kaal te laten: bij de default true is de allowlist een no-op, dus wie dat
  mist denkt beschermd te zijn terwijl al het uitgaand HTTPS openstaat.
  Beide uitgeschreven, inclusief dat de allowlist vóór de privilege-drop
  gelezen wordt en een wijziging dus een recreate vereist.

Geverifieerd tegen init-firewall.sh:84 dat het allowlist-blok inderdaad
alleen draait bij OPEN_HTTPS != true, en tegen regel 71 dat het script
terugvalt op false als de variabele ontbreekt — dat laatste staat er nu bij,
want het betekent dat een ontbrekende regel de sandbox stránger maakt.

Testresultaten van deze PR op een gehardende Tuxedo-host: alle stappen
groen, inclusief multi-uid (twee uid-mappings, PostgresSmokeTest) en het
hardeningsprotocol.
@ericwout-overheid
ericwout-overheid marked this pull request as draft August 5, 2026 09:03
ericwout-overheid added a commit that referenced this pull request Aug 6, 2026
…rdening (#44) (#46)

* docs(maven-podman): ontwerp rootless Podman-in-Docker PoC (#44)

Maven+Testcontainers in de sandbox via nested rootless Podman i.p.v. de
host-agent-bridge. Sluit de container->host code-execution van #44: geen
bridge, geen Docker-socket, geen --privileged. Cross-platform, lichter dan
sysbox/microVM. PoC moet seccomp/fuse/subuid/egress uitwijzen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): implementatieplan PoC (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(sandbox): rootless Podman-in-Docker PoC voor Maven+Testcontainers (#44)

INSTALL_PODMAN build-ARG (default false) installeert podman + fuse-overlayfs +
uidmap + passt/slirp4netns en zet subuid/subgid + rootless storage.conf.
compose.override.podman.yml.example levert /dev/fuse + seccomp-stand. PoC-map
met sample Testcontainers-project, smoke-test.sh en README (run-stappen,
ALLOWED_DOMAINS-egress, fallbacks). Niet live testbaar in deze omgeving (geen
runtime); gebruiker draait smoke-test op de host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): apparmor=unconfined voor rootless podman (#44)

newuidmap faalde met "write to uid_map failed: Operation not permitted".
Root cause: AppArmor docker-default-profiel medieert writes naar
/proc/<pid>/uid_map op Debian/Ubuntu-hosts. Userns (initieel), CAP_SETUID/SETGID
(bounding set), setuid-root newuidmap en NoNewPrivs=0 waren allemaal in orde;
AppArmor was de enige overgebleven blokkade. seccomp stond al op unconfined,
label=disable dekt alleen SELinux. README-fallbacktabel bijgewerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): single-uid + AppArmor-userns-profiel voor gehardende hosts (#44)

PoC wees uit dat naïeve multi-uid rootless podman niet werkt op gehardende
Ubuntu/Tuxedo (apparmor_restrict_unprivileged_userns=1): host blokkeert élke
userns-map, en de privileged newuidmap-range faalt apart. Herziene aanpak:

- Single-uid modus: geen subuid/subgid voor claude -> podman mapt alleen eigen
  uid als root, gebruikt newuidmap niet. ignore_chown_errors=true in storage.conf.
- Custom AppArmor-profiel (flags=(unconfined) { userns, }) + setup-host.sh laadt
  het op de host. Restrictie blijft systeembreed aan; alleen deze container krijgt
  userns. Override verwijst naar het profiel i.p.v. apparmor=unconfined.
- Spec uitgebreid met PoC-bevindingen, per-setup matrix en security-trade-off.
- README herzien: per-setup flow, setup-host.sh, fallbacks.

Nog te verifieren op een gehardende host (kan niet in deze werkomgeving).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): /dev/net/tun device + storage.conf via entrypoint (#44)

PoC-voortgang op gehardende host: AppArmor-userns-profiel lost userns op met
sysctl=1 (self-map OK), single-uid podman info werkt, en met ignore_chown_errors
slaagt image-extractie. Resterend: pasta-netwerk faalde op ontbrekend
/dev/net/tun.

- compose-override: /dev/net/tun device erbij (NET_ADMIN had de sandbox al).
- entrypoint: schrijft rootless storage.conf (single-uid + fuse-overlayfs +
  ignore_chown_errors) idempotent bij start, want een bestaand named volume
  schaduwt de baked-in image-versie.
- README-fallbacks: pasta/tun + storage.conf-shadow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): leeg default_sysctls (ping_group_range RO in container) (#44)

Nested container start faalde op crun "open /proc/sys/net/ipv4/ping_group_range:
Read-only file system": podman zet die sysctl default, maar /proc/sys is RO in de
outer container. Entrypoint schrijft nu ook containers.conf met default_sysctls=[]
zodat crun geen sysctls probeert te zetten. README-fallback bijgewerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): systempaths=unconfined voor nested proc-mount (#44)

Nested container start faalde op crun "mount proc: Operation not permitted":
Docker maskeert /proc-paden, waardoor de kernel (mount_too_revealing) een nieuwe
procfs in de geneste mount-namespace weigert. systempaths=unconfined heft de
masked/RO /proc-paden op de outer container op. Peelt outer-sandbox-hardening
verder af — security-trade-off staat in de override-comment en spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): mkdir socket-dir in smoke-test (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): SmokeIT -> SmokeTest zodat surefire hem draait (#44)

mvn test (surefire) sloeg *IT stil over -> groene build zonder dat de
Testcontainers-test ooit liep. Hernoemd naar *Test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): netavark firewall_driver=iptables (geen nft nodig) (#44)

PoC GESLAAGD (geverifieerd: Tests run: 1, Failures: 0, Errors: 0 — Testcontainers
alpine startte via podman). Testcontainers maakt een bridge-netwerk → netavark
riep default `nft` aan, niet in de image. iptables-driver gebruikt iptables-nft
(al aanwezig). Entrypoint zet dit nu in containers.conf.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(maven-podman): negeer sample target/ (mvn build-output) (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): PoC geslaagd — werkende config + security-balans (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): volledige Testcontainers-build bevestigd (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): TESTCONTAINERS_HOST_OVERRIDE=localhost + echte-build-bevestiging (#44)

Andere sessie draaide een echt Quarkus-project (Redis-stack Dev-Services):
289+46 tests groen via podman in de sandbox. Gap: containers met port-wait
(Postgres/Redis) time-outten omdat Testcontainers de netavark bridge-gateway
(10.88.0.1) als host resolvet terwijl rootless podman op localhost publisht.
Fix: TESTCONTAINERS_HOST_OVERRIDE=localhost in smoke-test + README + spec.
De alpine-GenericContainer miste dit (geen port-wait). Host-agent hiermee niet
meer nodig in de sandbox (blijft fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): setup-host.sh robuuster (modprobe tun/fuse, abi-fallback) (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): seccomp-verfijning geprobeerd — unconfined blijkt nodig (#44)

Geverifieerd: Docker-default seccomp faalt op 'cannot clone: Operation not
permitted' (podman re-exec gebruikt clone(CLONE_NEW*)). seccomp=unconfined is
dus nodig, geen gold-plating. Spec: hardening-verfijning-sectie met het
tailored-seccomp-vervolgpad + waarom apparmor/systempaths nauwelijks te
versmallen zijn. De veiligheid zit in de scoping, niet in het wegpoetsen van de
inherente relaxaties.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): tailored seccomp-blocklist i.p.v. unconfined (#44)

defaultAction=ALLOW (zodat podman's clone/unshare/mount/setns werken —
Docker-default brak hierop) + ERRNO-deny op de gevaarlijke kernel-escape-syscalls
die Docker-default ook blokkeert (module-load, kexec, reboot, iopl/ioperm,
swap, klok-zetten, bpf, perf_event_open, open_by_handle_at, acct, _sysctl,
vm86). Strikt veiliger dan unconfined zonder podman te breken. Override verwijst
naar seccomp/podman-sandbox.json (pad relatief t.o.v. compose-bestand).

Nog te verifieren op de host (recreate + smoke).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): tailored seccomp-profiel geverifieerd (Seccomp:2 + smoke groen) (#44)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): ADR 0001 + host-agent als fallback gedocumenteerd (#44)

#44-DoD: afweging vastgelegd als ADR (docs/adr/0001-...): podman-in-docker als
voorkeur, host-agent als fallback, security-balans, goedkope hardening, C/D
uitgesteld. maven-mcp-agent.md verwijst nu naar het podman-alternatief en
documenteert de host-agent expliciet als fallback + de goedkope hardening
(dedicated least-priv user, projecten buiten gedeelde map, bind 127.0.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): herschrijf naar as-is oplossing i.p.v. PoC; dir poc-podman→podman (#44)

Niet langer een PoC maar een voorstel tot echt gebruik dat de Maven host-agent
beoogt te vervangen (PR-review + collega-tests; bij succes kan de host-agent weg,
mits objectief beter). Teksten ontdaan van PoC-framing in README, spec, ADR,
maven-mcp-agent, override- en script-comments. Map host-agents/maven/poc-podman
→ podman (+ seccomp-pad in override). smoke-test eindmelding "PoC GESLAAGD" →
"OK — Testcontainers werkt". Plan-doc blijft als historisch record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): corrigeer zwakke host-agent-rechtvaardiging (#44)

De host-agent is een DEKKINGS-fallback (hosts waar podman-in-docker nog niet kan
+ Mac/Win te verifieren), geen security-upgrade. De eerdere claim "voor wie de
outer-relaxaties niet wil" klopt niet: die relaxaties verbreden het
kernel-oppervlak van de container (escape vereist nog een exploit), terwijl de
host-agent code direct op de host draait — voor een op container-escape beduchte
gebruiker juist zwakker. ADR + maven-mcp-agent bijgewerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): storage-driver via .env, default vfs i.p.v. fuse (#44)

Issue-comment merkt terecht op: /dev/fuse is een (klein) kernel-risico en hoort
pas toegevoegd bij traagheid, niet als default. Tot nu toe was fuse-overlayfs +
/dev/fuse de default. Nu:

- Default storage = vfs (geverifieerd: single-uid + [storage.options.vfs]
  ignore_chown_errors=true, smoke groen, geen /dev/fuse).
- Configureerbaar via .env: PODMAN_STORAGE_DRIVER=vfs|overlay +
  PODMAN_FUSE_DEVICE=/dev/null|/dev/fuse. Entrypoint genereert storage.conf uit
  de driver; valt terug op vfs + waarschuwing als overlay gevraagd is maar
  /dev/fuse ontbreekt (footgun-guard).
- compose-override: /dev/fuse vervangen door ${PODMAN_FUSE_DEVICE:-/dev/null}
  (no-op default) + PODMAN_STORAGE_DRIVER env-passthrough.
- Dockerfile baked storage.conf → vfs; fuse-overlayfs blijft geïnstalleerd zodat
  opt-in geen rebuild vergt (inert zonder /dev/fuse).
- Spec/ADR/README/.env.sample bijgewerkt: vfs default, fuse opt-in + security-noot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): review-ronde 1 — Dockerfile-comments uit RUN, smoke sourcet sdkman (#44)

- Dockerfile: inline #-comments uit de \-gecontinueerde case-RUN gehaald (werkte
  op buildkit, maar fragiel op classic builder) → naar comment-blok erboven.
- smoke-test.sh: sourcet nu zelf sdkman + faalt met duidelijke melding als mvn
  ontbreekt, zodat het standalone werkt (niet alleen via de README-wrapper).
  Standalone geverifieerd: smoke groen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(maven-podman): review-ronde 2 (security) — seccomp-blocklist uitgebreid (#44)

Security-review: de blocklist miste escape-relevante, cap-gated syscalls die
Docker-default wél blokkeert en die podman/JVM-Testcontainers niet nodig hebben.
Toegevoegd: userfaultfd, io_uring_{setup,enter,register}, NUMA
(mbind/set_mempolicy/migrate_pages/move_pages), process_vm_{readv,writev},
process_madvise, fanotify_init, kcmp, pidfd_getfd, en module-load-rest
(create_module/query_module/get_kernel_syms). ptrace bewust NIET geblokkeerd
(Docker-default laat het toe). Docs: vergelijkingstabel-rij geactualiseerd,
seccomp-omschrijving + ptrace-noot, SELinux label=disable in de security-balans.

Seccomp is create-time → uitgebreide blocklist nog op een recreate te bevestigen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): review-ronde 3 (consistentie) — spec Artefacten-tabel + .example (#44)

Drift gevonden in de spec "Artefacten"-tabel (stond buiten de ontwerp-disclaimer):
beschreef nog subuid baked + fuse-overlayfs storage + devices:[/dev/fuse]. Nu in
lijn met de werkelijke config (single-uid, vfs-default, /dev/net/tun +
${PODMAN_FUSE_DEVICE}). ADR/spec verwijzen nu naar compose.override.podman.yml.example
(met .example) i.p.v. de losse naam. Rename poc-podman→podman volledig schoon,
geen PoC-refs in living docs (alleen in het historische plan-doc), links resolven.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(maven-podman): drop .example van podman-override (#44)

compose.override.podman.yml.example → compose.override.podman.yml. Het bestand
wordt direct met -f meegegeven (geen kopieer-template), dus .example was
misleidend. Wordt niet auto-geladen (Compose auto-merget alleen compose.override.yml),
dus blijft opt-in. Refs in README/spec/ADR/plan/Dockerfile/.env.sample/entrypoint
bijgewerkt; header-noot "kopieer naar compose.override.yml" vervangen door
expliciete -f-instructie + waarschuwing tegen hernoemen. Linux-override blijft
.example (dat is wél een kopieer-template).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(podman): macOS Podman-machine support voor Testcontainers-sandbox

Bevestigd op een Mac met Podman-machine (applehv → Fedora CoreOS):
rootless podman-in-podman draait en de Maven+Testcontainers-smoke-test
slaagt end-to-end.

- Nieuwe compose.override.podman-macos.yml: apparmor=unconfined (Fedora
  CoreOS heeft geen AppArmor/userns-hardening), label=disable voor SELinux,
  absoluut seccomp-pad via ${PWD} (podman leest het profiel client-side).
- Hernoem compose.override.podman.yml -> compose.override.podman-linux.yml
  en werk alle verwijzingen bij (README, setup-host.sh, entrypoint.sh, ADR,
  spec, plan).
- README: macOS-sectie met de drie afwijkingen (podman-compose i.p.v.
  podman compose, BUILDAH_FORMAT=docker, absoluut seccomp-pad), fallback-
  rijen voor de macOS-fouten, en per-setup matrix bijgewerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(maven-podman): drop PODMAN_FUSE_DEVICE, /dev/fuse als uncomment-opt-in (#44)

PODMAN_FUSE_DEVICE was redundant naast PODMAN_STORAGE_DRIVER. Compose kan een
devices-entry niet uit de waarde van een var afleiden (geen conditionals in
interpolatie), dus i.p.v. een tweede env-var is /dev/fuse nu een
uitgecommentarieerde regel in de overrides die je uncomment voor overlay.
PODMAN_STORAGE_DRIVER blijft de enige .env-knop; entrypoint waarschuwt + valt
terug op vfs als overlay gekozen is zonder het device (geen broken start).
Linux- + macOS-override, entrypoint, .env.sample, README en spec bijgewerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): Kata-handleiding voor maximale isolatie op Linux (#44)

Nieuwe gids docs/kata-linux-maximale-isolatie.md: Kata Containers (eigen
guest-kernel per container) als de kernel-escape-laag op Linux-native — de plek
waar die grens by default ontbreekt. Inclusief: voorwaarden (KVM/nested virt),
eerlijke noot over Docker-Engine↔Kata-integratiefrictie (shim-v2 vs containerd/
nerdctl), interactie met de podman-hardening (host-AppArmor-profiel waarschijnlijk
overbodig onder Kata), en caveats. Mac/Windows: kernel-grens al aanwezig (VM via
Docker Desktop/Rancher/podman machine) → Kata niet nodig, met WSL2/projects-mount
kanttekeningen. Niet getest in deze omgeving; gemarkeerd als te verifiëren.
Gelinkt vanuit ADR (Optie D) + podman-README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(maven-podman): maximale-isolatie-gids omgebogen naar podman-in-een-VM (#44)

Hernoemd kata-linux-maximale-isolatie.md → maximale-isolatie-linux.md. Aanbevolen
route is nu de sandbox in een lichtgewicht VM draaien (podman in Lima/Multipass):
echte kernel-grens, podman+Testcontainers ongewijzigd, snapshot/wegwerp =
makkelijke recovery, en de in-VM-hardening mag simpeler (VM is de grens). Kata +
gVisor als alternatieven met waarom-niet. Mac/Windows: kernel-grens al aanwezig.
Plus de shared-mount-spanning (smal+RO+geen-autorun vs git-als-grens). ADR + podman
README-links bijgewerkt. Niet getest hier; gemarkeerd als te verifiëren.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(podman): entrypoint-guard + docs voor vergeten compose-override

Nested/detached podman faalde omdat de sandbox met alleen compose.yml
startte, zonder compose.override.podman-*.yml: geen /dev/net/tun (pasta
faalt) en geen systempaths=unconfined (proc-mount geweigerd). De
INSTALL_PODMAN build-arg en de runtime-override waren ontkoppeld en
niets verbond ze, dus faalde het pas veel later en stil.

- entrypoint.sh: waarschuw bij opstart als podman aanwezig is maar
  /dev/net/tun ontbreekt (betrouwbare tell dat de override ontbreekt)
- README.md + opstarten-en-afsluiten.md: kruisverwijzing bij
  INSTALL_PODMAN=true naar de override + per-OS-matrix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(maven-podman): multi-uid rootless podman als opt-in (#44)

Single-uid kan geen images draaien die naar een tweede uid chownen:
postgres, mysql en mariadb falen met `chown: ...: Invalid argument`,
waarna Testcontainers alleen "Container did not start correctly" meldt.

Multi-uid lost dat op, maar vereist CAP_SYS_ADMIN in de outer container.
De kernel laat een uid_map-write toe aan wie die capability heeft in de
doel-namespace of er de eigenaar van is (ns->owner == euid). newuidmap is
setuid-root, dus euid 0 terwijl de namespace van uid 1000 is: de
eigenaars-route valt weg en de capability is vereist. Docker laat
CAP_SYS_ADMIN per default uit de bounding set, dus setuid-root kan hem
niet krijgen.

Daarom opt-in en niet default: `claude` krijgt de capability niet
rechtstreeks in handen (CapEff=0), maar de impact van een root-escalatie
in de container groeit wel. Op Mac/Windows zit er nog een VM-kernelgrens
onder, op Linux met bare Docker niet.

- PODMAN_MULTIUID build-arg zet de subuid-range in de image
- compose.override.podman-multiuid.yml geeft de capability, te stapelen
  op de platform-override
- entrypoint waarschuwt bij multi-uid zonder CAP_SYS_ADMIN, zodat je het
  bij de start ziet i.p.v. halverwege een build
- ignore_chown_errors alleen nog in single-uid; in multi-uid zou het een
  echte chown-fout maskeren

Meegenomen omdat de regel toch herschreven werd: de `|| true` stond aan
het eind van de &&-keten en maakte daarmee ook een falende
`apt-get install podman` groen. Nu gescopet tot alleen de sed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(maven-podman): PostgresSmokeTest voor de multi-uid modus (#44)

De bestaande alpine-smoke chownt niet en wacht niet op een poort, en
dekt dus noch de single-uid-beperking noch het
TESTCONTAINERS_HOST_OVERRIDE-probleem af. PostgresSmokeTest doet beide:
PostgreSQLContainer plus een echte JDBC-query over de published port.

smoke-test.sh leest /etc/subuid en geeft -Dpodman.multiuid door, zodat
de test in single-uid netjes wordt overgeslagen in plaats van te falen
op iets dat daar niet kan werken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(maven-podman): newuidmap-EPERM herleid, Rancher/macOS bevestigd (#44)

Blokkade #2 in de spec stond genoteerd als "oorzaak niet sluitend
herleid". Dat is nu de ontbrekende CAP_SYS_ADMIN, met de meettabel
erbij. Let op de probe die misleidend slaagt: newuidmap als root op
root's eigen userns gaat via de eigenaars-route, niet via de
capability. Dat verklaart waarschijnlijk waarom capabilities bij de
oorspronkelijke eliminatie als uitgesloten genoteerd stond.

Rancher Desktop op macOS is bevestigd: Alpine-VM zonder AppArmor of
SELinux, geen userns-hardening, /dev/net/tun en /dev/fuse aanwezig, dus
geen enkele host-instelling nodig. Rancher's eigen docker compose stuurt
het seccomp-profiel inline mee en dockerd accepteert dat, waar podman's
API het weigert met "file name too long".

Verder de single-uid-beperking expliciet gemaakt in de per-setup matrix
en twee rijen aan de fallback-tabel toegevoegd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(maven-podman): multi-uid bevestigd op macOS podman machine (#44)

Postgres via Testcontainers draait nu ook groen op een applehv
podman-machine: PODMAN_MULTIUID=true plus de multiuid-override gestapeld
op de macOS-override, via podman-compose. De machine is rootful, dus
cap_add SYS_ADMIN landt echt in de bounding set — op een rootless
machine is dat niet getest, dat staat er expliciet bij.

Tweede vondst onderweg: een met de hand aangepaste containers.conf in
het claude-home volume brak podman met een pause.pid-fout. De entrypoint
schrijft dat bestand alleen als het ontbreekt, dus zulke drift blijft
stil staan en schaduwt de bedoelde config. De spec claimde "idempotent
geschreven", wat het niet is; dat is gecorrigeerd, met een fallback-rij
in de README en de vraag of storage.conf en containers.conf gelijk
getrokken moeten worden als openstaand punt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(maven-podman): podman-socket bij container-start i.p.v. per build (#44)

Testcontainers viel om met "chown: Invalid argument" op postgres terwijl
subuid-range en CAP_SYS_ADMIN gewoon aanwezig waren. Oorzaak: het eerste
podman-commando maakt het pause-proces aan dat de user-namespace
vastlegt, en alles daarna joint dat proces. Kwam dat single-uid op, dan
bleef de hele container single-uid — ook nadat de oorzaak weg was.

Eén bewezen trigger is no_new_privs: newuidmap is setuid-root en wint
onder die vlag geen privileges, dus de range-write faalt met dezelfde
EPERM als bij een ontbrekende capability. `setpriv --no-new-privs podman
info` reproduceert het, en met een gezond pause-proces geeft diezelfde
aanroep wél de volledige mapping — joinen vergt de capability niet.

Daarom zet de entrypoint de socket nu bij de start op, in een schone
omgeving voor er iets anders draait, en waarschuwt hij als de mapping dan
tóch single-uid is. De client-env staat op container-niveau in de
podman-overrides en niet in een shell-profiel: /etc/profile.d geldt
alleen voor login-shells en ~/.bashrc alleen voor interactieve, dus een
build die via `bash -c` start kreeg uit geen van beide iets mee.

De smoke-test start geen eigen service meer als er al een draait. Dat was
geen cosmetica: een tweede service op hetzelfde pad kan niet binden en
verwijdert bij het afsluiten de socket van de eerste, waarna
Testcontainers terugvalt op /var/run/docker.sock. Hij faalt nu ook hard
als multi-uid aanstaat maar podman één uid mapt.

Geverifieerd op een verse container (macOS podman machine): smoke-test
groen incl. PostgresSmokeTest, en 359 tests groen in een Quarkus-module
met Dev Services + Flyway tegen postgres:18, zonder handmatige env.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): ontwerp voor bridge-verwijdering en hardening (#44)

Twee gestapelde PR's. PR A verwijdert de Maven host-agent en geeft de
podman-in-sandbox-set een eigen plek onder claude-sandbox/podman/. PR B
sluit de container-escape uit de security-review op #76: root-entrypoint
met privilege-drop in plaats van de NOPASSWD SETENV-sudoers-regel, een
AppArmor-profiel dat daadwerkelijk mediateert, en een ruimere
seccomp-blocklist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): implementatieplan voor bridge-verwijdering en hardening (#44)

Zestien taken over twee gestapelde PR's, met per taak een controle die eerst
faalt. Legt onder meer vast dat de omschakeling naar USER root de
gedocumenteerde exec-commando's raakt, en waarom --no-new-privs bewust
ontbreekt bij de privilege-drop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(sandbox): podman-set naar claude-sandbox/podman/ (#44)

De podman-in-sandbox-set stond in de boom van de Maven host-agent, waardoor
wijzigingen aan podman lazen als wijzigingen aan die agent. Verplaatst naar
een eigen top-level map, met de seccomp-paden in beide overrides mee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sandbox): Maven host-agent verwijderd (#44)

De host-agent draaide mvn op de host als de host-user, met pom.xml en mvnw
uit de projects-map die de sandbox kan schrijven — een container→host
code-execution-bridge. Podman-in-de-sandbox vervangt hem, dus hij kan weg.

Verwijdert ook requirements.txt (509 regels). Dependabot heeft geen
pip-entry in de config, maar security-updates scannen manifests repo-breed;
die alert-stroom vervalt hiermee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): podman-gids als enige ingang, verwijzingen rechtgetrokken (#44)

Alle paden naar de oude host-agents-boom bijgewerkt. De podman-README is nu
de enige plek met bedieningsstappen; de rest verwijst ernaar. Voegt een
expliciete lijst ondersteunde platforms toe, inclusief wat niet bevestigd
is nu er geen terugvaloptie meer is. De per-setup-matrix en de sectie
Openstaand herhaalden die status en verwijzen er nu naar.

De twee 2026-06-10-documenten zijn historisch: daarin zijn alleen paden
bijgewerkt, geen inhoud.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(adr): 0001 naar Geaccepteerd, host-agent-spoor eruit (#44)

Het ADR was voorwaardelijk geformuleerd: de host-agent zou vervallen zodra
de podman-opzet breed bevestigd was. Dat is gebeurd, dus dit is de afronding
die het document zelf aankondigde.

De sectie "Goedkope hardening host-agent" is weg — die ging volledig over een
component die niet meer bestaat. De security-balans blijft in deze PR staan
zoals hij was; die wordt in de hardening-PR bijgewerkt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): reviewfixes op comments en verwijzingen (#44)

Uit de reviewronde op deze PR:
- Dockerfile-comment: "i.p.v. via de host" (route bestaat niet meer) weg, en
  het niet-bestaande compose.override.podman.yml vervangen door de echte
  -linux/-macos-bestanden.
- .gitignore: dode host-agents/**-regels weg.
- De seccomp-paduitleg die ik eerder toevoegde was fout: Compose resolvet een
  relatief pad tegen de projectdirectory (map van het eerste -f-bestand), niet
  tegen de werkdirectory. Rechtgezet in beide overrides én in de README-tabel,
  die het tegenovergestelde beweerde.
- macos-override: het voorbeeldcommando gebruikte `podman compose` (delegeert
  naar docker-compose, seccomp-inline faalt) met een niet-bestaande
  bestandsnaam; nu podman-compose met het juiste pad. ${PWD:?} faalt hard als
  PWD niet gezet is, i.p.v. een pad vanaf filesystem-root te vormen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sandbox): host.docker.internal-route verwijderd (#44)

Deze route — de compose.override.linux.yml.example, het extra_hosts-comment
in compose.yml en de OUTPUT-ACCEPT-regel in init-firewall.sh — bestond
uitsluitend om de container de Maven host-agent op poort 7777 te laten
bereiken. Die agent is verwijderd, dus de route is een uitzondering in een
default-DROP allowlist zonder consument. De firewall-comment noodde bovendien
nog uit tot "MCP-servers op de host".

De HOST_NETWORK-ACCEPT (regel eronder) blijft; die dekt Docker DNS-forwarding
en IDE-koppelingen binnen het bridge-netwerk. Alleen het VM-interne
host.docker.internal-IP buiten dat netwerk (Docker Desktop/Rancher) valt weg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(adr): capability-claim gecorrigeerd, verworpen alternatief bewaard (#44)

Reviewfixes op ADR 0001 nu het op Geaccepteerd staat:
- "Géén CAP_SYS_ADMIN" klopte niet meer: de multi-uid opt-in uit #76 zet hem
  in de bounding set. Gecorrigeerd, met de nuance dat claude CapEff=0 heeft.
  De volledige herschrijving van de security-balans volgt in de hardening-PR.
- Sectie "Overwogen en verworpen" toegevoegd: de host-agent en de
  Docker-socket-runner, met de reden waarom de host-agent ook geen veiliger
  alternatief is. Die redenering was bij het schrappen van spoor 2 verdwenen.
- "Optie C/D" verwees naar een A/B die uit het document geknipt was; hernoemd
  naar "sysbox/microVM".
- De consequentie voor Windows/WSL2-werkplekken expliciet benoemd en de
  "geen gebruikers"-onderbouwing gekwalificeerd als teamspecifiek.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): waarschuwing voor host-side builds + startcommando's ontdubbeld (#44)

- README:35-37 raadde aan de projects-map "buiten docker te bouwen, testen of
  opstarten" — dat is de #44-aanval zonder agent: een rogue pom.xml/mvnw/
  Makefile/git-hook draait dan met host-rechten. Met het verwijderen van de
  host-agent verdween ook de tegenmaatregel (die stond in maven-mcp-agent.md
  en de ADR-hardeningsectie), terwijl het risico aan de gedeelde mount hangt.
  Waarschuwing teruggezet op de plek waar de mount beschreven wordt.
- Het macOS-startcommando stond woordelijk op drie plekken en dreef al uiteen
  (mist BUILDAH_FORMAT/--build). De kopieën in README en opstarten-en-afsluiten
  wijzen nu naar podman/README.md i.p.v. een eigen, driftend commando.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(podman): migratieblok, platformtabellen samengevoegd, cwd + sudo expliciet (#44)

- "Migratie vanaf de host-agent" toegevoegd: git rm stopt geen draaiende
  run.sh (auth-loze listener op 0.0.0.0:7777) of MCP-registratie die in het
  claude-home volume een rebuild overleeft. Drie opruimstappen.
- De status-tabel en de per-setup-matrix somden dezelfde platforms met
  verschillende labels op; samengevoegd tot één tabel Platform | Status | Wat
  je nodig hebt, met de niet-ondersteunde platforms als expliciete rij.
- Eén regel bovenaan "Stappen" dat alles vanuit claude-sandbox/ draait (stap 2
  ging al uit van een cd die pas in stap 3 stond).
- Stap 2 vermeldt nu dat setup-host.sh sudo op de host vereist — op een
  beheerde werkplek zonder admin loop je daar anders vast.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: historische banner op de 06-10-documenten (#44)

Die twee documenten worden vanuit de compose-overrides, de Dockerfile en de
podman-README aangehaald als ontwerpbron, maar hun statusregel zei nog "in
review" en één regel beschreef de host-agent nog als beschikbare fallback —
in tegenspraak met de README ("geen terugvaloptie meer"). Een banner bovenaan
markeert ze als historisch en de fallback-bijzin is geschrapt. De inhoud
blijft verder bevroren; padverwijzingen als maven_agent.py beschrijven bewust
de toenmalige situatie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* perf(sandbox): .dockerignore om de build-context klein te houden (#44)

De build-context is claude-sandbox/, maar de Dockerfile COPY't alleen vendor/,
skills/, init-firewall.sh en entrypoint.sh. Zonder .dockerignore werd bij elke
--build de hele boom ingepakt en naar de daemon gestuurd, inclusief projects/
(de default mountlocatie met gebruikerscheckouts) en podman/sample/target/.
Sluit die uit; de image blijft identiek.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sandbox): root-entrypoint met privilege-drop i.p.v. sudoers SETENV (#44)

De firewall liep via een NOPASSWD-sudoers-regel met de SETENV-tag. Die tag
laat BASH_ENV de env_reset van sudo overleven, waarmee uid 1000 in één
commando container-root werd — en zonder userns-remap is dat host-root-uid.
Dezelfde regel maakte de egress-allowlist self-service: claude kon
init-firewall.sh opnieuw draaien met OPEN_HTTPS=true.

De container start nu als root, zet de firewall op en dropt met setpriv naar
claude. sudo en de sudoers-regel zijn weg. Beide entrypoints zijn
root-eigendom, want een door claude schrijfbaar bestand dat als root start
zou het gat gewoon verplaatsen.

Bewust geen --no-new-privs: setuid-root newuidmap loopt daarop stuk
(gemeten in #76), waarna multi-uid podman degradeert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): exec-commando's met -u claude (#44)

De container start sinds de vorige commit als root (om de firewall op te
zetten) en dropt daarna naar claude. Daardoor geeft `docker compose exec
claude …` en `podman exec claude-sandbox …` zonder -u een root-shell. Alle
gedocumenteerde exec-commando's gaan naar `-u claude`, met een noot dat in
`compose exec -u claude claude` de eerste claude de user en de tweede de
service is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(podman): AppArmor-profiel afgeleid van docker-default (#44)

Het profiel was flags=(unconfined) met één userns-regel: de MAC-laag deed
niets. Samen met systempaths=unconfined en het ontbreken van userns-remap
maakte dat /proc/sys/kernel/core_pattern schrijfbaar voor container-root, en
dat is host-root code-execution via de core-dump usermode-helper.

Nu afgeleid van docker-default met behoud van de /proc/sys- en /sys-denies,
plus userns en mount/umount/pivot_root omdat geneste podman daar niet zonder
kan. Sluit de core_pattern-write ook als systempaths=unconfined blijft staan.

Het profiel is niet in deze omgeving te parsen (apparmor_parser is host-side);
verificatie loopt via het testprotocol en het aa-complain/dmesg-recept in de
profiel-header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(podman): setup-host.sh weigert profielbestanden met vreemde stanzas (#44)

apparmor_parser -r vervangt profielen op de naam die in het bestand staat.
Een toegevoegde `profile docker-default flags=(unconfined) { }` zou dus de
afscherming van alle containers op de host slopen — en dat valt in een
configbestand veel minder op dan in een script. Nu wordt geweigerd wat niet
precies één claude-sandbox-podman-profiel is.

Voegt ook het aa-complain/dmesg-recept toe aan de slotmelding, voor als het
strakkere profiel podman breekt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(podman): keyring- en legacy-syscalls op de seccomp-blocklist (#44)

keyctl, add_key en request_key openen de kernel-keyring, een terugkerend
LPE-oppervlak dat via userns bereikbaar is. quotactl, syslog, uselib, ustat,
sysfs en de pciconfig-familie zijn legacy-oppervlak. Rootless podman, crun
en pasta gebruiken geen van alle.

De blocklist-vorm blijft: een allowlist zet clone/unshare/mount/setns achter
CAP_SYS_ADMIN en breekt daarmee rootless podman. 42 → 53 geblokkeerde
syscalls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): testprotocol voor de hardening + openstaand onderzoek (#44)

"De escape is dicht" is een bewering die bewijs nodig heeft, en die kan
alleen op een draaiende container geleverd worden. Het protocol test zowel
dat de escape sluit (sudo weg, core_pattern-write geweigerd, CapEff=0) als
dat de sandbox blijft werken (smoke-test groen), met de negatieve tests
expliciet erbij.

Legt ook de drie openstaande onderzoeksvragen vast (systempaths versmallen,
userns-remap, bounding set) in de meettabel van de spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): security-balans herschreven na de hardening (#44)

De multiuid-override stelde dat CAP_SYS_ADMIN "de impact van een
root-escalatie ín de container" vergroot. Zonder userns-remap is
container-root gelijk aan host-root-uid, dus die impact is host-breed. En de
noot noemde nog een sudoers-regel die nu weg is.

ADR 0001 heeft nu een security-balans in drie delen: wat dicht is (bridge,
de sudo-route naar container-root, de self-service egress-allowlist, de
/proc/sys-write), wat open blijft (outer-relaxaties, CAP_SYS_ADMIN in de
multi-uid opt-in, kernel-escapes), en welke laag welke escape sluit
(root-entrypoint voor het bereiken van root, AppArmor voor wat root dan kan).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): setuid-strip + AppArmor-enforce-borging sluiten de resterende root-routes (#44)

Uit de security-review op deze PR — twee gaten die de defense-in-depth in de
multi-uid opt-in ondermijnden:

1. setuid-route. De privilege-drop laat bewust --no-new-privs weg (nodig voor
   newuidmap) en houdt de bounding set. Daardoor kon claude na de drop euid 0 +
   bounding-set-caps winnen via elke setuid-root-binary (su, mount, passwd, ...);
   de kernel vult bij een setuid-exec de permitted caps uit de bounding set, in
   multi-uid inclusief CAP_SYS_ADMIN. De Dockerfile stript nu alle setuid/setgid-
   bits behalve newuidmap/newgidmap/fusermount3. Dat sluit ook de setuid mount/
   umount, en daarmee de proc-remount-bypass op binary-niveau.

2. AppArmor-complain-preconditie. In de multi-uid-stand is het AppArmor-profiel
   de laatste laag tegen de core_pattern-escape. Ons eigen debug-recept raadt
   aa-complain aan; blijft het profiel in complain staan, dan dwingen de denies
   niet af. entrypoint-root.sh faalt nu hard als ons profiel in complain-modus
   draait (macOS/unconfined blijft ongemoeid — daar zit een VM-grens onder).

Samen maken deze de root-entrypoint-laag en de AppArmor-laag weer echt
onafhankelijk, zoals de ADR-balans claimt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): security-balans eerlijk over de mount-bypass en de setuid-route (#44)

De doc-claim "AppArmor weigert /proc/sys-writes ongeacht de mount-opties" was
onjuist: de deny is padgebonden aan /proc, dus een verse proc-mount met
CAP_SYS_ADMIN omzeilt hem. Gecorrigeerd in de multiuid-override en de ADR:

- De setuid-strip is wat de weg naar CAP_SYS_ADMIN sluit; dat is de primaire
  mitigatie, met AppArmor als tweede laag voor de escapeklasse zelf.
- De ADR benoemt nu expliciet dat AppArmor de /proc/sys-usermode-helper-klasse
  sluit, niet elke container-root-capability (mount/pivot_root/caps staan toe).
- --no-new-privs-rationale toegevoegd aan de ADR.
- Het blokkeren van de mount-bypass in het profiel zelf (child-profiel voor de
  nested runtime) staat als vierde onderzoeksvraag in de spec-meettabel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): testprotocol uitgebreid + doc-drift opgeruimd (#44)

Reviewfixes:
- Testprotocol test nu ook de lagen die het meest onder de loep lagen:
  /proc/self/attr/current moet enforce zijn (nooit complain), setuid-enumeratie
  moet alleen newuidmap/newgidmap/fusermount3 tonen, en CapBnd bevestigt de
  multi-uid bounding-set-trade-off. De smoke-test-commando's kregen een cd zodat
  ze letterlijk uitvoerbaar zijn.
- Het aa-complain/dmesg/aa-enforce-recept stond 3x; het testprotocol is nu de
  canonieke plek, de profiel-header verwijst ernaar.
- Niet-resolveerbare "PR B"/"PR #76"-verwijzingen in code en operationele docs
  vervangen door "deze hardening" en de verwijzing naar de spec-meettabel.
- Twee exec-commando's zonder -u in het historische 06-10-plan kregen -u claude.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): complain-check debug-escape + ADR-nauwkeurigheid (#44)

Uit de tweede security-reviewronde — twee punten die de fixes zelf
introduceerden, geen security-gat maar wel echt:

- De complain-check faalde hard vóór de drop, waardoor het aa-complain-
  debugrecept uit het testprotocol niet meer in een draaiende container te
  reproduceren was. Nu degradeert ALLOW_APPARMOR_COMPLAIN=true de fatal tot een
  waarschuwing. Fail-closed by default; alleen de operator kan die env zetten
  vóór de drop, claude bereikt die fase niet.
- De ADR overclaimde de onafhankelijkheid van de AppArmor-laag ("en omgekeerd"):
  AppArmor alléén stopt geen CAP_SYS_ADMIN-houder (de proc-mount-bypass), dus
  het is defense-in-depth voor het directe /proc/sys-pad, niet een zelfstandige
  vervanger van de setuid-strip. Ook de enforce-borging preciezer geformuleerd
  (dekt complain, niet "profiel niet toegepast").

Plus een getcap-regel in het testprotocol: de setuid-strip raakt geen
file-capabilities, dus die apart controleren.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sandbox): registries automatisch in de whitelist bij podman (#44)

De docker.io-registries die Testcontainers nodig heeft, moesten in de
strikte-whitelist-modus (OPEN_HTTPS=false) met de hand aan ALLOWED_DOMAINS
worden toegevoegd. init-firewall.sh voegt ze nu zelf toe als rootless podman
in de image zit — consistent met hoe de Maven/SDKman-domeinen al onvoorwaardelijk
in de lijst staan. Zonder podman verbreedt het de whitelist niet, en bij
OPEN_HTTPS=true (default) is de hele lijst een no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): root-shell-waarschuwing alleen tonen als je echt root bent (#44)

De waarschuwing stond systeembreed in /etc/bash.bashrc en /etc/zsh/zshrc achter
`if [ -t 1 ]` — dat checkt alleen op een tty, niet op de uid. Daardoor kreeg
ook `claude` de "je bent root"-melding bij elke interactieve shell. De per-user
/root-versie vuurde impliciet alleen voor root; die beperking ging verloren bij
de omschakeling naar systeembreed. Guard aangevuld met [ "$(id -u)" -eq 0 ].

Blootgelegd door de host-verificatie (docker exec -tiu claude gaf de melding).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(podman): keyring-syscalls weer toestaan — crun heeft ze nodig (#44)

De seccomp-uitbreiding blokkeerde keyctl/add_key/request_key op de aanname dat
crun ze niet gebruikt. Fout: crun maakt bij elke container-start een
session-keyring via keyctl(KEYCTL_JOIN_SESSION_KEYRING), waardoor geneste
podman faalde met "crun: create keyring: Operation not permitted". Docker's
default-profiel staat deze syscalls juist toe, precies hierom.

Blootgelegd door de host-smoke-test (stap 2, nested container). De overige
B5-toevoegingen (quotactl/syslog/uselib/ustat/sysfs/pciconfig_*) raken crun
niet en blijven. ADR- en override-tekst gecorrigeerd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(podman): /proc/sys/net toestaan in AppArmor — netavark heeft het nodig (#44)

De van docker-default afgeleide deny `@{PROC}/sys/[^k]** w` blokkeerde ook
/proc/sys/net. netavark (podman's netwerk-backend) zet bij het starten van een
geneste container net-sysctls (ipv6 autoconf) en faalde met "netavark: failed
to set autoconf sysctl: Permission denied". Docker doet netwerk-setup buiten
AppArmor; geneste podman draait netavark ín de container onder dit profiel.

`[^k]` -> `[^kn]` zondert /proc/sys/net uit naast /proc/sys/kernel. net-sysctls
zijn per-netns namespaced (raakt alleen de eigen netns, niet de host); de
core_pattern-escape zit in /proc/sys/kernel en blijft gedekt.

Blootgelegd door de host-smoke-test (stap 4, SmokeTest).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(podman): pasta als default-netwerkmodus zodat Testcontainers werkt (#44)

netavark faalde in nested rootless podman met "failed to set autoconf sysctl:
Permission denied": voor een bridge zet het een IPv6-sysctl op een interface
in de outer container-netns, die door init_user_ns geowned wordt (geen
userns-remap), dus rootless podman mag er /proc/sys/net niet schrijven. Dit
blokkeerde alle Testcontainers-runs op een gehardende host.

De entrypoint zet nu netns="pasta" in containers.conf. Pasta geeft elke
container een eigen netwerk met port-forwarding naar localhost en vermijdt de
bridge. Op een echte host bevestigd: smoke-test groen incl. PostgresSmokeTest
(multi-uid). Geen testcode aangepast — de netns-default geldt ook voor
containers die via de podman-Docker-API starten.

Beperking: container-naar-container over netwerknamen (Testcontainers Network)
werkt niet zonder userns-remap; dat blijft de userns-remap-spike. Bestaande
volumes krijgen de netns-regel via een idempotente migratie in de entrypoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(podman): reviewfixes pasta — robuustere migratie, doc-drift, #82 (#44)

Uit de reviewronde op deze PR:
- containers.conf-migratie robuuster: de grep matcht nu ook een ingesprongen
  netns (geen duplicaat-key die podman's TOML-parser breekt), en bij een config
  zonder [containers]-sectie wordt die sectie geappend i.p.v. een sed die stil
  niets doet. Post-check waarschuwt als het toevoegen tóch faalt.
- README-drift rechtgetrokken: twee plekken zeiden nog dat de entrypoint
  containers.conf "alleen aanmaakt als hij ontbreekt"; hij plaatst nu ook de
  netns-regel bij (overige drift blijft staan).
- Openstaand-bullet toegevoegd voor de container-naar-container-beperking, en de
  vage "spike/#44"-verwijzing vervangen door het concrete ticket #82.
- Egress-winst gedocumenteerd: nested pasta-egress loopt via de OUTPUT-chain en
  valt onder de allowlist, waar de bridge via FORWARD die allowlist kon omzeilen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): reviewfixes registry-whitelist — interne registry + wording (#44)

Uit de reviewronde op deze PR:
- Happy-path-noot toegevoegd (README stap 1 + .env.sample): gebruik je een
  eigen/interne registry (Harbor, Nexus, mirror), zet die dan wél in
  ALLOWED_DOMAINS. Voorkomt dat een hergebruiker denkt dat álle registries
  automatisch gaan.
- .env.sample-wording gelijkgetrokken met de code: de firewall keyt op
  "podman in de image" (command -v podman), niet op de INSTALL_PODMAN-flag zelf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(plannen): plan- en designdocumenten naar een eigen PR

De vier documenten onder `docs/superpowers/` zijn 2738 van de 4477
toegevoegde regels en bevatten geen code. Ze gaan naar een losse PR zodat
deze PR beperkt blijft tot wat er daadwerkelijk aan de container verandert.

Inhoudelijk ongewijzigd — alleen verplaatst naar branch
`docs/podman-stack-plannen`.

* chore(ci): DS-0002 onderdrukken met motivering — root-fase is bewust (#44)

De Dockerfile eindigt op `USER root` omdat init-firewall.sh NET_ADMIN nodig
heeft; entrypoint-root.sh dropt daarna onherroepelijk naar `claude`. Trivy
DS-0002 kijkt naar de gedeclareerde image-gebruiker en ziet die drop niet.

De check is hier een false positive: de agent bereikt de root-fase nooit
(geen sudo, setuid-bits gestript, geen weg terug na setpriv), en de twee
paden die er nog wel naartoe leiden — `docker exec` zonder -u en een eigen
--entrypoint — vereisen Docker-CLI-toegang op de host. Wie die heeft is al
host-root en zit buiten de grens die de sandbox bewaakt.

Het alternatief zonder root in de hoofdcontainer is onderzocht en levert een
fail-open op bij herstart; uitgewerkt in #98.

Geverifieerd: geen docker-socket gemount in compose.yml of de drie
overrides; geen sudoers-regel in de image. Niet geverifieerd: of Trivy na
deze wijziging groen wordt — dat blijkt uit de CI-run op deze push.

* feat(podman)!: fuse-overlayfs-storage laten vallen, alleen vfs (#44)

Review-punt: als /dev/fuse een security-risico is, ondersteun het dan niet
half. De overlay-driver was opt-in, uitgecommentarieerd in beide overrides
en gaf alleen snelheidswinst — terwijl de filesystem-snelheid met vfs tot
nu toe voldoet.

Weg: het `fuse-overlayfs`-pakket, de `/dev/fuse`-device-regels, de
`PODMAN_STORAGE_DRIVER`-env met zijn terugvalpad, en het laden van de
fuse-kernelmodule in setup-host.sh. storage.conf is nu onvoorwaardelijk vfs.

Bijvangst voor de hardening: `fusermount3` hoeft niet meer uitgezonderd te
worden van de setuid-strip in de Dockerfile. Die strip is daarmee strikter
— alleen newuidmap/newgidmap blijven setuid. hardening-verificatie.md
verwacht dat nu ook zo.

BREAKING CHANGE: PODMAN_STORAGE_DRIVER bestaat niet meer. Stond die op
`overlay`, verwijder hem uit .env; de container draait voortaan op vfs.
Eenmalig `podman system reset` in de container ruimt de oude overlay-store op.

Geverifieerd: `bash -n` op alle drie de scripts, YAML-parse op compose.yml en
de drie overrides, en een grep die aantoont dat er buiten de spec geen
fuse/overlay-verwijzing meer staat. Niet geverifieerd: geen image gebouwd en
geen smoke-test gedraaid — dat moet op een echte host.

* docs(adr): ADR 0001 als enige plek voor de isolatie-onderbouwing (#44)

Review-punt: dezelfde uitleg stond op meerdere plekken (no_new_privs op vijf,
de core_pattern-escape op drie), en 15 comments verwezen naar het
spec-document — een historisch verslag, geen naslagwerk.

ADR 0001 is herschreven met genummerde secties die de opstartvolgorde
volgen: bouwfase (2.2), rootfase (2.3), gebruikersfase (2.4), relaxaties op
de buitenste container (2.5). Code verwijst met nummer én titel, zodat de
verwijzing leesbaar blijft en een hernummering opvalt:

    # Verwijder deze regel dus niet "voor de veiligheid" — zie ADR 0001
    # §2.3.3 "Privilege-drop zonder --no-new-privs".

Bij de code blijft staan wat je moet weten om díé regel te begrijpen; het
volledige dreigingsverhaal staat één keer in de ADR. Bewust niet andersom:
bij regels als de ontbrekende --no-new-privs en de setuid-strip is het
risico dat iemand ze later "opruimt", dus daar staat nu expliciet waarom ze
er zijn.

Commentaarregels: entrypoint-root.sh 44->27, multiuid-override 38->20,
entrypoint.sh 88->62, Dockerfile 104->80.

Verder meegenomen uit de review: all-caps kopjes zijn normale zinnen
geworden; "peelt" -> "pelt"; de multiuid-override beschrijft de
eindtoestand i.p.v. "sinds de hardening"; de netavark-uitleg bij
TESTCONTAINERS_HOST_OVERRIDE klopte niet meer sinds de pasta-default; het
AppArmor-profiel noemt de echte compose-aanroep i.p.v. "docker ..."; de
verwijzing naar de Copilot-bug is vervangen door het tijdloze punt
(socket-toegang is host-root).

Geverifieerd: `bash -n` op beide entrypoints, YAML-parse op compose.yml en
de drie overrides, en een grep die aantoont dat er geen spec-verwijzing meer
in levende code staat en geen all-caps kopje meer over is. Niet
geverifieerd: geen image gebouwd.

* docs(podman): README opschonen na review, isolatiedoc naar eigen PR (#44)

Review-punten op claude-sandbox/podman/README.md:

- Migratiesectie vanaf de host-agent verwijderd. Die was geschreven voor
  bestaande gebruikers, maar dat zijn er twee en die zijn al gemigreerd.
  Nieuwe lezers zouden een procedure aantreffen voor iets wat er nooit was.
- Voor de installatiestappen staat nu een wegwijzer met de drie varianten
  (Linux, macOS podman machine, Rancher Desktop). De sectie heet "Stappen op
  Linux" i.p.v. "Stappen op de host" — hij was al Linux-only, alleen stond
  dat er niet. Stap 2 is expliciet Linux-only i.p.v. "onschadelijk elders".
- "de fallback-tabel" bestond niet als term; het is nu een link naar
  "Fallbacks als het niet meteen draait" met de concrete symptoomrij erbij.
- "bewuste ontsnappingsklep" is uitgeschreven: containers.conf wordt niet
  overschreven, dus handmatige aanpassingen blijven staan — inclusief de
  keerzijde dat je dan afwijkt zonder melding.
- De openstaande bullet "seccomp/apparmor verder verfijnen (zie spec)" is
  weg: dat was een intentie zonder eigenaar of criterium.
- Multi-uid op gehardend Ubuntu is bevestigd getest; de bullet die het
  tegendeel beweerde is weg en de platformtabel is gelijkgetrokken met de
  macOS-rij.

hardening-verificatie.md heeft een kop gekregen die zegt wat het verifieert,
wanneer je het draait en wat je nodig hebt. Het delegeerde zijn context aan
de vier documenten die ernaar verwijzen.

docs/maximale-isolatie-linux.md is uit deze PR gehaald — ongetest, staat los
van de podman-stap en hoeft deze PR niet op te houden. De twee verwijzingen
ernaar zijn vervangen door de inhoudelijke strekking, dus er blijft geen
dode link achter. Het document volgt in een eigen PR.

Geverifieerd: script over alle *.md in de repo. Geen enkele link die deze PR
toevoegt of aanpast is stuk. Twee links blijven rood — `../issues` in
docs/verantwoording.md en `../../issues` in docs/oefeningen/verbeteringen.md
— maar die waren al stuk op main en worden in #97 gerepareerd.

* docs: tweede reviewronde — koppen, diagnoserecept en .env-documentatie (#44)

Vier punten uit de tweede review op #46:

- Het aa-complain/dmesg/aa-enforce-recept stond in de kop van het
  AppArmor-profiel. Dat beschrijft wat je doet als het profiel in de weg
  zit, niet wat het afdwingt. Verhuisd naar ADR 0001 §2.5.4 "Als het profiel
  te strak blijkt". In het profiel blijft staan dat je niet mag terugvallen
  op flags=(unconfined) — dat is een eigenschap van het profiel zelf, en
  staat op de plek waar iemand die vlag zou zetten.
- De drie installatievarianten (Linux, podman machine, Rancher) zijn
  subkoppen onder één "Installeren" geworden. Het waren losse ##-secties op
  hetzelfde niveau als "Multi-uid" en "Fallbacks", terwijl het alternatieven
  van dezelfde stap zijn. De wegwijzertabel linkt er nu naartoe.
- Het uitstel van de eigen-kernel-route verwees nergens naar. Beide plekken
  (§4.5 en §5) wijzen nu naar #99, met de kanttekening dat die routes
  ongetest zijn.
- OPEN_HTTPS had in .env.sample geen enkele toelichting en ALLOWED_DOMAINS
  alleen "Alleen nodig als OPEN_HTTPS niet true is". Net de verkeerde om
  kaal te laten: bij de default true is de allowlist een no-op, dus wie dat
  mist denkt beschermd te zijn terwijl al het uitgaand HTTPS openstaat.
  Beide uitgeschreven, inclusief dat de allowlist vóór de privilege-drop
  gelezen wordt en een wijziging dus een recreate vereist.

Geverifieerd tegen init-firewall.sh:84 dat het allowlist-blok inderdaad
alleen draait bij OPEN_HTTPS != true, en tegen regel 71 dat het script
terugvalt op false als de variabele ontbreekt — dat laatste staat er nu bij,
want het betekent dat een ontbrekende regel de sandbox stránger maakt.

Testresultaten van deze PR op een gehardende Tuxedo-host: alle stappen
groen, inclusief multi-uid (twee uid-mappings, PostgresSmokeTest) en het
hardeningsprotocol.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jordy Onrust <285647185+jonrust-minbzk@users.noreply.github.com>
Co-authored-by: Mark Reuvekamp <mark.reuvekamp@rijksoverheid.nl>
@ericwout-overheid ericwout-overheid self-assigned this Aug 27, 2026
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