fix(defaults): fail closed on the four remaining pre-audit gaps - #156
Merged
Conversation
Follow-up to #153. Those were controls that did not work; these are defaults and boundaries that worked exactly as written, but where what was written was the permissive choice. A scanner reads the default, not the warning next to it. 1. security_mode now defaults to 'strict'. It was 'permissive', so on any host without gVisor every job silently ran on runc while the docs called gVisor "the sole isolation boundary". Strict refuses to run instead. Set 'permissive' explicitly for local dev and CI on hosts without gVisor. 2. server_host now defaults to 127.0.0.1, and binding a non-loopback interface while api_auth_enabled is false is REFUSED at startup rather than warned about. That combination is an unauthenticated remote code-execution endpoint; a log line is the wrong control for it, because the operator who most needs it is the one not reading logs. allow_unauthenticated_network_access=true is the explicit opt-out for deployments that authenticate in front of Tako VM. 3. The shared uv dependency cache is now scoped per job type instead of one host-wide volume. It is mounted read-write at a path the sandbox user owns and stays mounted for the container's whole life, not just the install phase, so one job can write a cache entry a later job's `uv pip install` resolves and executes. This narrows the blast radius to a group the operator defines; it does NOT eliminate the channel, since jobs sharing a job type still share a cache. Tako VM has no tenant identity to key on. Documented as such in uv_cache_volume(). 4. build_session_run_command now validates workspace_dir before it becomes a read-write host bind mount: absolute, normalized, no '..', not a sensitive system directory, and symlink-resolved so a symlinked workspace cannot redirect the mount. Nothing calls this yet (Phase 1a scaffolding) -- the guard is added while the path is still unreachable, rather than after. Tests cover each: the secure defaults, that the unauthenticated network bind is refused and the loopback/authenticated/opted-out cases still work, that cache scopes are distinct and shell-safe, and a table of unsafe workspace paths including a symlink redirect. Existing tests that asserted the old defaults are updated to assert the new intent rather than the new strings, and the default-posture test relaxes only security_mode when the host has no gVisor, since seccomp and capabilities are runtime-independent. Docs, SECURITY.md and tako_vm.yaml.example updated to match. Verified on a host with gVisor installed: 802 passed under strict + runsc, and 798 passed with runsc removed to simulate a CI runner (remaining failures were this box's known load-related container stalls, which reproduce on origin/main and did not recur after cleanup).
Merged
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.
Follow-up to #153. Those were controls that did not work. These are defaults and boundaries that worked exactly as written, but where what was written was the permissive choice — and a scanner reads the default, not the warning next to it.
1.
security_modenow defaults tostrictIt was
permissive, so on any host without gVisor every job silently ran onruncwhileCLAUDE.mdsays "gVisor remains the sole isolation boundary". Strict refuses to run instead. Setpermissiveexplicitly for local dev and CI on hosts without gVisor.2. Loopback by default, and unauthenticated network bind is refused
server_hostnow defaults to127.0.0.1, and binding a non-loopback interface whileapi_auth_enabledis false is refused at startup rather than warned about. That combination is an unauthenticated remote code-execution endpoint; a log line is the wrong control for it, because the operator who most needs it is the one not reading logs.allow_unauthenticated_network_access: trueis the explicit opt-out for deployments that authenticate in front of Tako VM — and it still emits the warning.3. Dependency cache scoped per job type
The shared uv cache is mounted read-write at a path the sandbox user owns, and it stays mounted for the container's whole life, not just the install phase. So one job can write a cache entry that a later job's
uv pip installresolves and executes. It was a single host-wide volume.Now scoped per job type. This narrows the blast radius to a group the operator defines; it does not eliminate the channel — jobs sharing a job type still share a cache. Tako VM has no tenant identity to key on, and inventing one here would be the wrong call. Documented honestly in
uv_cache_volume().4. Session workspace containment, added before the path is reachable
build_session_run_commandputsworkspace_dirstraight into-v {dir}:/workspace, andSessionRecord.workspace_dirvalidated length only. Now validated at the point of use: absolute, normalized, no.., not a sensitive system directory, and symlink-resolved so a symlinked workspace cannot redirect the mount.Nothing calls this yet (Phase 1a scaffolding). The guard goes in while the path is still unreachable, rather than after.
Tests
Each item has coverage: the secure defaults; that the unauthenticated network bind is refused while loopback / authenticated / explicitly-opted-out all still work; that cache scopes are distinct and shell-safe; and a table of unsafe workspace paths including a symlink redirect.
Existing tests that asserted the old defaults are updated to assert the new intent, not the new strings.
test_default_posturerelaxes onlysecurity_modewhen the host has no gVisor, since seccomp and capabilities are runtime-independent.Verification
gVisor was installed on the audit host for this (it was the gap flagged in #153):
strict+runsc.runscremoved to simulate a CI runner; the remaining failures were this box's known load-related container stalls, which reproduce onorigin/mainand did not recur after freeing disk (the host sits at 92% full).Also worth recording from that exercise: of the four defects in #153, the seccomp-blocks-init one was runc-specific — runsc runs its own container init and never tripped the filter. The entrypoint
chownabort and the CAP_KILL timeout failure reproduced identically under gVisor, i.e. they broke the recommended production boundary.🤖 Generated with Claude Code
https://claude.ai/code/session_01HCi216irm5J9XxAW14WGPh