Skip to content

Latest commit

 

History

History
854 lines (613 loc) · 31 KB

File metadata and controls

854 lines (613 loc) · 31 KB

Proxy Mode

Network isolation, traffic inspection, and HTTP filtering.

Proxy mode routes all HTTP/HTTPS traffic through a local MITM (Man-in-the-Middle) proxy for inspection and logging. The sections below describe bwrap backend behavior by default. The Docker backend achieves the same goal with different mechanisms - see Backend-Specific Behavior for details.

Why Use Proxy Mode?

  • Audit AI agent activity - See exactly what API calls your AI coding assistant makes
  • Debug network issues - Inspect request/response headers and bodies
  • Security monitoring - Detect unexpected network connections
  • Compliance - Log all external communications for review

When to Enable Proxy Mode

Enable proxy when Skip proxy when
You want to monitor AI agent network activity You trust the code and don't need traffic visibility
You need credential injection (GitHub tokens) Tools use certificate pinning that breaks MITM
You need port forwarding (requires network isolation) You want the fastest possible startup
You need content redaction (secret scanning) You only need filesystem isolation
You need HTTP filtering (domain whitelist/blacklist)

Requirements (bwrap backend)

Proxy mode on the bwrap backend requires passt/pasta for network namespace creation. This is the only feature that requires passt-basic sandboxing works without it.

devsandbox includes an embedded pasta binary - no system packages required. To use a system-installed pasta instead, set use_embedded = false in configuration.

Docker backend: The Docker backend does NOT require pasta. It uses per-session Docker networks for isolation instead. See Backend-Specific Behavior.

Optionally install the system package as a fallback:

# Arch Linux
sudo pacman -S passt

# Debian/Ubuntu
sudo apt install passt

# Fedora
sudo dnf install passt

Verify installation:

devsandbox doctor

The doctor output shows whether each binary is embedded or system-installed.

Enabling Proxy Mode

Command Line

# Enable proxy mode for this session
devsandbox --proxy

# With custom port
devsandbox --proxy --proxy-port 9090

# Run a command with proxy
devsandbox --proxy npm install

Configuration File

Enable proxy mode by default in ~/.config/devsandbox/config.toml:

[proxy]
enabled = true
port = 8080

Transparent Proxy Mode (No MITM)

By default, the proxy performs MITM (Man-in-the-Middle) interception on HTTPS connections, using a generated CA certificate. This enables full traffic inspection, credential injection, and content redaction for HTTPS.

If you don't need HTTPS inspection - for example, when tools have certificate pinning or you only need network isolation with HTTP logging — you can disable MITM:

Command Line

devsandbox --proxy --no-mitm

Configuration File

[proxy]
enabled = true
mitm = false

What Changes

Feature MITM enabled (default) MITM disabled
HTTP filtering/logging Full Full
HTTPS body/header inspection Full None
HTTPS credential injection Works Does not work
HTTPS content redaction Works Does not work
HTTPS request logging Full request/response CONNECT hostname only
CA certificate injection Yes Skipped
Network isolation Yes Yes

When MITM is disabled, the proxy logs warnings at startup if credential injectors, redaction rules, or filter rules are configured - since these features cannot inspect encrypted HTTPS traffic.

When to disable MITM:

  • Tools with certificate pinning that reject the proxy CA
  • You only need network isolation and HTTP (not HTTPS) logging
  • You don't need credential injection, content redaction, or HTTPS filtering

If you're running AI coding assistants (Claude Code, aider, etc.), keep MITM enabled - it's required for credential injection and secret scanning.

Backend-Specific Behavior

The proxy achieves the same goal on both backends - intercept and log HTTP/HTTPS traffic — but the underlying mechanisms differ.

bwrap backend

  • Network isolation: pasta creates a new network namespace with its own network stack
  • Gateway address: Traffic is routed through 10.0.2.2 (pasta virtual gateway)
  • CA certificate path (inside sandbox): /tmp/devsandbox-ca.crt
  • Requirement: passt/pasta must be installed

