Refactor and improve documentation, add examples
This commit is contained in:
@@ -302,93 +302,4 @@ With `-m` on Linux, you only see proxy-level denials:
|
|||||||
|
|
||||||
## Security Model
|
## Security Model
|
||||||
|
|
||||||
### How Each Layer Works
|
See [`docs/security-model.md`](docs/security-model.md) for Fence's threat model, guarantees, and limitations.
|
||||||
|
|
||||||
#### Network Isolation
|
|
||||||
|
|
||||||
All outbound connections are routed through local HTTP/SOCKS5 proxies that filter by domain:
|
|
||||||
|
|
||||||
- Direct socket connections are blocked at the OS level (syscall filtering on macOS, network namespace on Linux)
|
|
||||||
- Only localhost connections to the proxy ports are allowed
|
|
||||||
- The proxy inspects the target domain and allows/denies based on config
|
|
||||||
- Environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`) route application traffic
|
|
||||||
|
|
||||||
#### Filesystem Restrictions
|
|
||||||
|
|
||||||
Access control follows a deny-by-default model for writes:
|
|
||||||
|
|
||||||
| Operation | Default | Config |
|
|
||||||
|-----------|---------|--------|
|
|
||||||
| Read | Allow all | `denyRead` blocks specific paths |
|
|
||||||
| Write | Deny all | `allowWrite` permits specific paths |
|
|
||||||
| Write exceptions | - | `denyWrite` overrides `allowWrite` |
|
|
||||||
|
|
||||||
#### Dangerous File Protection
|
|
||||||
|
|
||||||
Certain paths are always protected from writes regardless of config to prevent common attack vectors:
|
|
||||||
|
|
||||||
**Protected files:**
|
|
||||||
|
|
||||||
- Shell configs: `.bashrc`, `.bash_profile`, `.zshrc`, `.zprofile`, `.profile`
|
|
||||||
- Git config: `.gitconfig`, `.gitmodules`, `.git/config` (can define aliases that run code)
|
|
||||||
- Git hooks: `.git/hooks/*` (can execute arbitrary code on git operations)
|
|
||||||
- Tool configs: `.ripgreprc`, `.mcp.json`
|
|
||||||
|
|
||||||
**Protected directories:**
|
|
||||||
|
|
||||||
- IDE/editor settings: `.vscode`, `.idea`
|
|
||||||
- Claude agent configs: `.claude/commands`, `.claude/agents`
|
|
||||||
|
|
||||||
#### Process Isolation
|
|
||||||
|
|
||||||
- Commands run in isolated namespaces (Linux) or with syscall restrictions (macOS)
|
|
||||||
- PID namespace isolation prevents seeing/signaling host processes (Linux)
|
|
||||||
- New session prevents terminal control attacks
|
|
||||||
|
|
||||||
### What Fence Blocks
|
|
||||||
|
|
||||||
| Attack Vector | How It's Blocked |
|
|
||||||
|---------------|------------------|
|
|
||||||
| Arbitrary network access | Proxy filtering + OS-level enforcement |
|
|
||||||
| Data exfiltration to unknown hosts | Only allowlisted domains reachable |
|
|
||||||
| Writing malicious git hooks | `.git/hooks` always protected |
|
|
||||||
| Modifying shell startup files | `.bashrc`, `.zshrc`, etc. always protected |
|
|
||||||
| Reading sensitive paths | Configurable via `denyRead` |
|
|
||||||
| Arbitrary filesystem writes | Only `allowWrite` paths permitted |
|
|
||||||
|
|
||||||
### What Fence Allows
|
|
||||||
|
|
||||||
- Network access to explicitly allowed domains
|
|
||||||
- Filesystem reads (except `denyRead` paths)
|
|
||||||
- Filesystem writes to `allowWrite` paths
|
|
||||||
- Process spawning within the sandbox
|
|
||||||
- Inbound connections on exposed ports (`-p` flag)
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
|
|
||||||
#### Domain-based filtering only
|
|
||||||
|
|
||||||
Fence doesn't inspect request content. A sandboxed process can:
|
|
||||||
|
|
||||||
- Download arbitrary code from allowed domains
|
|
||||||
- Exfiltrate data by encoding it in requests to allowed domains
|
|
||||||
- Use allowed domains as tunnels if they support it
|
|
||||||
|
|
||||||
You're trusting your allowlist - if you allow `*.github.com`, the sandbox can reach any GitHub-hosted content.
|
|
||||||
|
|
||||||
#### Root processes may escape restrictions
|
|
||||||
|
|
||||||
- *Linux*: The sandboxed process has root inside its namespace and could potentially manipulate mounts, cgroups, or exploit kernel vulnerabilities. Network namespace isolation (`--unshare-net`) is solid, but filesystem isolation via bind mounts is more permeable.
|
|
||||||
- *macOS*: `sandbox-exec` is robust at the kernel level, but root can modify system state before the sandbox starts or (on older macOS) load kernel extensions.
|
|
||||||
|
|
||||||
#### macOS sandbox-exec is deprecated
|
|
||||||
|
|
||||||
Apple deprecated `sandbox-exec` but it still works on current macOS (including Sequoia). There's no good alternative for CLI sandboxing:
|
|
||||||
|
|
||||||
- App Sandbox requires signed `.app` bundles
|
|
||||||
- Virtualization.framework is heavyweight
|
|
||||||
- We use `sandbox-exec` pragmatically until Apple removes it
|
|
||||||
|
|
||||||
#### Not for hostile code containment
|
|
||||||
|
|
||||||
Fence is defense-in-depth for running semi-trusted code (npm install, build scripts, CI jobs), not a security boundary against actively malicious software designed to escape sandboxes.
|
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -1,10 +1,10 @@
|
|||||||
# fence
|
# Fence
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A Go implementation of process sandboxing with network and filesystem restrictions.
|
A Go implementation of process sandboxing with network and filesystem restrictions.
|
||||||
|
|
||||||
`fence` wraps commands in a sandbox that blocks network access by default and restricts filesystem operations based on configurable rules. Useful for AI coding agents, untrusted code execution, or running processes with controlled side effects.
|
Fence wraps commands in a sandbox that blocks network access by default and restricts filesystem operations based on configurable rules. It's most useful for running semi-trusted code (package installs, build scripts, CI jobs, unfamiliar repos) with controlled side effects, and it can also complement AI coding agents as defense-in-depth.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ A Go implementation of process sandboxing with network and filesystem restrictio
|
|||||||
- **Cross-Platform**: macOS (sandbox-exec) and Linux (bubblewrap)
|
- **Cross-Platform**: macOS (sandbox-exec) and Linux (bubblewrap)
|
||||||
- **HTTP/SOCKS5 Proxies**: Built-in filtering proxies for domain control
|
- **HTTP/SOCKS5 Proxies**: Built-in filtering proxies for domain control
|
||||||
|
|
||||||
You can use `fence` as a Go package or CLI tool.
|
You can use Fence as a Go package or CLI tool.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -44,51 +44,7 @@ fence -c "echo hello && ls"
|
|||||||
fence -d curl https://example.com
|
fence -d curl https://example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
For a more detailed introduction, see the [Quickstart Guide](docs/quickstart.md).
|
||||||
|
|
||||||
Create `~/.fence.json` to configure allowed domains and filesystem access:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"network": {
|
|
||||||
"allowedDomains": ["github.com", "*.npmjs.org", "registry.yarnpkg.com"],
|
|
||||||
"deniedDomains": ["evil.com"]
|
|
||||||
},
|
|
||||||
"filesystem": {
|
|
||||||
"denyRead": ["/etc/passwd"],
|
|
||||||
"allowWrite": [".", "/tmp"],
|
|
||||||
"denyWrite": [".git/hooks"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Network Configuration
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `allowedDomains` | List of allowed domains. Supports wildcards like `*.example.com` |
|
|
||||||
| `deniedDomains` | List of denied domains (checked before allowed) |
|
|
||||||
| `allowUnixSockets` | List of allowed Unix socket paths (macOS) |
|
|
||||||
| `allowAllUnixSockets` | Allow all Unix sockets |
|
|
||||||
| `allowLocalBinding` | Allow binding to local ports |
|
|
||||||
| `allowLocalOutbound` | Allow outbound connections to localhost, e.g., local DBs (defaults to `allowLocalBinding` if not set) |
|
|
||||||
| `httpProxyPort` | Fixed port for HTTP proxy (default: random available port) |
|
|
||||||
| `socksProxyPort` | Fixed port for SOCKS5 proxy (default: random available port) |
|
|
||||||
|
|
||||||
### Filesystem Configuration
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `denyRead` | Paths to deny reading (deny-only pattern) |
|
|
||||||
| `allowWrite` | Paths to allow writing |
|
|
||||||
| `denyWrite` | Paths to deny writing (takes precedence) |
|
|
||||||
| `allowGitConfig` | Allow writes to `.git/config` files |
|
|
||||||
|
|
||||||
### Other Options
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `allowPty` | Allow pseudo-terminal (PTY) allocation in the sandbox (for MacOS) |
|
|
||||||
|
|
||||||
## CLI Usage
|
## CLI Usage
|
||||||
|
|
||||||
@@ -174,25 +130,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## Documentation
|
||||||
|
|
||||||
### macOS (sandbox-exec)
|
- [Documentation index](docs/)
|
||||||
|
- [Security model](docs/security-model.md)
|
||||||
On macOS, fence uses Apple's `sandbox-exec` with a generated seatbelt profile that:
|
- [Architecture](ARCHITECTURE.md)
|
||||||
|
- [Examples](examples/)
|
||||||
- Denies all operations by default
|
|
||||||
- Allows specific Mach services needed for basic operation
|
|
||||||
- Controls network access via localhost proxies
|
|
||||||
- Restricts filesystem read/write based on configuration
|
|
||||||
|
|
||||||
### Linux (bubblewrap)
|
|
||||||
|
|
||||||
On Linux, fence uses `bubblewrap` (bwrap) with:
|
|
||||||
|
|
||||||
- Network namespace isolation (`--unshare-net`)
|
|
||||||
- Filesystem bind mounts for access control
|
|
||||||
- PID namespace isolation
|
|
||||||
- Unix socket bridges for proxy communication
|
|
||||||
|
|
||||||
For detailed security model, limitations, and architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
For detailed security model, limitations, and architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
||||||
|
|
||||||
@@ -208,12 +151,6 @@ For detailed security model, limitations, and architecture, see [ARCHITECTURE.md
|
|||||||
- `bubblewrap` (for sandboxing)
|
- `bubblewrap` (for sandboxing)
|
||||||
- `socat` (for network bridging)
|
- `socat` (for network bridging)
|
||||||
|
|
||||||
Install on Ubuntu/Debian:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
apt install bubblewrap socat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
Portions of this project are derived from Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) (Apache-2.0). This repository contains modifications and additional original work.
|
Portions of this project are derived from Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) (Apache-2.0). This repository contains modifications and additional original work.
|
||||||
|
|||||||
51
docs/README.md
Normal file
51
docs/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Fence Documentation
|
||||||
|
|
||||||
|
Fence is a sandboxing tool that restricts **network** and **filesystem** access for arbitrary commands. It's most useful for running semi-trusted code (package installs, build scripts, CI jobs, unfamiliar repos) with controlled side effects.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
- **[Quickstart](quickstart.md)** - Install fence and run your first sandboxed command in 5 minutes
|
||||||
|
- **[Why Fence](why-fence.md)** - What problem it solves (and what it doesn't)
|
||||||
|
|
||||||
|
## Guides
|
||||||
|
|
||||||
|
- **[Concepts](concepts.md)** - Mental model: OS sandbox + local proxies + config
|
||||||
|
- **[Troubleshooting](troubleshooting.md)** - Common failure modes and fixes
|
||||||
|
- **[Using Fence with AI Agents](agents.md)** - Defense-in-depth and policy standardization
|
||||||
|
- **[Recipes](recipes/README.md)** - Common workflows (npm/pip/git/CI)
|
||||||
|
- **[Config Templates](templates/)** - Copy/paste templates you can start from
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [README](../README.md) - CLI usage + configuration reference
|
||||||
|
- [Architecture](../ARCHITECTURE.md) - How fence works under the hood
|
||||||
|
- [Security Model](security-model.md) - Threat model, guarantees, and limitations
|
||||||
|
- [Security Policy](../SECURITY.md) - Vulnerability reporting policy
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See [`examples/`](../examples/README.md) for runnable demos.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Block all network (default)
|
||||||
|
fence <command>
|
||||||
|
|
||||||
|
# Use custom config
|
||||||
|
fence --settings ./fence.json <command>
|
||||||
|
|
||||||
|
# Debug mode (verbose output)
|
||||||
|
fence -d <command>
|
||||||
|
|
||||||
|
# Monitor mode (show blocked requests)
|
||||||
|
fence -m <command>
|
||||||
|
|
||||||
|
# Expose port for servers
|
||||||
|
fence -p 3000 <command>
|
||||||
|
|
||||||
|
# Run shell command
|
||||||
|
fence -c "echo hello && ls"
|
||||||
|
```
|
||||||
47
docs/agents.md
Normal file
47
docs/agents.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Using Fence with AI Agents
|
||||||
|
|
||||||
|
Many popular coding agents already include sandboxing. Fence can still be useful when you want a **tool-agnostic** policy layer that works the same way across:
|
||||||
|
|
||||||
|
- local developer machines
|
||||||
|
- CI jobs
|
||||||
|
- custom/internal agents or automation scripts
|
||||||
|
- different agent products (as defense-in-depth)
|
||||||
|
|
||||||
|
## Recommended approach
|
||||||
|
|
||||||
|
Treat an agent as "semi-trusted automation":
|
||||||
|
|
||||||
|
- **Restrict writes** to the workspace (and maybe `/tmp`)
|
||||||
|
- **Allowlist only the network destinations** you actually need
|
||||||
|
- Use `-m` (monitor mode) to audit blocked attempts and tighten policy
|
||||||
|
|
||||||
|
Fence can also reduce the risk of running agents with fewer interactive permission prompts (e.g. "skip permissions"), **as long as your Fence config tightly scopes writes and outbound destinations**. It's defense-in-depth, not a substitute for the agent's own safeguards.
|
||||||
|
|
||||||
|
## Example: API-only agent
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["api.openai.com", "api.anthropic.com"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["."]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json <agent-command>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protecting your environment
|
||||||
|
|
||||||
|
Fence includes additional "dangerous file protection (writes blocked regardless of config) to reduce persistence and environment-tampering vectors like:
|
||||||
|
|
||||||
|
- `.git/hooks/*`
|
||||||
|
- shell startup files (`.zshrc`, `.bashrc`, etc.)
|
||||||
|
- some editor/tool config directories
|
||||||
|
|
||||||
|
See `ARCHITECTURE.md` for the full list and rationale.
|
||||||
56
docs/concepts.md
Normal file
56
docs/concepts.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Concepts
|
||||||
|
|
||||||
|
Fence combines two ideas:
|
||||||
|
|
||||||
|
1. **An OS sandbox** to enforce "no direct network" and restrict filesystem operations.
|
||||||
|
2. **Local filtering proxies** (HTTP + SOCKS5) to selectively allow outbound traffic by domain.
|
||||||
|
|
||||||
|
## Network model
|
||||||
|
|
||||||
|
By default, fence blocks all outbound network access.
|
||||||
|
|
||||||
|
When you allow domains, fence:
|
||||||
|
|
||||||
|
- Starts local HTTP and SOCKS5 proxies
|
||||||
|
- Sets proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`)
|
||||||
|
- Allows the sandboxed process to connect only to the local proxies
|
||||||
|
- Filters outbound connections by **destination domain**
|
||||||
|
|
||||||
|
### Localhost controls
|
||||||
|
|
||||||
|
- `allowLocalBinding`: lets a sandboxed process *listen* on local ports (e.g. dev servers).
|
||||||
|
- `allowLocalOutbound`: lets a sandboxed process connect to `localhost` services (e.g. Redis/Postgres on your machine).
|
||||||
|
- `-p/--port`: exposes inbound ports so things outside the sandbox can reach your server.
|
||||||
|
|
||||||
|
These are separate on purpose. A typical safe default for dev servers is:
|
||||||
|
|
||||||
|
- allow binding + expose just the needed port(s)
|
||||||
|
- disallow localhost outbound unless you explicitly need it
|
||||||
|
|
||||||
|
## Filesystem model
|
||||||
|
|
||||||
|
Fence is designed around "read mostly, write narrowly":
|
||||||
|
|
||||||
|
- **Reads**: allowed by default (you can block specific paths via `denyRead`).
|
||||||
|
- **Writes**: denied by default (you must opt-in with `allowWrite`).
|
||||||
|
- **denyWrite**: overrides `allowWrite` (useful for protecting secrets and dangerous files).
|
||||||
|
|
||||||
|
Fence also protects some dangerous targets regardless of config (e.g. shell startup files and git hooks). See `ARCHITECTURE.md` for the full list.
|
||||||
|
|
||||||
|
## Debug vs Monitor mode
|
||||||
|
|
||||||
|
- `-d/--debug`: verbose output (proxy activity, filter decisions, sandbox command details).
|
||||||
|
- `-m/--monitor`: show blocked requests/violations only (great for auditing and policy tuning).
|
||||||
|
|
||||||
|
Workflow tip:
|
||||||
|
|
||||||
|
1. Start restrictive.
|
||||||
|
2. Run with `-m` to see what gets blocked.
|
||||||
|
3. Add the minimum domains/paths required.
|
||||||
|
|
||||||
|
## Platform notes
|
||||||
|
|
||||||
|
- **macOS**: uses `sandbox-exec` with generated Seatbelt profiles.
|
||||||
|
- **Linux**: uses `bubblewrap` for namespaces + `socat` bridges to connect the isolated network namespace to host-side proxies.
|
||||||
|
|
||||||
|
If you want the under-the-hood view, see [Architecture](../ARCHITECTURE.md).
|
||||||
52
docs/configuration.md
Normal file
52
docs/configuration.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
Fence reads settings from `~/.fence.json` by default (or pass `--settings ./fence.json`).
|
||||||
|
|
||||||
|
Example config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["github.com", "*.npmjs.org", "registry.yarnpkg.com"],
|
||||||
|
"deniedDomains": ["evil.com"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"denyRead": ["/etc/passwd"],
|
||||||
|
"allowWrite": [".", "/tmp"],
|
||||||
|
"denyWrite": [".git/hooks"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Configuration
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `allowedDomains` | List of allowed domains. Supports wildcards like `*.example.com` |
|
||||||
|
| `deniedDomains` | List of denied domains (checked before allowed) |
|
||||||
|
| `allowUnixSockets` | List of allowed Unix socket paths (macOS) |
|
||||||
|
| `allowAllUnixSockets` | Allow all Unix sockets |
|
||||||
|
| `allowLocalBinding` | Allow binding to local ports |
|
||||||
|
| `allowLocalOutbound` | Allow outbound connections to localhost, e.g., local DBs (defaults to `allowLocalBinding` if not set) |
|
||||||
|
| `httpProxyPort` | Fixed port for HTTP proxy (default: random available port) |
|
||||||
|
| `socksProxyPort` | Fixed port for SOCKS5 proxy (default: random available port) |
|
||||||
|
|
||||||
|
## Filesystem Configuration
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `denyRead` | Paths to deny reading (deny-only pattern) |
|
||||||
|
| `allowWrite` | Paths to allow writing |
|
||||||
|
| `denyWrite` | Paths to deny writing (takes precedence) |
|
||||||
|
| `allowGitConfig` | Allow writes to `.git/config` files |
|
||||||
|
|
||||||
|
## Other Options
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `allowPty` | Allow pseudo-terminal (PTY) allocation in the sandbox (for MacOS) |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- Config templates: [`docs/templates/`](docs/templates/)
|
||||||
|
- Workflow guides: [`docs/recipes/`](docs/recipes/)
|
||||||
128
docs/quickstart.md
Normal file
128
docs/quickstart.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Quickstart
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From Source (recommended for now)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Use-Tusk/fence
|
||||||
|
cd fence
|
||||||
|
go build -o fence ./cmd/fence
|
||||||
|
sudo mv fence /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Go Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/Use-Tusk/fence/cmd/fence@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux Dependencies
|
||||||
|
|
||||||
|
On Linux, you also need:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt install bubblewrap socat
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install bubblewrap socat
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S bubblewrap socat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your First Sandboxed Command
|
||||||
|
|
||||||
|
By default, fence blocks all network access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# This will fail - network is blocked
|
||||||
|
fence curl https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see something like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
curl: (56) CONNECT tunnel failed, response 403
|
||||||
|
```
|
||||||
|
|
||||||
|
## Allow Specific Domains
|
||||||
|
|
||||||
|
Create a config file at `~/.fence.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["example.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now try again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence curl https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This time it succeeds!
|
||||||
|
|
||||||
|
## Debug Mode
|
||||||
|
|
||||||
|
Use `-d` to see what's happening under the hood:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -d curl https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This shows:
|
||||||
|
|
||||||
|
- The sandbox command being run
|
||||||
|
- Proxy activity (allowed/blocked requests)
|
||||||
|
- Filter rule matches
|
||||||
|
|
||||||
|
## Monitor Mode
|
||||||
|
|
||||||
|
Use `-m` to see only violations and blocked requests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -m npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- Auditing what a command tries to access
|
||||||
|
- Debugging why something isn't working
|
||||||
|
- Understanding a package's network behavior
|
||||||
|
|
||||||
|
## Running Shell Commands
|
||||||
|
|
||||||
|
Use `-c` to run compound commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -c "echo hello && ls -la"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expose Ports for Servers
|
||||||
|
|
||||||
|
If you're running a server that needs to accept connections:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -p 3000 -c "npm run dev"
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows external connections to port 3000 while keeping outbound network restricted.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- Read **[Why Fence](why-fence.md)** to understand when fence is a good fit (and when it isn't).
|
||||||
|
- Learn the mental model in **[Concepts](concepts.md)**.
|
||||||
|
- Use **[Troubleshooting](troubleshooting.md)** if something is blocked unexpectedly.
|
||||||
|
- Start from copy/paste configs in **[`docs/templates/`](templates/README.md)**.
|
||||||
|
- Follow workflow-specific guides in **[Recipes](recipes/README.md)** (npm/pip/git/CI).
|
||||||
20
docs/recipes/README.md
Normal file
20
docs/recipes/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Recipes
|
||||||
|
|
||||||
|
These are "cookbook" guides for common workflows. Most follow the same loop:
|
||||||
|
|
||||||
|
1. Start with a restrictive config
|
||||||
|
2. Run with monitor mode (`-m`) to see what gets blocked
|
||||||
|
3. Allow the minimum domains/paths needed
|
||||||
|
|
||||||
|
## Package installs
|
||||||
|
|
||||||
|
- [npm install](npm-install.md)
|
||||||
|
- [pip / poetry](pip-poetry.md)
|
||||||
|
|
||||||
|
## Git / fetching code
|
||||||
|
|
||||||
|
- [git clone / git fetch](git-clone.md)
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
- [CI jobs](ci.md)
|
||||||
36
docs/recipes/ci.md
Normal file
36
docs/recipes/ci.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Recipe: CI jobs
|
||||||
|
|
||||||
|
Goal: make CI steps safer by default: minimal egress and controlled writes.
|
||||||
|
|
||||||
|
## Suggested baseline
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": []
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json -c "make test"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add only what you need
|
||||||
|
|
||||||
|
Use monitor mode to discover what a job tries to reach:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -m --settings ./fence.json -c "make test"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then allowlist only:
|
||||||
|
|
||||||
|
- your artifact/cache endpoints
|
||||||
|
- the minimum package registries required
|
||||||
|
- any internal services the job must access
|
||||||
32
docs/recipes/git-clone.md
Normal file
32
docs/recipes/git-clone.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Recipe: `git clone` / `git fetch`
|
||||||
|
|
||||||
|
Goal: allow fetching code from a limited set of hosts.
|
||||||
|
|
||||||
|
## HTTPS clone (GitHub example)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["github.com", "api.github.com", "codeload.github.com"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["."]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json git clone https://github.com/OWNER/REPO.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH clone
|
||||||
|
|
||||||
|
SSH traffic may go through SOCKS5 (`ALL_PROXY`) depending on your git/ssh configuration.
|
||||||
|
|
||||||
|
If it fails, use monitor/debug mode to see what was blocked:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -m --settings ./fence.json git clone git@github.com:OWNER/REPO.git
|
||||||
|
```
|
||||||
37
docs/recipes/npm-install.md
Normal file
37
docs/recipes/npm-install.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Recipe: `npm install`
|
||||||
|
|
||||||
|
Goal: allow npm to fetch packages, but block unexpected egress.
|
||||||
|
|
||||||
|
## Start restrictive
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["registry.npmjs.org", "*.npmjs.org"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "node_modules", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Iterate with monitor mode
|
||||||
|
|
||||||
|
If installs fail, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -m --settings ./fence.json npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the minimum extra domains required for your workflow (private registries, GitHub tarballs, etc.).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- If your dependencies fetch binaries during install, you may need to allow additional domains.
|
||||||
|
- Keep allowlists narrow; prefer specific hostnames over broad wildcards.
|
||||||
36
docs/recipes/pip-poetry.md
Normal file
36
docs/recipes/pip-poetry.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Recipe: `pip` / `poetry`
|
||||||
|
|
||||||
|
Goal: allow Python dependency fetching while keeping egress minimal.
|
||||||
|
|
||||||
|
## Start restrictive (PyPI)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["pypi.org", "files.pythonhosted.org"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
For Poetry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./fence.json poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Iterate with monitor mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -m --settings ./fence.json poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use private indexes, add those domains explicitly.
|
||||||
75
docs/security-model.md
Normal file
75
docs/security-model.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Security Model
|
||||||
|
|
||||||
|
Fence is intended as **defense-in-depth** for running semi-trusted commands with reduced side effects (package installs, build scripts, CI jobs, unfamiliar repos).
|
||||||
|
|
||||||
|
It is **not** designed to be a strong isolation boundary against actively malicious code that is attempting to escape.
|
||||||
|
|
||||||
|
## Threat model (what Fence helps with)
|
||||||
|
|
||||||
|
Fence is useful when you want to reduce risk from:
|
||||||
|
|
||||||
|
- Supply-chain scripts that unexpectedly call out to the network
|
||||||
|
- Tools that write broadly across your filesystem
|
||||||
|
- Accidental leakage of secrets via "phone home" behavior
|
||||||
|
- Unfamiliar repos that run surprising commands during install/build/test
|
||||||
|
|
||||||
|
## What Fence enforces
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
- **Default deny**: outbound network is blocked unless explicitly allowed.
|
||||||
|
- **Allowlisting by domain**: you can specify `allowedDomains` (with wildcard support like `*.example.com`).
|
||||||
|
- **Localhost controls**: inbound binding and localhost outbound are separately controlled.
|
||||||
|
|
||||||
|
Important: domain filtering does **not** inspect content. If you allow a domain, code can exfiltrate via that domain.
|
||||||
|
|
||||||
|
#### How allowlisting works (important nuance)
|
||||||
|
|
||||||
|
Fence combines **OS-level enforcement** with **proxy-based allowlisting**:
|
||||||
|
|
||||||
|
- The OS sandbox / network namespace is expected to block **direct outbound** connections.
|
||||||
|
- Domain allowlisting happens via local HTTP/SOCKS proxies and proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`).
|
||||||
|
|
||||||
|
If a program does not use proxy env vars (or uses a custom protocol/stack), it may **not benefit from domain allowlisting**. In that case it typically fails with connection errors rather than being "selectively allowed."
|
||||||
|
|
||||||
|
Localhost is separate from "external domains":
|
||||||
|
|
||||||
|
- `allowLocalOutbound=false` can intentionally block connections to local services like Redis on `127.0.0.1:6379` (see the dev-server example).
|
||||||
|
|
||||||
|
### Filesystem
|
||||||
|
|
||||||
|
- **Writes are denied by default**; you must opt in with `allowWrite`.
|
||||||
|
- **denyWrite** can block specific files/patterns even if the parent directory is writable.
|
||||||
|
- **denyRead** can block reads from sensitive paths.
|
||||||
|
- Fence includes an internal list of always-protected targets (e.g. shell configs, git hooks) to reduce common persistence vectors.
|
||||||
|
|
||||||
|
## Visibility / auditing
|
||||||
|
|
||||||
|
- `-m/--monitor` helps you discover what a command *tries* to access (blocked only).
|
||||||
|
- `-d/--debug` shows more detail to understand why something was blocked.
|
||||||
|
|
||||||
|
## Limitations (what Fence does NOT try to solve)
|
||||||
|
|
||||||
|
- **Hostile code containment**: assume determined attackers may escape via kernel/OS vulnerabilities.
|
||||||
|
- **Resource limits**: CPU, memory, disk, fork bombs, etc. are out of scope.
|
||||||
|
- **Content-based controls**: Fence does not block data exfiltration to *allowed* destinations.
|
||||||
|
- **Proxy limitations / protocol edge cases**: some programs may not respect proxy environment variables, so they won't get domain allowlisting unless you configure them to use a proxy (e.g. Node.js `http`/`https` without a proxy-aware client).
|
||||||
|
|
||||||
|
### Practical examples of proxy limitations
|
||||||
|
|
||||||
|
The proxy approach works well for many tools (curl, wget, git, npm, pip), but **not by default** for some stacks:
|
||||||
|
|
||||||
|
- Node.js native `http`/`https` (use a proxy-aware client, e.g. `undici` + `ProxyAgent`)
|
||||||
|
- Raw socket connections (custom TCP/UDP protocols)
|
||||||
|
|
||||||
|
Fence's OS-level sandbox is still expected to block direct outbound connections; bypassing the proxy should fail rather than silently succeeding.
|
||||||
|
|
||||||
|
### Domain-based filtering only
|
||||||
|
|
||||||
|
Fence does not inspect request content. If you allow a domain, a sandboxed process can still exfiltrate data to that domain.
|
||||||
|
|
||||||
|
### Not a hostile-code containment boundary
|
||||||
|
|
||||||
|
Fence is defense-in-depth for running semi-trusted code, not a strong isolation boundary against malware designed to escape sandboxes.
|
||||||
|
|
||||||
|
For implementation details (how proxies/sandboxes/bridges work), see [`ARCHITECTURE.md`](../ARCHITECTURE.md).
|
||||||
18
docs/templates/README.md
vendored
Normal file
18
docs/templates/README.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Config Templates
|
||||||
|
|
||||||
|
This directory contains Fence config templates. They are small and meant to be copied and customized.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
- `default-deny.json`: no network allowlist; no write access (most restrictive)
|
||||||
|
- `workspace-write.json`: allow writes in the current directory
|
||||||
|
- `npm-install.json`: allow npm registry; allow writes to workspace/node_modules/tmp
|
||||||
|
- `pip-install.json`: allow PyPI; allow writes to workspace/tmp
|
||||||
|
- `local-dev-server.json`: allow binding and localhost outbound; allow writes to workspace/tmp
|
||||||
|
- `agent-api-only.json`: allow common LLM API domains; allow writes to workspace
|
||||||
|
|
||||||
|
## Using a template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings ./docs/templates/npm-install.json npm install
|
||||||
|
```
|
||||||
8
docs/templates/agent-api-only.json
vendored
Normal file
8
docs/templates/agent-api-only.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["api.openai.com", "api.anthropic.com"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["."]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
docs/templates/default-deny.json
vendored
Normal file
8
docs/templates/default-deny.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": []
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": []
|
||||||
|
}
|
||||||
|
}
|
||||||
9
docs/templates/local-dev-server.json
vendored
Normal file
9
docs/templates/local-dev-server.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowLocalBinding": true,
|
||||||
|
"allowLocalOutbound": true
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
docs/templates/npm-install.json
vendored
Normal file
8
docs/templates/npm-install.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["registry.npmjs.org", "*.npmjs.org"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "node_modules", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
docs/templates/pip-install.json
vendored
Normal file
8
docs/templates/pip-install.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": ["pypi.org", "files.pythonhosted.org"]
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
docs/templates/workspace-write.json
vendored
Normal file
5
docs/templates/workspace-write.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["."]
|
||||||
|
}
|
||||||
|
}
|
||||||
79
docs/troubleshooting.md
Normal file
79
docs/troubleshooting.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## "curl: (56) CONNECT tunnel failed, response 403"
|
||||||
|
|
||||||
|
This usually means:
|
||||||
|
|
||||||
|
- the process tried to reach a domain that is **not allowed**, and
|
||||||
|
- the request went through fence's HTTP proxy, which returned `403`.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
|
||||||
|
- Run with monitor mode to see what was blocked:
|
||||||
|
- `fence -m <command>`
|
||||||
|
- Add the required destination(s) to `network.allowedDomains`.
|
||||||
|
|
||||||
|
## "It works outside fence but not inside"
|
||||||
|
|
||||||
|
Start with:
|
||||||
|
|
||||||
|
- `fence -m <command>` to see what's being denied
|
||||||
|
- `fence -d <command>` to see full proxy and sandbox detail
|
||||||
|
|
||||||
|
Common causes:
|
||||||
|
|
||||||
|
- Missing `allowedDomains`
|
||||||
|
- A tool attempting direct sockets that don't respect proxy environment variables
|
||||||
|
- Localhost outbound blocked (DB/cache on `127.0.0.1`)
|
||||||
|
- Writes blocked (you didn't include a directory in `filesystem.allowWrite`)
|
||||||
|
|
||||||
|
## Node.js HTTP(S) doesn't use proxy env vars by default
|
||||||
|
|
||||||
|
Node's built-in `http`/`https` modules ignore `HTTP_PROXY`/`HTTPS_PROXY`.
|
||||||
|
|
||||||
|
If your Node code makes outbound HTTP(S) requests, use a proxy-aware client.
|
||||||
|
For example with `undici`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ProxyAgent, fetch } from "undici";
|
||||||
|
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
dispatcher: new ProxyAgent(proxyUrl),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Fence's OS-level sandbox should still block direct connections; the above makes your requests go through the filtering proxy so allowlisting works as intended.
|
||||||
|
|
||||||
|
## Local services (Redis/Postgres/etc.) fail inside the sandbox
|
||||||
|
|
||||||
|
If your process needs to connect to `localhost` services, set:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"network": { "allowLocalOutbound": true }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're running a server inside the sandbox that must accept connections:
|
||||||
|
|
||||||
|
- set `network.allowLocalBinding: true` (to bind)
|
||||||
|
- use `-p <port>` (to expose inbound port(s))
|
||||||
|
|
||||||
|
## "Permission denied" on file writes
|
||||||
|
|
||||||
|
Writes are denied by default.
|
||||||
|
|
||||||
|
- Add the minimum required writable directories to `filesystem.allowWrite`.
|
||||||
|
- Protect sensitive targets with `filesystem.denyWrite` (and note fence protects some targets regardless).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"],
|
||||||
|
"denyWrite": [".env", "*.key"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
40
docs/why-fence.md
Normal file
40
docs/why-fence.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Why Fence?
|
||||||
|
|
||||||
|
Fence exists to reduce the blast radius of running commands you don't fully trust (or don't fully understand yet).
|
||||||
|
|
||||||
|
Common situations:
|
||||||
|
|
||||||
|
- Running `npm install`, `pip install`, or `cargo build` in an unfamiliar repo
|
||||||
|
- Executing build scripts or test runners that can read/write broadly and make network calls
|
||||||
|
- Running CI jobs where you want **default-deny egress** and **tightly scoped writes**
|
||||||
|
- Auditing what a command *tries* to do before you let it do it
|
||||||
|
|
||||||
|
Fence is intentionally simple: it focuses on **network allowlisting** (by domain) and **filesystem write restrictions** (by path), wrapped in a pragmatic OS sandbox (macOS `sandbox-exec`, Linux `bubblewrap`).
|
||||||
|
|
||||||
|
## What problem does it solve?
|
||||||
|
|
||||||
|
Fence helps you answer: "What can this command touch?"
|
||||||
|
|
||||||
|
- **Network**: block all outbound by default; then allow only the domains you choose.
|
||||||
|
- **Filesystem**: default-deny writes; then allow writes only where you choose (and deny sensitive writes regardless).
|
||||||
|
- **Visibility**: monitor blocked requests/violations (`-m`) to iteratively tighten or expand policy.
|
||||||
|
|
||||||
|
This is especially useful for supply-chain risk and "unknown repo" workflows where you want a safer default than "run it and hope".
|
||||||
|
|
||||||
|
## When Fence is useful even if tools already sandbox
|
||||||
|
|
||||||
|
Some coding agents and platforms ship sandboxing (Seatbelt/Landlock/etc.). Fence still provides value when you want:
|
||||||
|
|
||||||
|
- **Tool-agnostic policy**: apply the same rules to any command, not only inside one agent.
|
||||||
|
- **Standardization**: commit/review a config once, use it across developers and CI.
|
||||||
|
- **Defense-in-depth**: wrap an agent (or its subprocesses) with an additional layer and clearer audit signals.
|
||||||
|
- **Practical allowlisting**: start with default-deny egress and use `-m` to discover what domains a workflow actually needs.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
Fence is **not** a hardened containment boundary for actively malicious code.
|
||||||
|
|
||||||
|
- It does **not** attempt to prevent resource exhaustion (CPU/RAM/disk), timing attacks, or kernel-level escapes.
|
||||||
|
- Domain allowlisting is not content inspection: if you allow a domain, code can exfiltrate via that domain.
|
||||||
|
|
||||||
|
For details, see [Security Model](security-model.md).
|
||||||
23
examples/.gitignore
vendored
Normal file
23
examples/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
build-output/
|
||||||
|
dist/
|
||||||
|
generated/
|
||||||
|
|
||||||
|
# Lock files (we want fresh installs for demos)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Python
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
89
examples/01-dev-server/README.md
Normal file
89
examples/01-dev-server/README.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Dev Server + Redis Demo
|
||||||
|
|
||||||
|
This demo shows how fence controls network access: allowing specific external domains while blocking (or allowing) localhost connections.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You need Redis running on localhost:6379:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 6379:6379 redis:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo 1: Localhost allowed, external blocked
|
||||||
|
|
||||||
|
This shows that requests to Redis (local service) works, but external requests are blocked.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -p 3000 --settings fence-external-blocked.json npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Works - localhost outbound to Redis allowed
|
||||||
|
curl http://localhost:3000/api/users
|
||||||
|
|
||||||
|
# Blocked - no domains whitelisted for external requests
|
||||||
|
curl http://localhost:3000/api/external
|
||||||
|
```
|
||||||
|
|
||||||
|
## Demo 2: External Allowed, Localhost Blocked
|
||||||
|
|
||||||
|
This shows the opposite: whitelisted external domains work, but Redis (localhost) is blocked.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence -p 3000 --settings fence-external-only.json npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
You will immediately notice that Redis connection is blocked on app startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[app] Redis connection failed: connect EPERM 127.0.0.1:6379 - Local (0.0.0.0:0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Works - httpbin.org is in the allowlist
|
||||||
|
curl http://localhost:3000/api/external
|
||||||
|
|
||||||
|
# Blocked - localhost outbound to Redis not allowed
|
||||||
|
curl http://localhost:3000/api/users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Config | Redis (localhost) | External (httpbin.org) |
|
||||||
|
|--------|-------------------|------------------------|
|
||||||
|
| `fence-external-blocked.json` | ✓ Allowed | ✗ Blocked |
|
||||||
|
| `fence-external-only.json` | ✗ Blocked | ✓ Allowed |
|
||||||
|
|
||||||
|
## Key Settings
|
||||||
|
|
||||||
|
| Setting | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `allowLocalBinding` | Server can listen on ports |
|
||||||
|
| `allowLocalOutbound` | App can connect to localhost services |
|
||||||
|
| `allowedDomains` | Whitelist of external domains |
|
||||||
|
|
||||||
|
## Note: Node.js Proxy Support
|
||||||
|
|
||||||
|
Node.js's native `http`/`https` modules don't respect proxy environment variables. This demo uses [`undici`](https://github.com/nodejs/undici) with `ProxyAgent` to route requests through fence's proxy:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ProxyAgent, fetch } from "undici";
|
||||||
|
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
dispatcher: new ProxyAgent(proxyUrl),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Without this, external HTTP requests would fail with connection errors (the sandbox blocks them) rather than going through fence's proxy.
|
||||||
200
examples/01-dev-server/app.js
Normal file
200
examples/01-dev-server/app.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* Demo Express app that:
|
||||||
|
* 1. Serves an API on port 3000
|
||||||
|
* 2. Connects to Redis on localhost:6379
|
||||||
|
* 3. Attempts to call external APIs (blocked by fence)
|
||||||
|
*
|
||||||
|
* This demonstrates allowLocalOutbound - the app can reach
|
||||||
|
* local services (Redis) but not the external internet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from "express";
|
||||||
|
import Redis from "ioredis";
|
||||||
|
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
// Connect to Redis on localhost
|
||||||
|
const redis = new Redis({
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 6379,
|
||||||
|
connectTimeout: 3000,
|
||||||
|
retryStrategy: () => null, // Don't retry, fail fast for demo
|
||||||
|
});
|
||||||
|
|
||||||
|
let redisConnected = false;
|
||||||
|
|
||||||
|
redis.on("connect", () => {
|
||||||
|
redisConnected = true;
|
||||||
|
console.log("[app] Connected to Redis");
|
||||||
|
|
||||||
|
// Seed some demo data
|
||||||
|
redis.set(
|
||||||
|
"user:1",
|
||||||
|
JSON.stringify({ id: 1, name: "Alice", email: "alice@example.com" })
|
||||||
|
);
|
||||||
|
redis.set(
|
||||||
|
"user:2",
|
||||||
|
JSON.stringify({ id: 2, name: "Bob", email: "bob@example.com" })
|
||||||
|
);
|
||||||
|
redis.set(
|
||||||
|
"user:3",
|
||||||
|
JSON.stringify({ id: 3, name: "Charlie", email: "charlie@example.com" })
|
||||||
|
);
|
||||||
|
console.log("[app] Seeded demo data");
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.on("error", (err) => {
|
||||||
|
if (!redisConnected) {
|
||||||
|
console.log("[app] Redis connection failed:", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper: Make external API call using undici with proxy support
|
||||||
|
// Node.js native https doesn't respect HTTP_PROXY, so we use undici
|
||||||
|
async function fetchExternal(url) {
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use proxy if available (set by fence)
|
||||||
|
if (proxyUrl) {
|
||||||
|
options.dispatcher = new ProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await undiciFetch(url, options);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data: text.slice(0, 200),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: "Dev Server Demo",
|
||||||
|
redis: redisConnected ? "connected" : "disconnected",
|
||||||
|
endpoints: {
|
||||||
|
"/api/users": "List all users from Redis",
|
||||||
|
"/api/users/:id": "Get user by ID from Redis",
|
||||||
|
"/api/health": "Health check",
|
||||||
|
"/api/external": "Try to call external API (blocked by fence)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/users", async (req, res) => {
|
||||||
|
if (!redisConnected) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: "Redis not connected",
|
||||||
|
hint: "Start Redis: docker run -p 6379:6379 redis:alpine",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await redis.keys("user:*");
|
||||||
|
const users = await Promise.all(
|
||||||
|
keys.map(async (key) => JSON.parse(await redis.get(key)))
|
||||||
|
);
|
||||||
|
res.json({
|
||||||
|
source: "redis",
|
||||||
|
count: users.length,
|
||||||
|
data: users,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/users/:id", async (req, res) => {
|
||||||
|
if (!redisConnected) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: "Redis not connected",
|
||||||
|
hint: "Start Redis: docker run -p 6379:6379 redis:alpine",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await redis.get(`user:${req.params.id}`);
|
||||||
|
if (user) {
|
||||||
|
res.json({ source: "redis", data: JSON.parse(user) });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: "User not found" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/health", async (req, res) => {
|
||||||
|
if (!redisConnected) {
|
||||||
|
return res.status(503).json({
|
||||||
|
status: "unhealthy",
|
||||||
|
redis: "disconnected",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redis.ping();
|
||||||
|
res.json({
|
||||||
|
status: "healthy",
|
||||||
|
redis: "connected",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(503).json({
|
||||||
|
status: "unhealthy",
|
||||||
|
redis: "error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/external", async (req, res) => {
|
||||||
|
console.log("[app] Attempting external API call...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetchExternal("https://httpbin.org/get");
|
||||||
|
// Check if we're using a proxy (indicates fence is running)
|
||||||
|
const usingProxy = !!(process.env.HTTPS_PROXY || process.env.HTTP_PROXY);
|
||||||
|
res.json({
|
||||||
|
status: "success",
|
||||||
|
message: usingProxy
|
||||||
|
? "✓ Request allowed (httpbin.org is whitelisted)"
|
||||||
|
: "⚠️ No proxy detected - not running in fence",
|
||||||
|
proxy: usingProxy ? process.env.HTTPS_PROXY : null,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.json({
|
||||||
|
status: "blocked",
|
||||||
|
message: "✓ External call blocked by fence",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startup
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ Dev Server Demo ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ Server: http://localhost:${PORT} ║
|
||||||
|
║ Redis: localhost:6379 ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ Endpoints: ║
|
||||||
|
║ GET / - API info ║
|
||||||
|
║ GET /api/users - List users from Redis ║
|
||||||
|
║ GET /api/users/:id - Get user by ID ║
|
||||||
|
║ GET /api/health - Health check ║
|
||||||
|
║ GET /api/external - Try external call (blocked) ║
|
||||||
|
╚═══════════════════════════════════════════════════════════╝
|
||||||
|
`);
|
||||||
|
});
|
||||||
9
examples/01-dev-server/fence-external-blocked.json
Normal file
9
examples/01-dev-server/fence-external-blocked.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowLocalBinding": true,
|
||||||
|
"allowLocalOutbound": true
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
examples/01-dev-server/fence-external-only.json
Normal file
10
examples/01-dev-server/fence-external-only.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowLocalBinding": true,
|
||||||
|
"allowedDomains": ["httpbin.org"],
|
||||||
|
"allowLocalOutbound": false
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": [".", "/tmp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/01-dev-server/package.json
Normal file
15
examples/01-dev-server/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "dev-server-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Demo: Dev server with Redis in fence sandbox",
|
||||||
|
"type": "module",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
|
"undici": "^6.19.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
examples/02-filesystem/README.md
Normal file
67
examples/02-filesystem/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Filesystem Sandbox Demo
|
||||||
|
|
||||||
|
This demo shows how fence controls filesystem access with `allowWrite`, `denyWrite`, and `denyRead`.
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
| Operation | Without Fence | With Fence |
|
||||||
|
|-----------|---------------|------------|
|
||||||
|
| Write to `./output/` | ✓ | ✓ (in allowWrite) |
|
||||||
|
| Write to `./` | ✓ | ✗ (not in allowWrite) |
|
||||||
|
| Write to `.env` | ✓ | ✗ (in denyWrite) |
|
||||||
|
| Write to `*.key` | ✓ | ✗ (in denyWrite) |
|
||||||
|
| Read `./demo.py` | ✓ | ✓ (allowed by default) |
|
||||||
|
| Read `/etc/shadow` | ✗ | ✗ (in denyRead) |
|
||||||
|
| Read `/etc/passwd` | ✓ | ✗ (in denyRead) |
|
||||||
|
|
||||||
|
## Run the demo
|
||||||
|
|
||||||
|
### Without fence (all writes succeed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### With fence (unauthorized operations blocked)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fence --settings fence.json python demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fence config
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["./output"],
|
||||||
|
"denyWrite": [".env", "*.key"],
|
||||||
|
"denyRead": ["/etc/shadow", "/etc/passwd"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. **allowWrite** - Only paths listed here are writable. Everything else is read-only.
|
||||||
|
|
||||||
|
2. **denyWrite** - These paths are blocked even if they'd otherwise be allowed. Useful for protecting secrets.
|
||||||
|
|
||||||
|
3. **denyRead** - Block reads from sensitive system files.
|
||||||
|
|
||||||
|
## Key settings
|
||||||
|
|
||||||
|
| Setting | Default | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `allowWrite` | `[]` (nothing) | Directories where writes are allowed |
|
||||||
|
| `denyWrite` | `[]` | Paths to block writes (overrides allowWrite) |
|
||||||
|
| `denyRead` | `[]` | Paths to block reads |
|
||||||
|
|
||||||
|
## Protected paths
|
||||||
|
|
||||||
|
Fence also automatically protects certain paths regardless of config:
|
||||||
|
|
||||||
|
- Shell configs: `.bashrc`, `.zshrc`, `.profile`
|
||||||
|
- Git hooks: `.git/hooks/*`
|
||||||
|
- Git config: `.gitconfig`
|
||||||
|
|
||||||
|
See [ARCHITECTURE.md](../../ARCHITECTURE.md) for the full list.
|
||||||
147
examples/02-filesystem/demo.py
Executable file
147
examples/02-filesystem/demo.py
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Filesystem Sandbox Demo
|
||||||
|
|
||||||
|
This script demonstrates fence's filesystem controls:
|
||||||
|
- allowWrite: Only specific directories are writable
|
||||||
|
- denyWrite: Block writes to sensitive files
|
||||||
|
- denyRead: Block reads from sensitive paths
|
||||||
|
|
||||||
|
Run WITHOUT fence to see all operations succeed.
|
||||||
|
Run WITH fence to see unauthorized operations blocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
os.chdir(SCRIPT_DIR)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
|
||||||
|
def log(operation, status, message):
|
||||||
|
icon = "✓" if status == "success" else "✗"
|
||||||
|
print(f"[{icon}] {operation}: {message}")
|
||||||
|
results.append({"operation": operation, "status": status, "message": message})
|
||||||
|
|
||||||
|
|
||||||
|
def try_write(filepath, content, description):
|
||||||
|
"""Attempt to write to a file."""
|
||||||
|
try:
|
||||||
|
path = Path(filepath)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content)
|
||||||
|
log(description, "success", f"Wrote to {filepath}")
|
||||||
|
return True
|
||||||
|
except PermissionError:
|
||||||
|
log(description, "blocked", f"Permission denied: {filepath}")
|
||||||
|
return False
|
||||||
|
except OSError as e:
|
||||||
|
log(description, "blocked", f"OS error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def try_read(filepath, description):
|
||||||
|
"""Attempt to read from a file."""
|
||||||
|
try:
|
||||||
|
path = Path(filepath)
|
||||||
|
content = path.read_text()
|
||||||
|
log(description, "success", f"Read {len(content)} bytes from {filepath}")
|
||||||
|
return True
|
||||||
|
except PermissionError:
|
||||||
|
log(description, "blocked", f"Permission denied: {filepath}")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
log(description, "skipped", f"File not found: {filepath}")
|
||||||
|
return False
|
||||||
|
except OSError as e:
|
||||||
|
log(description, "blocked", f"OS error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
"""Clean up test files."""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(SCRIPT_DIR / "output", ignore_errors=True)
|
||||||
|
(SCRIPT_DIR / "unauthorized.txt").unlink(missing_ok=True)
|
||||||
|
(SCRIPT_DIR / ".env").unlink(missing_ok=True)
|
||||||
|
(SCRIPT_DIR / "secrets.key").unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ Filesystem Sandbox Demo ║
|
||||||
|
╠═══════════════════════════════════════════════════════════╣
|
||||||
|
║ Tests fence's filesystem controls: ║
|
||||||
|
║ - allowWrite: Only ./output/ is writable ║
|
||||||
|
║ - denyWrite: .env and *.key files are protected ║
|
||||||
|
║ - denyRead: /etc/shadow is blocked ║
|
||||||
|
╚═══════════════════════════════════════════════════════════╝
|
||||||
|
""")
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
print("--- WRITE TESTS ---\n")
|
||||||
|
|
||||||
|
# Test 1: Write to allowed directory (should succeed)
|
||||||
|
try_write(
|
||||||
|
"output/data.txt",
|
||||||
|
"This file is in the allowed output directory.\n",
|
||||||
|
"Write to ./output/ (allowed)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 2: Write to project root (should fail with fence)
|
||||||
|
try_write(
|
||||||
|
"unauthorized.txt",
|
||||||
|
"This should not be writable.\n",
|
||||||
|
"Write to ./ (not in allowWrite)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 3: Write to .env file (should fail - denyWrite)
|
||||||
|
try_write(".env", "SECRET_KEY=stolen\n", "Write to .env (in denyWrite)")
|
||||||
|
|
||||||
|
# Test 4: Write to .key file (should fail - denyWrite pattern)
|
||||||
|
try_write(
|
||||||
|
"secrets.key", "-----BEGIN PRIVATE KEY-----\n", "Write to *.key (in denyWrite)"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n--- READ TESTS ---\n")
|
||||||
|
|
||||||
|
# Test 5: Read from allowed file (should succeed)
|
||||||
|
try_read("demo.py", "Read ./demo.py (allowed)")
|
||||||
|
|
||||||
|
# Test 6: Read from /etc/shadow (should fail - denyRead)
|
||||||
|
try_read("/etc/shadow", "Read /etc/shadow (in denyRead)")
|
||||||
|
|
||||||
|
# Test 7: Read from /etc/passwd (should fail if in denyRead)
|
||||||
|
try_read("/etc/passwd", "Read /etc/passwd (in denyRead)")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n--- SUMMARY ---\n")
|
||||||
|
|
||||||
|
blocked = sum(1 for r in results if r["status"] == "blocked")
|
||||||
|
succeeded = sum(1 for r in results if r["status"] == "success")
|
||||||
|
skipped = sum(1 for r in results if r["status"] == "skipped")
|
||||||
|
|
||||||
|
if skipped > 0:
|
||||||
|
print(f"({skipped} test(s) skipped - file not found)")
|
||||||
|
|
||||||
|
if blocked > 0:
|
||||||
|
print(f"✅ Fence blocked {blocked} unauthorized operation(s)")
|
||||||
|
print(f"{succeeded} allowed operation(s) succeeded")
|
||||||
|
print("\nFilesystem sandbox is working!\n")
|
||||||
|
else:
|
||||||
|
print("⚠️ All operations succeeded - you are likely not running in fence")
|
||||||
|
print("Run with: fence --settings fence.json python demo.py\n")
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
examples/02-filesystem/fence.json
Normal file
10
examples/02-filesystem/fence.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"allowedDomains": []
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"allowWrite": ["./output"],
|
||||||
|
"denyWrite": [".env", "*.key"],
|
||||||
|
"denyRead": ["/etc/shadow", "/etc/passwd"]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/README.md
Normal file
15
examples/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Fence Examples
|
||||||
|
|
||||||
|
Runnable examples demonstrating `fence` capabilities.
|
||||||
|
|
||||||
|
If you're looking for copy/paste configs and "cookbook" workflows, also see:
|
||||||
|
|
||||||
|
- Config templates: [`docs/templates/`](../docs/templates/)
|
||||||
|
- Recipes for common workflows: [`docs/recipes/`](../docs/recipes/)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
| Example | What it demonstrates | How to run |
|
||||||
|
|--------|-----------------------|------------|
|
||||||
|
| **[01-dev-server](01-dev-server/README.md)** | Running a dev server in the sandbox, controlling **external domains** vs **localhost outbound** (Redis), and exposing an inbound port (`-p`) | `cd examples/01-dev-server && fence -p 3000 --settings fence-external-blocked.json npm start` |
|
||||||
|
| **[02-filesystem](02-filesystem/README.md)** | Filesystem controls: `allowWrite`, `denyWrite`, `denyRead` | `cd examples/02-filesystem && fence --settings fence.json python demo.py` |
|
||||||
Reference in New Issue
Block a user