Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ Or download a pre-built binary from the [releases page](https://github.com/coder
# Allow only requests to github.com (JS)
httpjail --js "r.host === 'github.com'" -- your-app

# Load JS from a file (auto-reloads on file changes)
# Load JS from a file (auto-reloads only in --server mode)
echo "/^api\\.example\\.com$/.test(r.host) && r.method === 'GET'" > rules.js
httpjail --js-file rules.js -- curl https://api.example.com/health
# File changes are detected and reloaded automatically on each request
# In command mode the policy is frozen for this run so the app cannot edit it.
# In --server mode, file changes are detected and reloaded on each request.

# Log requests to a file
httpjail --request-log requests.log --js "true" -- npm install
Expand All @@ -60,9 +61,24 @@ httpjail --server --js "true"
# HTTP_PROXY=http://localhost:8080 HTTPS_PROXY=http://localhost:8443

# Run Docker containers with network isolation (Linux only)
httpjail --js "r.host === 'api.github.com'" --docker-run -- --rm alpine:latest wget -qO- https://api.github.com
sudo httpjail --js "r.host === 'api.github.com'" --docker-run -- --rm alpine:latest wget -qO- https://api.github.com
```

## Security-related defaults

- Structured rule results must explicitly set `allow: true` (or an `allow` byte-limit policy) to permit traffic. An empty object or a result with only `deny_message` denies the request. Stalled `--proc` input or output times out and denies the request; shell evaluator output and processor response lines are limited to 64 KiB.
- `max_tx_bytes` counts request-line, header, body-data, and trailer fields; it is a logical-byte limit, not an exact HTTP wire-byte cap (chunked framing can add bytes).
- `--timeout` terminates the command process group before cleaning up its jail; a process that deliberately leaves that group or a separately daemon-managed process may require additional supervision.
- Linux strong mode requires a non-root invoking user through `sudo`; running directly as root is rejected so a jailed root process cannot edit its own firewall. The payload runs with `no_new_privs` and no supplementary groups. The native strong jail blocks connections to host Unix-domain services via seccomp; local stream socketpairs remain available for in-process IPC, while Unix-socket clients/agents may not work. It does **not** isolate the filesystem or close explicitly inherited descriptors: keep policy files and their parent directories outside payload write access.
- When run as root on Linux, including `--test`, `--sh` and `--proc` must name standalone executables in root-owned paths whose ancestors cannot be written by other users; inline `--sh` shell command strings are refused. Evaluators run as an unprivileged identity with a clean environment. Scripts that depend on user-owned imports, local agents, or privileged access need a separately trusted setup; these restrictions do not apply to weak or non-root server mode.
- Linux strong mode requires trusted system helpers at `/usr/sbin/ip`, `/usr/sbin/nft`, and `/usr/bin/setpriv` (Docker mode also requires `/usr/bin/docker`); privileged setup and cleanup never search a payload-controlled `PATH`.
- Native Linux strong mode gives the payload a private PID namespace and `/proc` mount so it cannot import file descriptors from same-user host processes; `/usr/bin/unshare` is required.
- Newly created CA private keys and request logs use owner-only permissions on Unix. When Linux httpjail runs as root under sudo, request logs must be in a directory owned by the invoking user; new logs are assigned to that user, and existing logs must already belong to them (symlinks and protected host files are rejected). Existing log permissions remain unchanged; logs include full URLs, including query strings, so protect them accordingly.
- On Linux, strong-mode canaries and Docker public-certificate snapshots live under root-owned `/var/lib/httpjail` rather than sudo-preserved `HOME` or `TMPDIR`; its ancestors must not be writable by other users. Privileged Linux CA files likewise live under `/var/lib/httpjail/ca` instead of a sudo-preserved home directory; upgrading from a user-home CA generates a new root-only CA, so update client trust and remove trust in the old certificate. Interrupted CA writes are repaired on startup: a valid signing key is preserved, but a missing or corrupt key generates a new CA and requires renewing client trust.
- HTTP/1 request headers have a 10-second read deadline; request bodies and upgraded connections do not inherit that deadline.
- Docker mode requires the local daemon at `/var/run/docker.sock` (remote Docker contexts are ignored), mounts only that public-certificate snapshot (never the signing key), drops `NET_RAW`, and accepts only restricted `docker run` flags before the image (environment, user, working directory, resource limits, read-only/init, and `--rm`). Network, DNS, privileged, volume, port-publishing, and other unrecognized Docker flags are rejected. Arguments after the image are passed to the container unchanged.
- Legacy canaryless namespaces, networks, and leftover firewall tables require manual administrator inspection rather than unsafe automatic deletion.

## Documentation

Docs are stored in the `docs/` directory and served
Expand Down
11 changes: 7 additions & 4 deletions docs/advanced/request-body-limiting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ This is primarily designed for mitigating code exfiltration attacks through cove

## Size Calculation

The `max_tx_bytes` limit applies to **complete** HTTP requests, including:
The `max_tx_bytes` limit applies to request fields (before transport encoding), including:

1. **Request line**: `METHOD /path HTTP/1.1\r\n`
2. **Headers**: Each header as `Name: Value\r\n`
3. **Header separator**: Final `\r\n` between headers and body
4. **Body**: Request body bytes
4. **Body**: Request body data bytes
5. **Trailers**: Trailer names, values, and separators when present

HTTP/1 chunk delimiters and other framing inserted by the HTTP client are not included in this logical-byte budget. Do not use `max_tx_bytes` as an exact wire-byte accounting guarantee.

## Response Format

Expand Down Expand Up @@ -47,15 +50,15 @@ When the request includes a `Content-Length` header (most standard HTTP clients)
HTTP/1.1 413 Payload Too Large
Content-Type: text/plain

Request body size (5000 bytes) exceeds maximum allowed (1024 bytes)
Request size (5000 bytes) exceeds maximum allowed (1024 bytes)
```

### Without Content-Length Header

When the request uses chunked encoding or doesn't include `Content-Length`:

1. **Stream Truncation**: The request body is truncated at the limit during streaming
2. **Upstream Receives Partial**: The upstream server receives exactly `max_tx_bytes` total bytes (url + headers + truncated body)
2. **Upstream Receives Partial**: The upstream receives no more than the logical request budget (URL + headers + truncated body/trailers); framing may add wire bytes.
3. **Connection Closes**: The connection terminates after reaching the limit

## Examples
Expand Down
13 changes: 7 additions & 6 deletions docs/guide/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ httpjail works differently on each platform due to OS-specific networking capabi
| Traffic isolation | ✅ Namespaces + nftables | ⚠️ Env vars only | 🚧 Planned |
| TLS interception | ✅ Transparent | ✅ Via proxy settings | 🚧 Planned |
| Sudo required | ⚠️ Yes | ✅ No | 🚧 |
| Force all traffic | ✅ Yes | ❌ Apps must cooperate | 🚧 |
| Force direct IP traffic | ✅ Yes | ❌ Apps must cooperate | 🚧 |

## Linux

Full network isolation using namespaces and nftables.
Direct IP traffic isolation uses namespaces and nftables. Native strong mode denies new connections to host Unix-domain services with seccomp (while allowing local stream socketpairs); it does not isolate the filesystem or explicitly inherited file descriptors. Some workloads that rely on local IPC will not work.

```
┌─────────────────────────────────────────────────┐
Expand All @@ -38,10 +38,11 @@ Full network isolation using namespaces and nftables.

### Prerequisites

- Linux kernel 3.8+ (network namespace support)
- nftables (`nft` command)
- Linux kernel with network namespaces and seccomp filter support (4.14+ recommended)
- `ip` at `/usr/sbin/ip`, `nft` at `/usr/sbin/nft`, `setpriv` at `/usr/bin/setpriv`, and `unshare` at `/usr/bin/unshare`
- libssl-dev (for TLS)
- sudo access (for namespace creation)
- `sudo` from a non-root user (direct-root invocation is refused)
- Docker mode: Docker CLI at `/usr/bin/docker`

### How It Works

Expand All @@ -53,7 +54,7 @@ Full network isolation using namespaces and nftables.
### Usage

```bash
# Strong mode (default) - full isolation
# Strong mode (default) - direct IP isolation, not filesystem isolation
sudo httpjail --js "r.host === 'github.com'" -- curl https://api.github.com

# Weak mode - environment variables only (no sudo)
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ fi
```

```bash
# Linux strong mode: keep policies and their parent dirs outside the jailed user's write access.
sudo install -d -m 755 /etc/httpjail
sudo install -o root -g root -m 755 check.sh /etc/httpjail/check.sh
sudo httpjail --sh /etc/httpjail/check.sh -- git clone https://github.com/user/repo.git

# macOS/weak mode: environment-only proxying (not a hard isolation boundary)
chmod +x check.sh
httpjail --sh ./check.sh -- git clone https://github.com/user/repo.git
```
Expand Down
Loading
Loading