Docker backend

  • Network isolation: A per-session Docker network is created (no pasta required)
  • Gateway address: host.docker.internal (Docker's built-in host access)
  • CA certificate path (inside container): /etc/ssl/certs/devsandbox-ca.crt
  • Requirement: Docker daemon running (no additional dependencies)
Aspect bwrap Docker
Network isolation pasta namespace Per-session Docker network
Gateway IP 10.0.2.2 host.docker.internal
CA cert location /tmp/devsandbox-ca.crt /etc/ssl/certs/devsandbox-ca.crt
Extra dependency passt/pasta None (Docker only)

How It Works (bwrap)

  1. Network Isolation - pasta creates a new network namespace with its own network stack
  2. Gateway Setup - Traffic is routed through a virtual gateway (10.0.2.2)
  3. Proxy Server - A local HTTP/HTTPS proxy runs on the host
  4. Traffic Enforcement - All HTTP(S) traffic must go through the proxy
  5. TLS Interception - A generated CA certificate enables HTTPS inspection

Network Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Host System                          │
│  ┌─────────────────┐                                        │
│  │  Proxy Server   │◄─────┐                                 │
│  │  127.0.0.1:8080 │      │                                 │
│  └────────┬────────┘      │                                 │
│           │               │                                 │
│           ▼               │                                 │
│      Internet             │ pasta NAT                       │
│                           │ (10.0.2.2 → 127.0.0.1)          │
├───────────────────────────┼─────────────────────────────────┤
│                           │        Sandbox (netns)          │
│                    ┌──────┴──────┐                          │
│                    │   Gateway   │                          │
│                    │  10.0.2.2   │                          │
│                    └──────▲──────┘                          │
│                           │                                 │
│                    ┌──────┴──────┐                          │
│                    │ Application │                          │
│                    │ HTTP_PROXY= │                          │
│                    │ 10.0.2.2:   │                          │
│                    └─────────────┘                          │
└─────────────────────────────────────────────────────────────┘

CA Certificate

A CA certificate is automatically generated for HTTPS interception and stored at:

~/.local/share/devsandbox/<project>/.ca/ca.crt

Inside the sandbox, the certificate is available at /tmp/devsandbox-ca.crt (bwrap backend) or /etc/ssl/certs/devsandbox-ca.crt (Docker backend) and automatically configured via environment variables:

Variable Purpose
NODE_EXTRA_CA_CERTS Node.js
REQUESTS_CA_BUNDLE Python requests
CURL_CA_BUNDLE curl
GIT_SSL_CAINFO Git HTTPS
SSL_CERT_FILE General SSL/TLS

Tools with Certificate Pinning

Some tools implement certificate pinning and won't work with the MITM proxy:

  • Mobile app backends
  • Some cloud SDKs
  • Security-focused applications

Viewing Logs

Proxy Request Logs

View HTTP/HTTPS traffic captured in proxy mode:

# View all proxy logs for current project
devsandbox logs proxy

# View last 50 requests
devsandbox logs proxy --last 50

# Follow/tail logs in real-time
devsandbox logs proxy -f

Filtering Logs

# Filter by time
devsandbox logs proxy --since 1h          # Last hour
devsandbox logs proxy --since today       # Since midnight
devsandbox logs proxy --since 2024-01-15  # Since specific date
devsandbox logs proxy --until 2024-01-15T12:00:00

# Filter by content
devsandbox logs proxy --url /api          # URL contains "/api"
devsandbox logs proxy --method POST       # Only POST requests
devsandbox logs proxy --status 200        # Specific status code
devsandbox logs proxy --status 400-599    # Status code range
devsandbox logs proxy --status ">=400"    # Comparison
devsandbox logs proxy --errors            # All errors (status >= 400)

# Combine filters
devsandbox logs proxy --method POST --url /api --since 1h

Output Formats

# Table format (default)
devsandbox logs proxy

# Compact one-line format
devsandbox logs proxy --compact

# JSON output (for scripting)
devsandbox logs proxy --json

# Include request/response bodies
devsandbox logs proxy --body

# Show summary statistics
devsandbox logs proxy --stats

# Disable colors (for piping)
devsandbox logs proxy --no-color

Example Output

Table format:

┌──────────┬────────┬────────┬──────────┬────────────────────────┐
│   TIME   │ METHOD │ STATUS │ DURATION │          URL           │
├──────────┼────────┼────────┼──────────┼────────────────────────┤
│ 10:30:05 │ GET    │ 200    │ 150ms    │ https://api.example.com│
│ 10:30:06 │ POST   │ 201    │ 89ms     │ https://api.example.com│
└──────────┴────────┴────────┴──────────┴────────────────────────┘

Compact format:

10:30:05 GET  200 150ms https://api.example.com/users
10:30:06 POST 201  89ms https://api.example.com/orders

Stats output:

Summary:
  Total requests: 150
  Success (2xx):  120 (80.0%)
  Redirect (3xx): 10 (6.7%)
  Client err (4xx): 15 (10.0%)
  Server err (5xx): 5 (3.3%)
  Avg duration: 245ms

Log Storage

Logs are stored as gzip-compressed JSONL files:

~/.local/share/devsandbox/<project>/logs/
├── proxy/
│   ├── requests_20240115_0000.jsonl.gz
│   ├── requests_20240115_0001.jsonl.gz
│   └── ...
└── internal/
    ├── proxy_20240115_0000.log.gz
    └── logging-errors.log

Log Rotation

  • Files rotate when they reach 50MB
  • Maximum 5 files kept per type
  • Older files are automatically pruned

Log Entry Format

Each log entry contains:

{
  "ts": "2024-01-15T10:30:05.123Z",
  "method": "POST",
  "url": "https://api.example.com/users",
  "req_headers": {
    "Content-Type": [
      "application/json"
    ],
    "Authorization": [
      "Bearer ..."
    ]
  },
  "req_body": "eyJ1c2VyIjogImpvaG4ifQ==",
  "status": 201,
  "resp_headers": {
    "Content-Type": [
      "application/json"
    ]
  },
  "resp_body": "eyJpZCI6IDEyM30=",
  "duration_ns": 89000000,
  "error": ""
}

Note: Request/response bodies are base64-encoded.

Internal Logs

View proxy server errors and warnings:

# View all internal logs
devsandbox logs internal

# Filter by log type
devsandbox logs internal --type proxy    # Proxy server logs
devsandbox logs internal --type logging  # Remote logging errors

# Follow internal logs
devsandbox logs internal -f

# Show last N lines
devsandbox logs internal --last 100

Remote Logging

Proxy logs can be forwarded to remote destinations. See Configuration - Remote Logging for setup instructions.

HTTP Filtering

HTTP filtering allows you to control which requests are allowed, blocked, or require user approval.

How It Works

Filtering is enabled by setting default_action which determines what happens to requests that don't match any rule:

Default Action Behavior
block Block unmatched requests (whitelist behavior)
allow Allow unmatched requests (blacklist behavior)
ask Prompt user for each unmatched request

Quick Start

# Whitelist behavior - only allow specific domains, block everything else
devsandbox --proxy --filter-default=block \
  --allow-domain="*.github.com" \
  --allow-domain="api.anthropic.com"

# Blacklist behavior - block specific domains, allow everything else
devsandbox --proxy --filter-default=allow \
  --block-domain="*.tracking.io" \
  --block-domain="ads.example.com"

# Ask mode - interactive approval for unmatched requests
devsandbox --proxy --filter-default=ask

Configuration File

Add filter rules to ~/.config/devsandbox/config.toml:

[proxy.filter]
# Enable filtering with default action for unmatched requests
default_action = "block"  # whitelist behavior
ask_timeout = 30
cache_decisions = true

[[proxy.filter.rules]]
pattern = "*.github.com"
action = "allow"
scope = "host"

[[proxy.filter.rules]]
pattern = "api.anthropic.com"
action = "allow"
scope = "host"

[[proxy.filter.rules]]
pattern = "*.internal.corp"
action = "block"
scope = "host"
reason = "Internal network blocked"

AI Agent Filtering Example

Lock down an AI coding assistant to only communicate with known services:

[proxy.filter]
default_action = "block"

[[proxy.filter.rules]]
pattern = "api.anthropic.com"
action = "allow"
scope = "host"

[[proxy.filter.rules]]
pattern = "*.github.com"
action = "allow"
scope = "host"

[[proxy.filter.rules]]
pattern = "registry.npmjs.org"
action = "allow"
scope = "host"

You can generate filter rules from a "known good" session using devsandbox proxy filter generate (see Generate Filter Rules).

Pattern Types

Default is glob. Patterns containing regex characters (^$|()[]{}\+) are auto-detected as regex.

Type Example Description
glob *.example.com Glob patterns (* and ?) - default
exact api.example.com Exact string match
regex ^api\.(dev|prod)\.com$ Regular expressions

Scopes

Default is host.

Scope Description Example Match
host Request host only - default api.example.com
path Request path only /api/v1/users
url Full URL https://api.example.com/v1/users

Ask Mode

In ask mode, unmatched requests require user approval via a separate monitor terminal. This is particularly useful when running AI agents autonomously -- you can approve or block each network request the agent makes, giving you real-time control over what data leaves your machine.

Step 1: Start the sandbox with ask mode:

devsandbox --proxy --filter-default=ask

The sandbox will display:

Filter: ask mode (default action for unmatched requests)

Run in another terminal to approve/deny requests:
  devsandbox proxy monitor

Requests without response within 30s will be rejected.

Step 2: Open another terminal (in the same project directory) and run the monitor:

devsandbox proxy monitor

The socket path is auto-detected from the current directory's sandbox. You can also specify it explicitly:

devsandbox proxy monitor /path/to/ask.sock

The monitor displays incoming requests:

┌──────────────────────────────────────────────────────────────────┐
│  Request #1                                                      │
├──────────────────────────────────────────────────────────────────┤
│  Method: GET                                                     │
│  Host:   api.example.com                                         │
│  Path:   /v1/users                                               │
├──────────────────────────────────────────────────────────────────┤
│  [A]llow    [B]lock    Allow [S]ession    Block [N]ever         │
└──────────────────────────────────────────────────────────────────┘
Decision:

Keys (instant response, no Enter needed):

  • a - Allow this request
  • b - Block this request
  • s - Allow and remember for session
  • n - Block and remember for session

Timeout: Requests that don't receive a response within 30 seconds are automatically rejected and logged to internal logs as unanswered.

Generate Filter Rules from Logs

Analyze existing proxy logs to generate filter configuration:

# Generate whitelist rules from current project's logs (default: block unmatched)
devsandbox proxy filter generate

# Generate from specific log directory
devsandbox proxy filter generate --from-logs ~/.local/share/devsandbox/myproject/logs/proxy/

# Generate blacklist rules (allow unmatched)
devsandbox proxy filter generate --default-action allow

# Save to file
devsandbox proxy filter generate -o filter-rules.toml

# Only include domains with 5+ requests
devsandbox proxy filter generate --min-requests 5

Show Current Configuration

devsandbox proxy filter show

Filter Logs

Filter decisions are logged with requests:

{
  "ts": "2024-01-15T10:30:05Z",
  "method": "GET",
  "url": "https://blocked.example.com/",
  "status": 403,
  "filter_action": "block",
  "filter_reason": "matched rule: *.blocked.com"
}

Credential Injection

The proxy can inject authentication credentials into requests for specific domains, keeping tokens completely out of the sandbox environment. The sandboxed process never sees the token - it stays on the host side.

How It Works

  1. Intercept - The proxy intercepts outgoing requests from the sandbox.
  2. Match - For each configured credential injector, it checks whether the request host matches the injector's host (exact match or glob).
  3. Inject - If matched, the injector sets the configured header to the rendered value_format with {token} substituted from the resolved source. The existing header is preserved unless overwrite = true.
  4. Isolate - The sandbox process never sees the token. It is read from the host environment and added transparently.

Universal Schema

Every injector is defined by the same set of fields under [proxy.credentials.<name>]:

Field Purpose
enabled Master switch. Injector is inert unless enabled = true.
host Hostname to match. Exact (api.github.com) or glob (*.example.com).
header HTTP header to set on matching requests. Canonicalized at load (authorizationAuthorization).
value_format Template for the header value. {token} is replaced with the resolved source value. Defaults to "{token}".
overwrite When true, replaces any existing value for the configured header. Default false.
preset Optional name of a built-in preset whose defaults are used as the base for this injector.
[...source] sub-table Where the token comes from: env, file, or value.

A custom non-GitHub injector - no Go code required:

[proxy.credentials.gitlab]
enabled = true
host = "gitlab.com"
header = "PRIVATE-TOKEN"
value_format = "{token}"

  [proxy.credentials.gitlab.source]
  env = "GITLAB_TOKEN"

Built-in Presets

Built-in preset names are reserved - using one as the section name (e.g. [proxy.credentials.github]) automatically applies the preset's defaults. User fields override preset defaults; [...source] overrides the preset's default source.

Preset host header value_format Default source
github api.github.com Authorization Bearer {token} env = "GITHUB_TOKEN" (with GH_TOKEN fallback when no explicit source is set)

Minimal GitHub configuration - the preset supplies everything else:

[proxy.credentials.github]
enabled = true

Source Types

Field Description Example
env Read from an environment variable env = "DEVSANDBOX_GITHUB_TOKEN"
file Read from a file (supports ~ expansion, whitespace trimmed) file = "~/.config/devsandbox/github-token"
value Static value in config value = "github_pat_..."

When multiple fields are set, priority is: value > env > file. Set exactly one for clarity.

Specificity Ordering

When more than one configured injector could match the same request host, the most-specific one wins:

  1. Exact host beats any glob.
  2. Among globs, the longer literal portion (len(host) - count('*')) wins.
  3. Ties are broken by injector name in alphabetical order.

So an exact api.github.com injector wins over a *.github.com injector for api.github.com, and the proxy injects exactly one credential per request.

A glob that doesn't match any actual request is not an error - host coverage is enforced lazily at request time, not at config load.

Overwriting Existing Authorization Headers

By default the injector never replaces an existing value for its configured header - the sandboxed tool wins. That's safe, but breaks the pattern where a CLI inside the sandbox needs a token set in its environment to start (e.g. gh CLI refuses to run without GH_TOKEN).

To handle this, set overwrite = true and inject a placeholder env var into the sandbox so the CLI starts:

[sandbox.environment.GH_TOKEN]
value = "placeholder"

[proxy.credentials.github]
enabled = true
overwrite = true

[proxy.credentials.github.source]
env = "GH_RO_TOKEN"   # real read-only token on the host

Export GH_RO_TOKEN on the host only. The sandbox sees GH_TOKEN=placeholder; gh adds Authorization: Bearer placeholder to its requests; the proxy replaces the header with the real token from GH_RO_TOKEN before forwarding to api.github.com.

Security trade-off: the sandbox sees a non-functional placeholder, not the real token - leaking the placeholder is harmless. This preserves the core guarantee: the real credential never enters the sandbox.

AI agent workflow: Credential injection is particularly useful for AI coding assistants like Claude Code that need GitHub API access. The token stays on the host - the AI agent never sees it, but its API requests to github.com are automatically authenticated.

Notes:

  • Credential injection requires proxy mode (--proxy) with MITM enabled (the default).
  • Injectors are only active when explicitly enabled = true and the credential source resolves to a non-empty value. An empty source silently disables the injector - it is not a config error.
  • By default the injector never overwrites an existing value for its configured header. Set overwrite = true to change this.
  • Invalid configuration fails fast at load time: unknown preset, missing host/header when enabled = true, invalid glob, or unreadable source file.

See Configuration: Proxy Credentials for the complete TOML reference.

Content Redaction

Content redaction scans outgoing requests for secrets before they leave your machine. It checks request bodies, headers, and URLs against configured rules.

Actions

Action What happens
Block Request rejected with HTTP 403. Secret never leaves your machine.
Redact Secret replaced with [REDACTED:<rule-name>] in body, headers, and URL. Modified request forwarded to destination.
Log Request forwarded unmodified. Match recorded in proxy logs as a warning.

Quick Start

# Block requests containing your API key
devsandbox --proxy
# ~/.config/devsandbox/config.toml or .devsandbox.toml
[proxy.redaction]
enabled = true
default_action = "block"

[[proxy.redaction.rules]]
name = "api-key"
[proxy.redaction.rules.source]
env = "API_SECRET_KEY"

Any outgoing request containing the value of $API_SECRET_KEY is blocked with HTTP 403.

Source Types

Rules detect secrets using either a source (exact value lookup) or a pattern (regex match).

Field Description Example
env Environment variable on the host env = "API_SECRET_KEY"
file File path (supports ~, whitespace trimmed) file = "~/.secrets/token"
env_file_key Key in project .env file env_file_key = "DB_PASSWORD"
value Static value in config value = "literal-secret"

Choosing an action:

  • Block when the secret must never leave your machine (most secure, may break the tool's request)
  • Redact when the request should proceed but without the secret (destination sees [REDACTED:rule-name])
  • Log when you want visibility without enforcement (monitoring only)

Log Entries

Redaction events appear in proxy logs with additional fields:

{
  "ts": "2026-02-23T10:30:05Z",
  "method": "POST",
  "url": "https://api.example.com/v1/chat",
  "status": 403,
  "redaction_action": "block",
  "redaction_matches": ["api-key"]
}

For the redact action, the logged URL and body contain the replacement placeholders - the original secret never appears in logs.

View redaction events:

devsandbox logs proxy --json | jq 'select(.redaction_action != null)'

Important Behavior

  • Content redaction requires proxy mode (--proxy) with MITM enabled.
  • All source values must resolve at startup. If an environment variable is missing or a file is unreadable, devsandbox exits with an error (fail-closed).
  • Log entries for blocked and redacted requests have secrets replaced - secrets never appear in proxy logs.
  • Redaction rules are always additive when merging configs. The default action uses most-restrictive-wins.
  • Redaction rules must not match values used by credential injectors. If a redaction rule (source or pattern) would match an injected credential, devsandbox exits with an error at startup. This prevents the confusing situation where credential injection adds a token and redaction immediately blocks it.
  • When multiple rules match, the most severe action wins: block > redact > log.

Configuration Reference

See Configuration: Content Redaction for the full TOML reference, pattern rules, and merge behavior.

Skipping Log Entries

Log-skip rules drop matching requests from the proxy log entirely. This is for noise reduction, not for security: matched requests still pass through (filtering, redaction, and credential injection still apply); they simply never appear in logs/proxy/requests.jsonl and are never forwarded to remote log dispatchers (syslog/OTLP).

The intended use case is endpoints you'd otherwise see hundreds of times per session and don't care about — for example, telemetry traffic that an agent sends to your own observability infrastructure, where the data is already captured upstream.

How It Works

Each rule has a pattern and an optional scope (default host) and type (default glob, regex auto-detected on metacharacters). Matching reuses the same engine as filter rules: host matches the request hostname (port stripped), path matches the URL path, url matches the full URL string. Rules are evaluated in order, first match wins. Skip is absolute — a matched entry is never logged, even if the request errored, was blocked by the security filter, or triggered a redaction rule.

Configuration

[[proxy.log_skip.rules]]
pattern = "telemetry.example.com"
# scope defaults to "host", type auto-detects glob

[[proxy.log_skip.rules]]
pattern = "*/v1/traces"
scope = "url"
type = "glob"

[[proxy.log_skip.rules]]
pattern = "/v1/metrics"
scope = "path"
type = "exact"

Interaction With Other Features

  • Filter (allow/block): independent. A request blocked by the filter is still skipped from logs if it also matches a log_skip rule. If you want to keep a record of blocked attempts, do not log-skip the same hosts you block.
  • Redaction: independent. Redaction still scrubs request/response bodies in flight; log-skip just decides whether the (already-redacted) entry gets persisted.
  • Credential injection: independent. Tokens are still injected on outbound requests; log-skip only affects the local log artifact.

Configuration Reference

See Configuration: Log Skip for the full TOML reference.

Troubleshooting

"proxy mode requires pasta"

devsandbox includes an embedded pasta binary. If you see this error, extraction failed and no system package is installed:

# Check doctor for details (shows embedded vs system source)
devsandbox doctor

# Install system package as fallback (see Requirements above)

Requests timing out

  1. Check if the target allows proxy connections
  2. Some services block known proxy IPs
  3. Try accessing the URL directly to verify it's reachable

Certificate errors

  1. Ensure the CA environment variables are set correctly
  2. Some tools require manual CA configuration
  3. Certificate pinning may prevent interception

No logs appearing

  1. Verify proxy mode is enabled: devsandbox --proxy --info
  2. Check the log directory exists
  3. Make HTTP requests (not just TCP connections)

See Also

Back to docs index | Back to README