Skip to content

Commit 02de554

Browse files
committed
refactor(sandbox): remove binary path allowlisting entirely
Binary identity enforcement via `/proc/<pid>/exe` is nearly impossible to make reliable. There is a reason that security systems like SELinux assign security "domains" to a process that are preserved across `execve()`. One of the biggest is `LD_PRELOAD` and `LD_LIBRARY_PATH`: the glibc dynamic linker will easily load arbitrary shared libraries into the address space even for "fixed purpose" binaries. And most uses of agents will have *some* writable directory. But a much bigger problem is that many binaries (including `claude`) effectively include the ability to execute arbitrary code inside that process - they are interpreters. For example, `claude` is a Bun single-file executable. A sandbox policy allowlisting only `claude` to reach api.anthropic.com is bypassed with: printf "process.stderr.write('INJECTED\n'" > /tmp/e.js BUN_OPTIONS="--preload /tmp/e.js" claude --version # => INJECTED # => 2.1.146 (Claude Code) Arbitrary JS runs inside the claude process before the app starts — with full access to credentials and sockets — while `/proc/<pid>/exe` still shows `claude`. The binary check passes; the exfiltration is permitted. It would absolutely be possible to try to craft an execution environment for an agent that closed some of these loopholes, but my opinion is that anyone doing that is already in the business of minimizing what code goes into the container, and at which point they are actually doing something better: restricting what binaries *are present at all*. That said, to do anything truly strong here gets into things like [trusted_for](https://lwn.net/Articles/832959/) etc. Policy evaluation is now just enforced based on code running within the sandbox - the same way that many other network enforcement tools work. We do not claim that we can reliably drill down into the binary-name level. Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 09bd8a9 commit 02de554

88 files changed

Lines changed: 504 additions & 5196 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/generate-sandbox-policy/SKILL.md

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@ Regardless of tier, extract (or infer) these from the user's description:
7878
| **Methods** | Specific HTTP methods to allow | Only for custom/fine-grained |
7979
| **Paths** | Specific URL paths or patterns | Only for custom/fine-grained |
8080
| **Enforcement** | `enforce` or `audit`? Default to `enforce`. | No — has a default |
81-
| **Binary** | Which binary/process should have access | Yes — ask if not stated |
82-
83-
If the host and access level are clear but binaries are not specified, ask the user which binary or process will be making the requests. Suggest common defaults like `/usr/bin/curl`, `/usr/local/bin/claude`, etc.
8481

8582
## Step 2: Refine Scope (Clarification Loop)
8683

@@ -92,7 +89,6 @@ Always ask about these if the user hasn't already specified them:
9289

9390
| Missing info | Question to ask |
9491
|-------------|----------------|
95-
| **Binary** not specified | "Which binary or process will make these requests? (e.g., `/usr/bin/curl`, `/usr/local/bin/claude`)" |
9692
| **Port** not specified | "Which port does this API use? (443 for HTTPS is typical)" |
9793
| **Enforcement** not stated | "Should policy violations be blocked (`enforce`) or just logged for review (`audit`)? I'll default to `enforce` if you're not sure." |
9894

@@ -105,7 +101,6 @@ Ask these when the user's intent is broad and more specificity is possible:
105101
| "Full access" / "allow everything" | "Do you actually need DELETE access, or would read-write (everything except DELETE) be enough?" |
106102
| "Allow access to api.example.com" (no method/path detail) | "Do you know which specific API paths or operations you need? If so, I can lock the policy down to just those. Otherwise I'll use a broad preset." |
107103
| L4-only / "just pass it through" | "L4-only means the proxy won't inspect HTTP traffic at all — any method and path will be allowed. Are you sure you don't want at least read-only or read-write restriction?" |
108-
| Wildcard binary (`/usr/bin/*`) | "A wildcard binary pattern means any binary in that directory can use this policy. Can you narrow it to specific binaries?" |
109104
| Multiple hosts in one policy | "Do all of these hosts need the same access level? If some need tighter restrictions, I can split them into separate policies." |
110105
| `access: full` with `enforcement: audit` | "Full access in audit mode means nothing is actually restricted — all traffic flows through and violations are only logged. Is that intentional, or did you want to enforce restrictions?" |
111106
| `**` path glob on all rules | "Using `**` on all paths allows any URL path. Do you know the specific API path prefixes you need (e.g., `/api/v1/`)?" |
@@ -262,10 +257,10 @@ network_policies:
262257
# Optional: allow private IP destinations (CIDR or exact IP)
263258
# allowed_ips:
264259
# - "10.0.5.0/24"
265-
binaries:
266-
- { path: <binary_path> }
267260
```
268261

262+
> **Note:** Binary allowlisting has been removed. The `binaries:` field is accepted in policy YAML for backward compatibility but is silently ignored — do not generate it in new policies.
263+
269264
### Deny Rules
270265

271266
Use `deny_rules` to block specific dangerous operations while allowing broad access. Deny rules are evaluated after allow rules and take precedence. This is the inverse of the `rules` approach — instead of enumerating every allowed operation, you grant broad access and block a small set of dangerous ones.
@@ -287,8 +282,6 @@ github_api:
287282
path: "/repos/*/branches/*/protection"
288283
- method: "*"
289284
path: "/repos/*/rulesets"
290-
binaries:
291-
- { path: /usr/bin/curl }
292285
```
293286

