# Security Audit Round 2 — Agent-Agnostic Architecture

**Date:** 2026-03-17
**Host:** lima-ubuntu, Linux 6.8.0-101-generic aarch64
**Backends tested:** bwrap, firejail, landlock
**Agents installed:** claude 2.1.77, aider 0.86.2, codex 0.115.0, gemini 0.34.0, opencode 0.0.55
**Test projects:** alpha, beta, gamma/deep/nested/project
**Methodology:** 4 specialist agents (pentest, functional, Slurm scope, Gemini review) + automated test suite

---

## Executive Summary

Multi-agent security audit covering the new agent-agnostic architecture, agent overlay system, cross-project isolation, Slurm job scoping, and hardened sandbox-config directories. All findings from Round 1 were verified as fixed. New vulnerabilities discovered and fixed during this round.

| Severity | Found | Fixed | Remaining |
|----------|-------|-------|-----------|
| **Critical** | 2 | 2 | 0 |
| **High** | 3 | 2 | 1 (design) |
| **Medium** | 4 | 3 | 1 (design) |
| **Low** | 5 | 2 | 3 (accepted) |

---

## Findings Fixed During This Audit

### F1: Merged instruction files writable from inside sandbox (CRITICAL → FIXED)
**Commit:** `3f6c6c7`

The `chmod a-w` protection on merged CLAUDE.md, settings.json, AGENTS.md, GEMINI.md was completely bypassable because the file owner can `chmod u+w`, then `rm` and recreate.

**Fix:** bwrap `--ro-bind` and firejail `--read-only` on all sandbox-config directories. Protection is now kernel-enforced (Read-only file system).

**Verification:**
```
write: BLOCKED (Permission denied)
chmod: BLOCKED (Read-only file system)
rm: BLOCKED (Read-only file system)
touch: BLOCKED (Read-only file system)
```

### F2: Chaperon session ID changes between requests (CRITICAL → FIXED)
**Commit:** current

`_CHAPERON_SESSION_ID` in `_handler_lib.sh` was unconditionally re-evaluated each time the file was sourced (every handler dispatch). Since the epoch component changes each second, the session ID was different between sbatch and squeue calls, making `SLURM_SCOPE=session` fundamentally broken.

**Fix:** Guard with `if [[ -z "${_CHAPERON_SESSION_ID:-}" ]]` so it's set only once per chaperon process.

### F3: SLURM_SCOPE env override ignored (HIGH → FIXED)
**Commit:** current

`SLURM_SCOPE=session sandbox-exec.sh ...` didn't work because `sandbox.conf` unconditionally sets `SLURM_SCOPE="project"`, overwriting the env value during config loading.

**Fix:** Save env value before config loading (`_SLURM_SCOPE_ENV`), restore it before exporting to chaperon.

### F4: Chaperon squeue/scancel hang on firejail/landlock (CRITICAL → FIXED)
**Commit:** `fb81a32`

The chaperon's `squeue --me` call hung indefinitely on firejail/landlock backends. Root cause: child processes inherited FD 3 (req FIFO) from the chaperon, and/or slurmctld connection issues specific to the chaperon's process context.

**Fix:** Added `timeout 10` on all squeue calls, explicit `/usr/bin/squeue` path, and `3>&-` FD closure in dispatch.

### F5: User config silently ignored (HIGH → FIXED)
**Commit:** `fb81a32`

When admin config exists at `/app/lib/agent-sandbox/sandbox.conf`, the code looked for `user.conf` but users had `sandbox.conf`. All user customizations (additional blocked env vars, extra paths) were silently dropped.

**Fix:** Fall back to loading `sandbox.conf` when `user.conf` is missing.

### F6: Phantom agent detection from stub directories (HIGH → FIXED)
**Commit:** `fb81a32`

Stub directory creation (e.g., `~/.config/opencode/`) caused agents to be "detected" on subsequent runs even after uninstallation, triggering env var unblocking from their `env.conf`.

**Fix:** Removed stub directory creation entirely.

### F7: Firejail BLOCKED_FILES symlink resolution (MEDIUM → FIXED)
**Commit:** `fb81a32`

Firejail `--blacklist` didn't resolve symlinks (unlike bwrap's `readlink -f`), allowing blocked files to be accessed through symlink indirection.

**Fix:** Resolve symlinks and blacklist both the link and its target.

### F8: Overlay race condition on concurrent instances (MEDIUM → FIXED)
**Commit:** current

Two concurrent sandbox instances racing on `CLAUDE.md.tmp` caused `mv: cannot stat` errors. The shared `.tmp` filename was the contention point.

**Fix:** PID-based tmp filenames (`CLAUDE.md.tmp.$$`) to eliminate contention.

### F9: No timeout on agent detect.sh (MEDIUM → FIXED)
**Commit:** `fb81a32`

A broken or malicious `detect.sh` could hang indefinitely, stalling sandbox startup.

**Fix:** Wrapped in `timeout 2`.

---

## Remaining Known Issues (Design Limitations)

### R1: Project-dir == SANDBOX_DIR allows sandbox script modification (HIGH)
**Status:** Accepted (requires admin-owned installation to mitigate)

When `--project-dir` points to the sandbox installation directory itself, the writable project-dir bind mount overrides the read-only SANDBOX_DIR mount. A sandboxed agent can modify sandbox scripts, enabling persistent escape on the next invocation.

**Mitigation:** Admin-owned installation at `/app/lib/agent-sandbox/` (see `ADMIN_INSTALL.md`).

### R2: Cross-project READ access via bwrap (MEDIUM)
**Status:** Accepted (bwrap design limitation)

