KB-1755

Deny-by-Default Sandbox Profile — Phase-2 Offline Packet MVP (reproducible spec; B4′ substrate)

13 min read Revision 1
tool-kiem-thusandbox-profiledeny-by-defaultB4-primephase2-offline-mvpseccompreproducible2026-06-09

Deny-by-Default Sandbox Profile — Phase-2 Offline Packet MVP

Reproducible specification of the rev4 §12.1 L1 deny-by-default execution sandbox — the primary enforcement boundary for the offline packet MVP. Macro: PROGRAM_MACRO_PROVISION_AND_ATTEST_DENY_BY_DEFAULT_SANDBOX_FOR_PHASE2_OFFLINE_MVP_2026_06_09 Date: 2026-06-09 · Status: SANDBOX_PROFILE_SPECIFIED_NOT_ATTESTED Production mutation: NO · Install: NO · Container created by Claude: NO · Codex: NO

This document is a design/output artifact, not authority. It is a reproducible recipe an operator runs on an approved venue. It does not itself provision, run, or attest anything. Until the operator runs §6 on an approved venue and returns the §7 evidence, B4′ stays BLOCKED and the MVP build must not run.


0. Authority & provenance (Article 13)

  • Source of the requirement: KB designs/implementation-package-dot-v0-1-gap-only-scope-spec-rev4-2026-06-09.md §12.1 (deny-by-default sandbox) and checkpoints/operator-action-packet-sandbox-host-for-phase2-mvp-2026-06-09.md §2 (the 6-row minimum bar). Read KB-first.
  • Runtime evidence: governed-native mcp__claude_ai_Incomex_VPS__list_docker (read-only) — Docker runtime live, 11 containers — recorded in reports/sandbox-host-attestation-for-phase2-offline-mvp-2026-06-09.{md,json}.
  • This profile is derived from those KB sources; it is local/output content, not a competing authority. If it conflicts with rev4 §12.1, rev4 wins and this profile must be repaired.

1. Deny-by-default posture (what "deny-by-default" means here)

The sandbox grants nothing unless explicitly listed. Every capability below is absent by construction, not promised by the application:

Boundary Enforced by (load-bearing) NOT by
No network egress Absent network namespace (--network none) — there is no interface to reach anything not a client-side "don't connect" promise; not seccomp alone
No host filesystem Mount namespace = read-only rootfs + exactly two binds (/in:ro, /out:rw) not path-string filtering in the app
No secrets/credentials Scrubbed env (--env-file /dev/null, no -e) + no secret mounts not redaction in the app
No privilege escalation --cap-drop ALL + --security-opt no-new-privileges not "we run as non-root and behave"
No subprocess / no socket / no ptrace seccomp targeted-deny (execve/execveat/socket/connect/bind/ptrace → EPERM) + distroless image without a shell not an import denylist alone (that is L2, secondary)
No output-path escape rootfs read-only; only /out is writable; /in is ro not app-level path checks alone