294287
Deny rules support the same matching capabilities as allow rules: `method`, `path`, `command` (SQL), and `query` parameter matchers. When generating policies, prefer deny rules when the user needs broad access with a small set of blocked operations — it produces a shorter, more maintainable policy than enumerating 60+ allow rules.
@@ -311,8 +304,6 @@ internal_api:
311304
port: 8080
312305
allowed_ips:
313306
- "10.0.5.0/24"
314-
binaries:
315-
- { path: /usr/bin/curl }
316307
```
317308

318309
### Policy Key Naming
@@ -345,9 +336,8 @@ Before presenting the policy to the user, verify correctness **and** flag breadt
345336

346337
### Structural Checks
347338

348-
- [ ] Every policy has `name`, `endpoints`, and `binaries`
339+
- [ ] Every policy has `name` and `endpoints`
349340
- [ ] Every endpoint has `host` and `port`
350-
- [ ] Every binary has `path`
351341
- [ ] Policy key matches `name` field
352342

353343
### Breadth Warnings
@@ -360,7 +350,6 @@ Evaluate the generated policy for overly broad access and **include warnings in
360350
| **`access: full`** | "This policy allows all HTTP methods (including DELETE) on all paths. If you don't need DELETE, `read-write` is safer. If you only need to read, `read-only` is the most restrictive option." |
361351
| **`access: full` + `enforcement: audit`** | "Full access in audit mode provides no actual restriction — all traffic flows through. This is effectively a monitoring-only policy." |
362352
| **`access: read-write`** when user hasn't confirmed write need | "This policy allows POST, PUT, and PATCH on all paths. If you only need to read data, `read-only` is more restrictive." |
363-
| **Wildcard binary** (`*` or `**` in binary path) | "This policy allows any binary matching the glob pattern. A compromised or unexpected binary in that directory could use this policy. Consider listing specific binary paths." |
364353
| **`**` path glob** on all explicit rules | "All rules use `**` path patterns, which match any URL path. This is equivalent to a preset — consider using `access: read-only` (or similar) for clarity, or narrowing paths if you know the API structure." |
365354
| **Multiple broad endpoints** in one policy | "This policy grants the same broad access to N different hosts. If any of these hosts needs tighter restrictions later, you'll need to split the policy." |
366355
| **Hostless `allowed_ips`** (no `host` field) | "This endpoint has no `host` — any domain resolving to the allowed IP range on this port will be permitted. Consider adding a `host` field to restrict which domains can use this allowlist." |
@@ -396,12 +385,12 @@ The policy needs to go somewhere. Determine which mode applies:
396385
- Whether the file uses compact (`{ host: ..., port: ... }`) or expanded YAML style
397386

398387
2. **Check for conflicts**:
399-
- Does a policy with the same key already exist? If so, ask the user whether to **replace** it, **merge** new endpoints/binaries into it, or use a different key.
388+
- Does a policy with the same key already exist? If so, ask the user whether to **replace** it, **merge** new endpoints into it, or use a different key.
400389
- Does an existing policy already cover the same host:port? Warn the user — overlapping endpoint coverage across policies causes OPA evaluation errors (complete rule conflict).
401390

402391
3. **Apply the change**:
403392
- **Adding a new policy**: Insert the new policy block under `network_policies`, maintaining the file's existing indentation and style.
404-
- **Modifying an existing policy**: Edit the specific policy in place — add/remove endpoints, change access presets, update rules, add binaries, etc.
393+
- **Modifying an existing policy**: Edit the specific policy in place — add/remove endpoints, change access presets, update rules, etc.
405394
- **Removing a policy**: Delete the policy block if the user asks.
406395

407396
4. **Preserve everything else**: Do not modify `filesystem_policy`, `landlock`, `process`, or other policies unless the user explicitly asks.
@@ -458,7 +447,7 @@ Show the generated policy YAML with:
458447

459448
After presenting or applying the policy, ask if the user wants to:
460449
- Tighten or loosen any rules
461-
- Add more endpoints or binaries
450+
- Add more endpoints
462451
- Switch between enforce/audit mode
463452
- Move from a preset to explicit rules (or vice versa)
464453
- Apply the policy to a file (if presented only)
@@ -473,8 +462,6 @@ my_api:
473462
name: my_api
474463
endpoints:
475464
- { host: api.example.com, port: 443 }
476-
binaries:
477-
- { path: /usr/bin/curl }
478465
```
479466
480467
### HTTPS API with Read-Only Preset
@@ -489,8 +476,6 @@ my_api_readonly:
489476
tls: terminate
490477
enforcement: enforce
491478
access: read-only
492-
binaries:
493-
- { path: /usr/bin/curl }
494479
```
495480
496481
### HTTPS API with Explicit Rules
@@ -511,9 +496,6 @@ my_api_custom:
511496
- allow:
512497
method: POST
513498
path: "/api/v1/data"
514-
binaries:
515-
- { path: /usr/bin/curl }
516-
- { path: /usr/local/bin/myapp }
517499
```
518500
519501
### HTTP (non-TLS) Internal API
@@ -533,8 +515,6 @@ internal_svc:
533515
- allow:
534516
method: POST
535517
path: "/api/v1/jobs"
536-
binaries:
537-
- { path: /usr/bin/curl }
538518
```
539519
540520
### Private IP Access (Host + Allowlist)
@@ -547,8 +527,6 @@ internal_db:
547527
port: 5432
548528
allowed_ips:
549529
- "10.0.5.0/24"
550-
binaries:
551-
- { path: /usr/bin/curl }
552530
```
553531
554532
### Private IP Access (Hostless — Any Domain in Range)
@@ -561,8 +539,6 @@ private_services:
561539
allowed_ips:
562540
- "10.0.5.0/24"
563541
- "10.0.6.0/24"
564-
binaries:
565-
- { path: /usr/bin/curl }
566542
```
567543
568544
## Additional Resources

0 commit comments

Comments
 (0)