When projects are subdirectories of a common parent (e.g., both under `~/projects/`), the parent is ro-bound, giving read access to sibling projects. Write isolation is enforced.

**Note:** Landlock does NOT have this issue — it properly denies cross-project reads.

### R3: Shared agent config dirs across projects (LOW)
**Status:** Accepted (by design)

All sandbox instances share `~/.claude/`, `~/.codex/`, etc. Session history from one project is readable by another. The sandbox-config overlay is shared (single merged CLAUDE.md for all projects).

### R4: Landlock backend limitations (LOW)
**Status:** Documented

No BLOCKED_FILES, no /tmp isolation, no PID namespace, full /dev access, full process visibility. Inherent kernel-level limitations of Landlock LSM.

### R5: Cross-agent env var unblocking (LOW)
**Status:** Accepted (by design)

When multiple agents are detected, all env.conf unblocking applies. E.g., aider's presence unblocks OPENAI_API_KEY even for Claude sessions.

---

## Test Results

### Automated Test Suite
- **bwrap:** 93 passed, 0 failed, 0 skipped
- **firejail:** 90 passed, 0 failed, 1 skipped
- **landlock:** 73 passed, 0 failed, 7 skipped
- **Total:** 256 passed, 0 failed, 8 skipped

### Slurm Job Scoping
| Test | Status |
|------|--------|
| Project scope isolation (alpha vs beta) | PASS |
| Session scope (same-session visibility) | PASS (after fix) |
| User scope (all-jobs visibility) | PASS |
| Cross-scope scancel (beta can't cancel alpha) | PASS |
| Nested project dir (gamma/deep/nested/project) | PASS |
| Comment tag preservation | PASS |
| scontrol show job scoping | PASS |
| scontrol update Comment blocked | PASS |

### Agent Overlay Verification
| Agent | Detected | Overlay | Launch | Read-only |
|-------|----------|---------|--------|-----------|
| Claude | YES | CLAUDE.md + settings.json | N/A | PASS (ro-bind) |
| Aider | YES | AIDER_READ | v0.86.2 | via SANDBOX_DIR |
| Codex | YES | AGENTS.md | v0.115.0 | PASS (ro-bind) |
| Gemini | YES | GEMINI.md | v0.34.0 | PASS (ro-bind) |
| OpenCode | YES | AGENTS.md | v0.0.55 | PASS (ro-bind) |

### Cross-Project Isolation
| Test | bwrap | firejail | landlock |
|------|-------|----------|----------|
| Write isolation | PASS | PASS | PASS |
| Read isolation | FAIL* | PASS | PASS |
| conf.d guard bypass | PASS | PASS | PASS |
| Path traversal | BLOCKED | BLOCKED | BLOCKED |
| Symlink escape | FAIL* | PASS | PASS |
| Concurrent instances | PASS | PASS | PASS |

*bwrap allows read (not write) to sibling dirs under the same parent mount — see R2.

---

## Previous Round 1 Fixes — Verified

| Fix | Status |
|-----|--------|
| HOME override via getent | Verified |
| _load_untrusted_config subprocess isolation | Verified |
| Admin policy enforcement after conf.d | Verified |
| FD > 2 closure before sandbox entry | Verified |
| Seccomp filter (io_uring, userfaultfd, kexec) | Verified |
| PID namespace isolation (bwrap/firejail) | Verified |
| /tmp isolation (PRIVATE_TMP) | Verified |
| FILTER_PASSWD (LDAP user enumeration) | Verified |

---

## Round 3 — Final Convergence Audit

**Date:** 2026-03-17
**Method:** 5 specialist agents (pentest, functional, README audit, ADMIN audit, CHAPERON audit)
**Result:** No new code bugs. Only design limitations and edge cases found.

### Findings (all accepted as design limitations or low-severity)

| # | Finding | Severity | Status |
|---|---------|----------|--------|
| 1 | Abstract Unix socket cross-sandbox communication | MEDIUM | Accepted (needs --unshare-net, breaks API access) |
| 2 | Shared HOME writable paths cross-sandbox | MEDIUM | Accepted (by design for agent state) |
| 3 | LD_PRELOAD/LD_LIBRARY_PATH not blocked | LOW | Accepted (conda, CUDA, MKL rely on them; agent already has code exec) |
| 4 | Concurrent overlay race on chmod cycle | LOW | Accepted (PID-based tmps minimize, flock would add latency) |
| 5 | inotify on read-only sandbox-config | LOW | Inherent to mount namespace |
| 6 | /proc/self/mem accessible | INFO | Expected (own process memory) |
| 7 | Landlock shared /tmp and /dev/shm | LOW | Known Landlock limitation |
| 8 | Firejail spaces in project path | LOW | Firejail argument parser limitation |

### Functional Test Results
- **Full test suite:** 256 passed, 0 failed, 8 skipped
- **Agent×project×backend matrix:** 36/36 pass (4 agents × 3 dirs × 3 backends)
- **Overlay idempotency:** 5/5 identical (md5sum stable)
- **Concurrent instances:** PASS
- **Edge cases:** empty dir PASS, spaces PASS (bwrap/landlock), FAIL (firejail)

### Documentation Audit Results
- **README.md:** 3 fixes (codex env.conf description, SLURM_SCOPE docs, credential table)
- **ADMIN_INSTALL.md:** 8 fixes (chaperon install, agent profile contract, SLURM_SCOPE, user.conf fallback)
- **CHAPERON.md:** 8 fixes (FD closure, session ID, CWD validation, timeouts, protocol accuracy)
- **sandbox-lib.sh:** 1 comment fix (PRIVATE_TMP scope)