In-process guards (L2 static-build-guard, L3 runtime self-check) are secondary / fail-fast and bypassable in principle — they do not replace this structural boundary (this is the direct answer to Codex's "static scans are bypassable" objection).

2. Minimum bar (rev4 §12.1 / operator-packet §2) → realization

# Requirement Docker/Podman realization (Option B, primary) bwrap realization (Option C, fallback)
1 No network --network none --unshare-net
2 Read-only input mount only -v <input>:/in:ro --ro-bind <input> /in
3 Write-only/output mount only -v <output>:/out:rw (sole writable path) --bind <output> /out
4 No home/project/etc/secret mounts minimal image; no extra -v; --read-only rootfs --unshare-all; no extra binds; --ro-bind /usr /usr etc. minimal
5 Scrubbed env --env-file /dev/null (no -e/--env) --clearenv
6 Syscall restriction --security-opt seccomp=seccomp-deny-by-default.json + --security-opt no-new-privileges + --cap-drop ALL --seccomp <fd> + --new-session --die-with-parent --cap-drop ALL

3. Image (Dockerfile)

Use a distroless base: no shell, no package manager → reinforces "no subprocess" structurally (there is no /bin/sh to exec). The inspector is copied in read-only.

# Dockerfile.sandbox — deny-by-default offline inspector image
# Build:  docker build -f Dockerfile.sandbox -t tki-inspector:attest .
# Distroless: no shell, no apt, nonroot uid 65532 by default.
FROM gcr.io/distroless/python3-debian12:nonroot

# Inspector code is read-only application content (built in Phase 2; placeholder dir here).
# At attestation time, a tiny probe module is sufficient; the real inspector replaces it later.
WORKDIR /app
COPY --chown=root:root inspector/ /app/

# No ENTRYPOINT that spawns a shell. Direct python invocation only.
USER 65532:65532
ENTRYPOINT ["python", "/app/main.py"]

Note: the inspector/ payload is not built by this macro (building the MVP is out of scope). For attestation, a ≤30-line probe main.py/probe.py exercising §6 is sufficient and is the operator's to place. The real inspector replaces it in the Phase-2 build.

4. seccomp profile (seccomp-deny-by-default.json)

Two valid realizations; both make execve/execveat/socket/connect/bind/ptrace return EPERM, which is what rev4 §12.1 names. The structural --network none already blocks egress regardless of seccomp; the socket denies are defense-in-depth.

Starts from the standard allow posture and explicitly denies the named dangerous syscalls. This is robust to Python's syscall needs (no risk of accidentally breaking the runtime), so it can be attested without an iterate-the-allowlist loop.

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "archMap": [
    { "architecture": "SCMP_ARCH_X86_64",
      "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] },
    { "architecture": "SCMP_ARCH_AARCH64",
      "subArchitectures": ["SCMP_ARCH_ARM"] }
  ],
  "syscalls": [
    { "names": [
        "socket", "socketpair", "connect", "bind", "listen", "accept", "accept4",
        "sendto", "sendmsg", "recvfrom", "recvmsg", "getpeername", "getsockname",
        "execve", "execveat",
        "ptrace", "process_vm_readv", "process_vm_writev",
        "mount", "umount2", "pivot_root", "chroot",
        "setns", "unshare", "clone3",
        "init_module", "finit_module", "delete_module",
        "kexec_load", "reboot", "swapon", "swapoff",
        "bpf", "perf_event_open"
      ],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}

errnoRet: 1 = EPERM. (Network calls may also surface EAFNOSUPPORT/ENETUNREACH from --network none; either is an acceptable proof-of-block — see §7.)

4b. Default-deny allowlist (hardened target) — attest after 4a, with an iterate step

"defaultAction": "SCMP_ACT_ERRNO" plus an explicit allowlist of the syscalls a Python read/write workload needs (read, write, openat, close, fstat, mmap, brk, rt_sigaction, futex, exit_group, getdents64, lseek, pread64, pwrite64, clock_gettime, epoll_*, poll, getrandom, arch_prctl, set_robust_list, madvise, …) and omitting socket/execve/ptrace. Honest caveat: the exact minimal allowlist is workload-specific and cannot be finalized without running the real inspector; treat 4b as a hardening step to validate iteratively (run → add the one syscall it EPERMs on that is provably benign → repeat) only after 4a passes. Do not present 4b as attested until it actually runs the inspector clean.

5. Run command (exact)

# Variables the operator sets (must be throwaway dirs, NOT prod paths):
INPUT_DIR=/abs/path/to/readonly/packet      # contains the governed export packet
OUTPUT_DIR=/abs/path/to/writable/out        # empty throwaway dir
SECCOMP=/abs/path/to/seccomp-deny-by-default.json

docker run --rm \
  --name tki-sandbox-attest \
  --network none \
  --user 65532:65532 \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --security-opt seccomp="$SECCOMP" \
  --pids-limit 64 \
  --memory 256m --memory-swap 256m --cpus 1 \
  --env-file /dev/null \
  -v "$INPUT_DIR":/in:ro \
  -v "$OUTPUT_DIR":/out:rw \
  tki-inspector:attest <probe-or-inspector-args>

Explicitly NOT present (deny-by-default): no --privileged, no --network host, no --pid host, no --ipc host, no -v /:/..., no -v $HOME, no -v /var/run/docker.sock, no -e SECRET=..., no --cap-add. Their absence is the boundary.

Rootless Podman equivalent (preferred for least privilege where available): replace docker with podman, keep all flags; rootless adds user-namespace isolation for free.

6. Attestation probes (exact operations the operator runs)

Run each inside the §5 container (e.g. as <probe args> → a probe main.py, or via a one-shot python -c). Each probe captures operation identity + exit code + stderr/errno + artifact path. Distroless has no shell, so probes are Python one-liners through the image's python entrypoint.

Probe Operation (inside sandbox) Expected proof-of-block Boundary / rev4 L1 test
PR-NET-1 socket.socket(); s.connect(("1.1.1.1",53)) PermissionError/OSError EPERM or ENETUNREACH/EAFNOSUPPORT process-level egress — #27
PR-NET-2 read /proc/net/dev only lo present (empty net ns) net namespace empty — #27 sibling
PR-SOCK-1 socket.socket(AF_INET,SOCK_STREAM) PermissionError EPERM (seccomp) socket() denial — #25
PR-ENV-1 sorted(os.environ.keys()) no *TOKEN*/*SECRET*/*PASSWORD*/*_DSN*/*CONN*/PG*/KB* keys scrubbed env — #28
PR-FS-RO-IN open("/in/__probe","w") OSError EROFS input mount read-only — #33
PR-FS-ESC-1 open("/etc/__probe","w") OSError EROFS (read-only rootfs) no host write / path escape — #29
PR-FS-ESC-2 open("/root/__probe","w") / open("/app/__probe","w") OSError EROFS/EACCES no escape outside /out#29/#33
PR-FS-OUT-OK open("/out/report.md","w").write("ok") success (positive control: the one writable path) output mount works — control for #33
PR-EXEC-1 os.execv("/bin/ls",["ls"]) / subprocess.run(["/bin/ls"]) PermissionError EPERM (seccomp) and/or FileNotFound (no shell) no subprocess — #34
PR-MOUNT-1 read /proc/self/mountinfo exactly: rootfs ro, /in ro, /out rw, /tmp tmpfs noexec; nothing else mount table = 2 binds only — #29/#33
PR-SOCK-DOCKER os.path.exists("/var/run/docker.sock") False no docker socket — #36
PR-PTRACE-1 (opt) ptrace(PTRACE_TRACEME) via ctypes EPERM no ptrace — #37

The rev4 acceptance matrix enumerates the L1 structural-bypass tests as #24–#37 (designs/acceptance-test-matrix-…-rev4-2026-06-09.md). The probe→test bindings above name the specific cases the build prompt cited (#25/#27/#28/#29/#33/#34/#35/#37); the final per-number binding is fixed when the harness is built in Phase 2. Probe PR-DYNIMPORT (dynamic-import outside L2 allowlist → #35) is an L2 concern and is asserted by the build-time guard, not this OS probe — noted so #35 is not silently dropped.

7. Evidence the operator returns (so Claude can verify read-only later)

A single bundle (text/JSON, deposited where Claude can read it: a KB doc under tool-kiem-thu/reports/… or the governed VPS write area /opt/incomex/docs/mcp-writes/…) containing, for every probe in §6:

{ "probe_id", "operation", "expected", "actual_stderr_or_value",
  "errno_or_exit", "verdict": "PASS|FAIL", "artifact_path" }

plus the raw /proc/self/mountinfo, the env keyset (names only), the /proc/net/dev dump, the seccomp profile sha256, the image digest, the venue (host/CI), and the runtime+version. Claude will then bind each to matrix #24–#37 and only then can B4′ acceptance be asserted — by a later run, not this one.

8. Venue & honesty bound (Article 14)

  • Approved venues: the operator's chosen deny-by-default host (Option B on the already-deployed runtime, isolated throwaway container) or a CI deny-by-default ephemeral runner (Option D). A local developer Mac attestation proves only a local/ephemeral venue and is not accepted as a substitute for the operator/CI venue (it would risk an Article-14 venue-confusion) — per owner direction 2026-06-09.
  • No fake-green: this document attests nothing. It is a recipe. PASS exists only after §6 runs and §7 evidence is verified. Until then: B4′ = BLOCKED, MVP build = must not run (P1 self-check fails closed to BLOCKED/exit 3 by design).

Authored read-only. No build, install, container creation, sandbox run, production mutation, or Codex call was performed. The profile is reproducible and waiting for the operator to execute §5–§6 on an approved venue and return §7 evidence.

Back to Knowledge Hub knowledge/dev/laws/tool-kiem-thu/designs/deny-by-default-sandbox-profile-phase2-offline-mvp-2026-06-09.md