Skip to content

fix(defaults): fail closed on the four remaining pre-audit gaps - #156

Merged
las7 merged 1 commit into
mainfrom
harden/secure-defaults
Aug 11, 2026
Merged

fix(defaults): fail closed on the four remaining pre-audit gaps#156
las7 merged 1 commit into
mainfrom
harden/secure-defaults

Conversation

@las7

@las7 las7 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

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_mode now defaults to strict

It was permissive, so on any host without gVisor every job silently ran on runc while CLAUDE.md says "gVisor remains the sole isolation boundary". Strict refuses to run instead. Set permissive explicitly for local dev and CI on hosts without gVisor.

2. Loopback by default, and unauthenticated network bind is refused

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 — 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 install resolves 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_command puts workspace_dir straight into -v {dir}:/workspace, and SessionRecord.workspace_dir validated 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_posture relaxes only security_mode when 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):

  • 802 passed under strict + runsc.
  • 798 passed with runsc removed to simulate a CI runner; the remaining failures were this box's known load-related container stalls, which reproduce on origin/main and 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 chown abort 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

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).
@las7
las7 requested a review from ethanbailie as a code owner August 11, 2026 18:23
@las7
las7 merged commit 168536a into main Aug 11, 2026
4 checks passed
@las7 las7 mentioned this pull request Aug 12, 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