> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clawker.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Egress Firewall — Network Isolation for AI Agents

> Clawker's deny-by-default egress firewall for sandboxing AI coding agents: eBPF cgroup egress enforcement, CoreDNS, and Envoy deliver network isolation and data exfiltration prevention. Architecture, configuration, CLI commands, and troubleshooting.

Clawker ships a **deny-by-default egress firewall** — the network isolation layer that makes it a sandbox for AI coding agents. It restricts all container egress to explicitly allowed domains, so a prompt-injected agent cannot exfiltrate your code or credentials to any destination outside your allowlist — egress to everything you haven't approved is denied at the kernel. The firewall runs as a shared stack — an Envoy egress proxy, a custom CoreDNS resolver with a `dnsbpf` plugin, and a set of eBPF cgroup programs that enforce egress at the kernel — managed automatically by clawker on an isolated Docker bridge network. One firewall stack serves all clawker-managed containers on the host (1:N). It provides DNS-level blocking, per-domain TCP routing, and TLS-level inspection without granting your agent containers any special privileges.

## Architecture

The firewall is composed of two managed Docker containers plus a set of eBPF programs attached from outside each agent container:

* **Envoy** (`envoyproxy/envoy`, TLS listener `10000`, sequential TCP listeners from `10001`) — TLS termination with per-domain certificates for all allowed domains. Envoy terminates TLS, inspects HTTP traffic (paths, methods, response codes visible), then re-encrypts upstream. Default deny (connection reset) for unrecognized SNI.
* **CoreDNS** (`clawker-coredns:latest`, port `53`) — a *custom* CoreDNS build from `cmd/coredns-clawker`. Provides deny-by-default DNS filtering (NXDOMAIN for anything not in the allowlist, Cloudflare malware-blocking upstream `1.1.1.2` / `1.0.0.2`) and embeds the first-party `dnsbpf` plugin that writes every resolved IP to the BPF `dns_cache` map in real time — this is what lets the BPF `connect4/connect6` programs do per-domain TCP routing. Runs with `CAP_BPF + CAP_SYS_ADMIN` and a `/sys/fs/bpf` bind mount, the minimum required to update the pinned cache map.
* **eBPF cgroup programs** (`connect4`, `sendmsg4`, `recvmsg4`, `connect6`, `sendmsg6`, `recvmsg6`, `sock_create`) — loaded and attached **from outside the agent container** by the clawker control plane. Owns the pinned BPF maps under `/sys/fs/bpf/clawker/`: `container_map`, `route_map`, `dns_cache`, `bypass_map`, `metrics_map`, plus the netlogger telemetry maps `events_ringbuf` (decision-point event channel), `events_drops` (kernel-fault drop counter), `ratelimit_state` (per-cgroup token bucket), `ratelimit_drops` (per-cgroup throttled-event counter). The agent container itself runs fully unprivileged — no Linux capabilities, no firewall scripts, no init-time network gymnastics. The control plane container that owns the load step runs with `CAP_BPF + CAP_SYS_ADMIN`, a `/sys/fs/bpf` RW bind mount, a `/sys/fs/cgroup` RO bind mount, the Docker socket, and `apparmor=unconfined` — see [Control Plane → Container Privileges](/control-plane#container-privileges) for the full set and why each is required.

Both managed containers run on the `clawker-net` bridge network with deterministic static IPs computed from the network gateway by replacing its last octet: Envoy at `<network>.200`, CoreDNS at `<network>.201` (so e.g. `192.168.215.200` / `.201` on a default Docker bridge with gateway `192.168.215.1`). Agent containers join the same network with `--dns` pointed at CoreDNS, and the eBPF `connect4/connect6` programs redirect all outbound TCP to Envoy. CoreDNS forwards Docker internal names (`host.docker.internal`, monitoring stack containers) back to Docker's embedded DNS at `127.0.0.11` so internal networking keeps working.

```
Agent Container      CoreDNS (.201)     eBPF maps       Envoy (.200)      Internet
    |                      |                |                |               |
    |-- DNS query -------->|                |                |               |
    |                      |-- dnsbpf write IP->hash ------->|               |
    |<- NXDOMAIN (blocked)-|                                                  |
    |<- resolved IP -------|                                                  |
    |                                                                          |
    |-- TCP (connect4/connect6 rewrite via route_map + dns_cache) --->|       |
    |                                                        |-- TLS inspect ->|
    |                                                        |<- response ----|
    |<-------------------------------------------------------|               |
```

Each connect4/connect6/sendmsg/sock\_create decision also emits one event onto a parallel BPF ringbuf drained by the netlogger pipeline — see [Egress Observability](/observability) for the record shape and where it lands.

## How It Works

1. When you run `clawker run` or `clawker container create` with the firewall enabled, clawker brings the firewall stack up automatically (this happens transparently — see [Control Plane](/control-plane) if you want the lifecycle details).
2. The `clawker-net` bridge network is created, the eBPF programs are loaded into the kernel and their maps pinned under `/sys/fs/bpf/clawker/`, then CoreDNS and Envoy are launched. CoreDNS opens the pinned `dns_cache` map on startup, so the eBPF state must exist before CoreDNS boots — this ordering is preserved on every reload.
3. Project rules from `.clawker.yaml` are merged with system-required rules (additive merge, dedup by `destination:protocol:port`).
4. Envoy, CoreDNS, and the global `route_map` are (re)generated from the merged ruleset.
5. Agent containers join the firewall network; the eBPF cgroup programs attach to each container's cgroup, and an entry is written to `container_map` (cgroup\_id → container config). Presence in `container_map` is what gates enforcement — the `route_map` itself is global.
6. As the agent resolves DNS, the `dnsbpf` plugin writes `IP → {domain_hash, TTL}` entries into the `dns_cache` map. On each outbound TCP connection, the `connect4` / `connect6` programs look up the destination IP in `dns_cache`, then look up `{domain_hash, dst_port}` in `route_map` to decide which Envoy listener to redirect to.
7. Envoy and CoreDNS are health-probed continuously. When the last clawker-managed agent container exits, the firewall stack and eBPF state are drained and flushed cleanly.
8. At every cgroup decision point, the BPF program also reserves a slot in the parallel `events_ringbuf` and writes the verdict + 4-tuple + `cgroup_id`. The CP-side netlogger drains the ringbuf, enriches each record by `cgroup_id` with container/agent/project attribution, and emits one OTLP log record per decision. See [Egress Observability](/observability) for the record shape, attribute reference, and the OpenSearch index they land in.

<Note>
  Because `route_map` is global, `clawker firewall add`, `clawker firewall remove`, and `clawker firewall reload` immediately propagate rule changes to **all** running agent containers via an atomic route-map sync — no agent restart, no firewall stack restart.
</Note>

### IPv4, IPv6, and dual-stack

The BPF `connect6` program applies the full `connect4` routing logic to **IPv4-mapped IPv6 addresses** (`::ffff:x.x.x.x`). Dual-stack clients — SSH, curl, Node.js, Go's default resolver — use IPv4-mapped sockets, so they get the same per-domain routing through Envoy as native IPv4.

<Warning>
  **Native IPv6 is denied.** The firewall does not currently route native IPv6 egress (only loopback `::1` and IPv4-mapped addresses pass through). If your agent truly needs native IPv6 to reach a service, it will fail with a connection reset. In practice this is rare because dual-stack clients fall back to IPv4-mapped automatically.
</Warning>

## Default Allowed Domains

The hardcoded allowlist is deliberately **bare-bones** — only domains required for Claude Code itself are included (API access, OAuth, telemetry):

| Domain                    | Purpose                                                                          |
| ------------------------- | -------------------------------------------------------------------------------- |
| `api.anthropic.com`       | Claude API                                                                       |
| `claude.com`              | OAuth/login                                                                      |
| `platform.claude.com`     | OAuth token exchange                                                             |
| `.claude.ai`              | OAuth authorization, downloads (with `/public/` and `/share/` path-deny for UGC) |
| `mcp-proxy.anthropic.com` | MCP tool server proxy                                                            |
| `registry.npmjs.org`      | npm (Node.js is baked into every image; required for global package installs)    |
| `sentry.io`               | Error tracking                                                                   |
| `statsig.anthropic.com`   | Feature flags                                                                    |
| `statsig.com`             | Feature flags                                                                    |
| `.datadoghq.com`          | Telemetry (all Datadog subdomains/regions)                                       |
| `.datadoghq.eu`           | Telemetry (Datadog EU regions)                                                   |

<Info>
  A leading dot (e.g., `.datadoghq.com`) is the **wildcard convention** — it matches the apex domain and all subdomains. Use this for services with region-specific subdomains. Without the leading dot, only the exact domain is allowed.
</Info>

<Warning>
  **GitHub, PyPI, and most other services are NOT in this list.** (npm registry is included because Node.js is baked into every image.) When you run `clawker project init`, the generated `.clawker.yaml` template includes `github.com` and `api.github.com` as a convenience starting point — but these are project-level config entries, not system defaults. If you start from a blank config, GitHub SSH and HTTPS will be blocked.

  You must explicitly configure every external domain your agent needs. This is by design — an AI agent should not have unrestricted internet access. Review your project's network dependencies and add them to `add_domains` or `rules` in your `.clawker.yaml`.
</Warning>

### Common Setups

Here are examples for common services your agent will likely need. Add what you need to your `.clawker.yaml` (see the default allowlist above for what is already included).

#### Git SSH (GitHub, GitLab, Bitbucket)

SSH git operations (`git clone git@github.com:...`) require an explicit SSH rule. The `add_domains` shorthand only covers HTTPS (port 443) — SSH needs a dedicated `rules` entry on port 22:

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - github.com       # HTTPS git + API
      - api.github.com   # GitHub API (needed for gh CLI)
    rules:
      - dst: github.com
        proto: ssh
        port: "22"
        action: allow
```

Without the SSH rule, `git push` and `git clone` over SSH will fail with a connection reset. The agent's SSH keys are forwarded from your host (see [Credential Forwarding](/credentials)), but the firewall still needs to allow the traffic through.

For GitLab or Bitbucket, add equivalent rules:

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - gitlab.com
      - bitbucket.org
    rules:
      - dst: gitlab.com
        proto: ssh
        port: "22"
        action: allow
      - dst: bitbucket.org
        proto: ssh
        port: "22"
        action: allow
```

#### Package Registries

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      # Node.js / npm — registry.npmjs.org is already in the default allowlist
      - registry.yarnpkg.com
      # Python / pip
      - pypi.org
      - files.pythonhosted.org
      # Go modules
      - proxy.golang.org
      - sum.golang.org
      - storage.googleapis.com
      # Rust / cargo
      - crates.io
      - static.crates.io
```

#### Full Working Example

A typical project that uses GitHub and npm (npm registry is already in the default allowlist):

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - github.com
      - api.github.com
    rules:
      - dst: github.com
        proto: ssh
        port: "22"
        action: allow
```

<Tip>
  The `clawker project init` template pre-populates GitHub domains and SSH rules for you. If you're starting from the template, Git should work out of the box. These examples are for understanding what's required if you're building a config from scratch or adding new services.
</Tip>

## Configuration

The firewall is configured in two places:

* **Global toggle**: `firewall.enable` in `settings.yaml` (enable/disable the entire firewall)
* **Per-project rules**: `security.firewall` section of `.clawker.yaml` (which domains to allow)

Project-level firewall configuration in `.clawker.yaml`:

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - "api.openai.com"
      - "registry.npmjs.org"
    rules:
      - dst: "api.example.com"
        proto: https
        port: "443"
        action: allow
        path_rules:
          - path: "/v1/chat"
            action: allow
        path_default: deny
```

### `add_domains`

A convenience shorthand for allowlisting entire domains. Each entry is automatically converted to an `https` allow rule on port 443 with no path restrictions (allow-all routing through TLS inspection).

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - "api.openai.com"
      - "registry.npmjs.org"
      - "pypi.org"
      - ".datadoghq.com"   # Leading dot = wildcard (all subdomains)
```

A **leading dot** (e.g., `.datadoghq.com`) enables wildcard matching — the apex domain and all subdomains are allowed. Without the leading dot, only the exact domain is matched. Use wildcards for services with region-specific subdomains (e.g., Datadog's `us5.datadoghq.com`, `eu1.datadoghq.com`).

### `rules`

Full rule specification for fine-grained control:

| Field          | Type   | Description                                                                                                                                                        |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dst`          | string | Domain name or IP address. Prefix with `.` for wildcard (e.g., `.datadoghq.com` matches all subdomains)                                                            |
| `proto`        | string | `https` (default), `http`, `tcp`, `ssh`, `ws`, `wss`, `udp`, or any opaque L7 name for TCP pass-through. The legacy value `tls` is silently translated to `https`. |
| `port`         | string | Destination port: a single port (`"443"`) or an inclusive range (`"9000-9100"`). Empty means the protocol default (443 for https/wss, 80 for http/ws, 22 for ssh). |
| `action`       | string | `allow` or `deny` (default: `allow`)                                                                                                                               |
| `path_rules`   | list   | Optional path prefix rules (each entry has `path`, `action`, and optional `methods`). Supported on `https`, `http`, `ws`, and `wss` protocols only                 |
| `path_default` | string | Default action for paths not matching any `path_rules` entry (default: `deny`)                                                                                     |

Each `path_rules` entry:

| Field     | Type   | Description                                                                                                                                                                                                                                                                                |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `path`    | string | URL path to match. By default a **prefix** (e.g. `/v1`). Prefix the value with `~` to match it as a **regular expression** instead, which is anchored end-to-end for exact matching (e.g. `~/repos/(myorg\|other)/?`). See [Exact and pattern matching](#exact-and-pattern-matching) below |
| `action`  | string | `allow` or `deny` for requests matching this path (and methods, if set)                                                                                                                                                                                                                    |
| `methods` | list   | Optional HTTP methods this rule applies to (e.g. `[GET, HEAD]`). Empty = all methods. See [Method gating](#method-gating) below                                                                                                                                                            |

#### Protocol behavior

* **`https`** (default) --- HTTPS traffic. Envoy terminates TLS with a per-domain certificate, inspects HTTP traffic (paths visible in access logs), then re-encrypts upstream. With `path_rules`, per-path routing is applied; without `path_rules`, all traffic to the domain is allowed. `wss` is the WebSocket-over-TLS variant (same stack, WebSocket upgrade enabled per route).
* **`http`** --- Plain HTTP traffic. Envoy inspects the Host header for domain matching and applies path rules directly. No TLS involved. `ws` is the WebSocket-over-HTTP variant.
* **`udp`** --- Raw UDP datagrams. No domain or path inspection; each rule gets a dedicated `udp_proxy` listener pinned to the rule's host.
* **`tcp`** --- Raw TCP forwarding to a specific port. No domain or path inspection. See the port-level routing note below.
* **`ssh`** --- SSH traffic forwarding. Functionally identical to `tcp` but semantically distinct. See the port-level routing note below.

<Warning>
  **TCP/SSH rules pin all traffic on that port to the whitelisted host — the agent cannot reach any other server on that port.** TLS and HTTP protocols carry domain metadata (SNI and Host header) that Envoy uses to match traffic to the correct rule. Raw TCP and SSH have no such mechanism. Instead, each `ssh`/`tcp` rule gets its own dedicated eBPF route keyed by `(domain, port)` that redirects matching traffic to a dedicated Envoy TCP listener pinned to that domain. Envoy resolves the whitelisted domain at connection time and forwards traffic there.

  For example, a `proto: ssh` rule for `github.com` on port 22 means every outbound port 22 connection from the container that resolved `github.com` ends up at GitHub's SSH endpoint. Multiple SSH rules for different hosts on the same port each get their own dedicated listener — `github.com:22` and `gitlab.com:22` are independent routes. An unresolved IP on port 22 (e.g. a hardcoded IP that didn't go through CoreDNS) has no matching route and is denied.

  **For security testers:** a successful `connect()` on a port with a whitelisted rule does not mean you have reached your intended target. The connection was silently redirected to the whitelisted service. Always verify the remote banner or certificate before concluding you have egress — otherwise you may be sending data to GitHub, GitLab, or another corporate service rather than your C2.
</Warning>

#### Path rules

Path rules give you fine-grained control over which URL paths are allowed for a domain. Envoy uses **prefix matching** --- a rule for `/api/v1` matches `/api/v1`, `/api/v1/users`, `/api/v1/models/list`, etc.

When `path_rules` is specified, `path_default` controls what happens to paths that don't match any rule. It defaults to `deny`, meaning only explicitly allowed paths get through.

##### Exact and pattern matching

Prefix matching is open-ended, which can let more through than you intend: an
`allow` rule for `/repos/myorg` also matches `/repos/myorg-evil`. On a host where
the path embeds a name you don't control (`/repos/<user>`, `/u/<name>`), that gap
is exploitable.

To match exactly, prefix the `path` with `~`. The rest of the value is then a
[regular expression](https://github.com/google/re2/wiki/Syntax) matched against
the **whole** path, so it is anchored at both ends --- no open-ended tail:

| Rule                       | Matches                                                   | Does **not** match                        |
| -------------------------- | --------------------------------------------------------- | ----------------------------------------- |
| `/repos/myorg` (prefix)    | `/repos/myorg`, `/repos/myorg/pulls`, `/repos/myorg-evil` | ---                                       |
| `~/repos/myorg` (exact)    | `/repos/myorg` only                                       | `/repos/myorg/`, `/repos/myorg-evil`      |
| `~/repos/myorg/?`          | `/repos/myorg`, `/repos/myorg/`                           | `/repos/myorg-evil`, `/repos/myorg/pulls` |
| `~/repos/(myorg\|other)/?` | those two repos, with or without a trailing slash         | `/repos/myorg-evil`                       |
| `~/repos/myorg(/.*)?`      | `/repos/myorg` and everything under it                    | `/repos/myorg-evil`                       |

The trailing slash is significant: `~/blog` and `~/blog/` are different rules, so
match the form(s) your server actually serves (use the `/?` or `(/.*)?` tail if it
accepts both). Rules without `~` keep prefix behavior --- nothing changes for
existing rules.

A path must start with `/` (a regex must anchor at `/` or `^/`); the firewall does
not guess. An invalid path --- a literal missing the leading `/`, a literal
containing characters that can't appear in a URL path (often a regex written
without the leading `~`), or a regex that won't compile --- **fails the whole
add/refresh operation** with an error rather than silently loosening the rule.

When several rules match the same path, the **longest rule string wins**, and
equal-length matches fall to declaration order (first listed wins). A regex's
"length" is its literal character count, **not** how much it matches --- so a
short, broad regex can lose to a longer, more specific literal and vice versa.
To force a particular winner among overlapping rules, list it first or make its
string longer.

When adding a regex path from the CLI, quote it --- the shell otherwise expands
`~/` and treats `(`, `|`, `?` as special:

```bash theme={"dark"}
clawker firewall add api.github.com --path '~/repos/(myorg|other)/?' --action allow
```

**Pattern 1: Allow specific paths, deny everything else** (default deny)

```yaml theme={"dark"}
security:
  firewall:
    rules:
      - dst: "api.example.com"
        proto: https
        port: "443"
        action: allow
        path_rules:
          - path: "/v1/chat"
            action: allow
          - path: "/v1/models"
            action: allow
        path_default: deny  # this is the default, shown for clarity
```

Requests to `/v1/chat/completions` and `/v1/models` are forwarded. Everything else (e.g., `/admin`, `/internal`) gets a `403 Forbidden` response.

**Pattern 2: Deny specific paths, allow everything else**

```yaml theme={"dark"}
security:
  firewall:
    rules:
      - dst: "example.com"
        proto: http
        port: "80"
        action: allow
        path_rules:
          - path: "/evil"
            action: deny
        path_default: allow
```

All paths are forwarded except those starting with `/evil`, which get a `403`.

#### Method gating

Because the firewall MITM-terminates HTTPS, the decrypted HTTP request line --- including the request **method** --- is visible at the proxy. A path rule's optional `methods` field narrows its `action` to a set of HTTP verbs (`GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, ...). It is a **match condition**, not a separate verdict: `action` supplies the polarity, and methods not in the set fall through to later path rules or `path_default`.

* Empty `methods` (the default) = the rule applies to **all** methods --- a rule with no `methods` field is method-agnostic.
* `action: allow` + `methods` = allow-list those verbs (others fall through).
* `action: deny` + `methods` = deny-list those verbs (others fall through).
* HTTP-family protos only (`https`/`http`/`ws`/`wss`). On `tcp`/`ssh`/`udp` there is no HTTP request line, so `methods` (like `path_rules`) is ignored with a warning.

There is no rule-level `methods` field. To make a whole host read-only, use a single `/` path rule --- a path rule for `/` matches every request:

```yaml theme={"dark"}
security:
  firewall:
    rules:
      # GitHub read-only: allow GET/HEAD everywhere, deny every mutating method
      - dst: "api.github.com"
        proto: https
        port: "443"
        action: allow
        path_rules:
          - path: "/"
            action: allow
            methods: [GET, HEAD]
        path_default: deny
```

A `GET` or `HEAD` to any path is forwarded; a `POST`/`PUT`/`PATCH`/`DELETE` matches no route and falls to `path_default: deny` → `403`. This blocks `git push` (`POST .../git-receive-pack`) and contents-API writes (`PUT|DELETE /repos/.../contents/...`) without enumerating every write path.

The inverse --- block writes on a prefix while leaving reads open --- pairs `action: deny` with the mutating verbs:

```yaml theme={"dark"}
security:
  firewall:
    rules:
      - dst: "api.github.com"
        proto: https
        port: "443"
        action: allow
        path_rules:
          # deny writes under /repos/; GET/HEAD fall through to path_default
          - path: "/repos/"
            action: deny
            methods: [POST, PUT, PATCH, DELETE]
        path_default: allow
```

You can also set method gates from the CLI: `clawker firewall add api.github.com --path / --action allow --methods GET,HEAD`.

**Mixed protocol examples:**

```yaml theme={"dark"}
security:
  firewall:
    rules:
      - dst: "api.example.com"
        proto: https
        port: "443"
        action: allow
        path_rules:
          - path: "/v1/chat"
            action: allow
        path_default: deny
      - dst: "git.internal.corp"
        proto: ssh
        port: "22"
        action: allow
      - dst: "10.0.0.5"
        proto: tcp
        port: "8080"
        action: allow
      - dst: "example.com"
        proto: http
        port: "80"
        action: allow
```

## Global vs Project Rules

* **System-required rules** (from `cfg.RequiredFirewallRules()`) are always present --- Claude API, OAuth, error tracking, and feature flags
* **Project rules** come from `add_domains` and `rules` in your `.clawker.yaml`
* Rules merge **additively** --- project rules add to (never replace) system rules
* Dedup key: `destination:protocol:port` --- duplicate rules are silently ignored
* This means you cannot accidentally override or remove a system-required rule

## CLI Commands

All firewall operations are available under `clawker firewall`:

| Command                                  | Purpose                                                                                                                                                                                                                     |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clawker firewall status`                | Show firewall health, running containers, rule count                                                                                                                                                                        |
| `clawker firewall list`                  | List all active egress rules                                                                                                                                                                                                |
| `clawker firewall add DOMAIN`            | Add a domain allow rule (use `--path` + `--action` to attach a path-scoped rule; `--path` is a URL path prefix, or a `~`-prefixed regex for exact matching — see [Exact and pattern matching](#exact-and-pattern-matching)) |
| `clawker firewall remove DOMAIN`         | Remove a domain rule (use `--path` to drop a single path entry; the lookup is exact-string against the stored `path` so a typo or sub-prefix won't match)                                                                   |
| `clawker firewall reload`                | Force regenerate Envoy/CoreDNS configs from the current rule **state** (does not re-read `.clawker.yaml`)                                                                                                                   |
| `clawker firewall refresh`               | Re-read the current project's `.clawker.yaml` and sync its `add_domains`/`rules` into the store live — apply yaml edits without a container restart                                                                         |
| `clawker firewall up`                    | Start the firewall daemon (usually automatic)                                                                                                                                                                               |
| `clawker firewall down`                  | Stop the firewall daemon                                                                                                                                                                                                    |
| `clawker firewall enable --agent AGENT`  | Re-enroll a container in the firewall's per-container routing (BPF programs stay attached; idempotent)                                                                                                                      |
| `clawker firewall disable --agent AGENT` | Remove a container from the firewall's per-container routing (BPF programs stay attached; fast re-enable via `enable`)                                                                                                      |
| `clawker firewall bypass`                | Temporary unrestricted egress (eBPF bypass flag + timed re-enable)                                                                                                                                                          |
| `clawker firewall rotate-ca`             | Regenerate CA and all domain certificates                                                                                                                                                                                   |

## Certificate Management

The firewall uses a self-signed certificate authority for TLS inspection. All HTTPS traffic is terminated at Envoy with per-domain certificates, inspected at the HTTP level (making request paths, methods, and response codes visible), then re-encrypted upstream.

* An **ECDSA P256 CA** is auto-generated during `clawker build` and baked into agent container images via `update-ca-certificates`
* Per-domain certificates are generated for **every** `https`/`wss` rule --- Envoy terminates TLS for all allowed TLS domains
* Domains with `path_rules` get per-path routing; domains without get allow-all routing --- both go through TLS inspection
* The CA keypair is persisted in the firewall data directory and shared between the bundler and firewall manager
* Use `clawker firewall rotate-ca` to regenerate the CA and all domain certs

<Note>
  After rotating the CA, you must restart any running agent containers for them to pick up the new certificate.
</Note>

### Tools with custom CA bundles

Some tools (notably Python packages installed via `uv`/`uvx`, like semgrep) ship their own CA certificate bundles and ignore the system trust store. Clawker sets `SSL_CERT_FILE` and `CURL_CA_BUNDLE` in the container environment to point these tools at the system store, which includes the firewall CA.

If a tool still reports certificate errors, it may need its own environment variable. Add it to your project config:

```yaml theme={"dark"}
# clawker.yaml
agent:
  env:
    REQUESTS_CA_BUNDLE: "${SSL_CERT_FILE}"
```

<Note>
  `SSL_CERT_FILE` and `CURL_CA_BUNDLE` are set automatically in the container. They point at the system CA bundle which includes the firewall CA. Use `${SSL_CERT_FILE}` when configuring additional tools rather than hardcoding paths.
</Note>

## Bypass (Escape Hatch)

For situations where you need temporary unrestricted network access:

```bash theme={"dark"}
# Grant unrestricted egress for 5 minutes
clawker firewall bypass 5m --agent dev

# Cancel bypass early
clawker firewall bypass --stop --agent dev
```

Bypass mode sets the eBPF bypass flag, completely bypassing both DNS filtering and TLS inspection. The flag auto-clears after the specified timeout, re-enabling the firewall.

Bypassed traffic still produces a per-decision audit trail. The eBPF cgroup programs emit a `verdict=bypassed` record (with attribution, destination 4-tuple, and resolved domain when available) for every connect/sendmsg/sock\_create call that takes the bypass path. The records land in OpenSearch alongside `allowed`/`denied` decisions — see [Egress Observability](/observability) for the record shape and how to query them.

<Warning>
  Bypass mode removes all network restrictions for the specified agent. Use it sparingly and with short timeouts. The agent has full internet access during a bypass.
</Warning>

## Disabling the Firewall

To disable the firewall entirely, set `firewall.enable` to `false` in your **settings.yaml** (not the project config):

```yaml theme={"dark"}
# ~/.config/clawker/settings.yaml
firewall:
  enable: false
```

<Warning>
  Disabling the firewall removes **all** outbound network restrictions. The agent can reach any endpoint on the internet. Only do this in trusted environments where you accept the risk of unrestricted egress.
</Warning>

## Troubleshooting

### Health check failures

Run `clawker firewall status` to see the health of the stack (Envoy, CoreDNS, and the eBPF subsystem). Envoy is probed over `clawker-net` on its internal health listener port 9902 (`HTTP GET /`), and CoreDNS is probed on its health port `18902` (`HTTP GET /health`). The eBPF subsystem is considered healthy when the pinned programs and maps under `/sys/fs/bpf/clawker/` are present — they survive across firewall stack restarts by design.

If a container is unhealthy, try:

```bash theme={"dark"}
clawker firewall down
clawker firewall up
```

### Blocked domains

Check which rules are active with `clawker firewall list`. If a domain you need is missing, add it:

```bash theme={"dark"}
clawker firewall add api.openai.com
```

Or add it permanently in `.clawker.yaml`:

```yaml theme={"dark"}
security:
  firewall:
    add_domains:
      - "api.openai.com"
```

Edits to `.clawker.yaml` only take effect on the next container start. To apply them to running agents without a restart, run `clawker firewall refresh` — it re-reads the current project's config and syncs the new rules into the live store. (Sync is add/update only; domains you delete from the yaml are not pruned — use `clawker firewall remove` for that.)

To attach a path-scoped rule onto an existing entry:

```bash theme={"dark"}
clawker firewall add api.example.com --path /v1 --action allow
clawker firewall add api.example.com --path /v2 --action deny
```

Path rules accumulate across calls. Each entry's `path` is matched as a prefix at request time, unless it is prefixed with `~`, which matches it as an anchored regex for exact matching (see [Exact and pattern matching](#exact-and-pattern-matching)). Adds, updates, and removes look the entry up by exact-string match on `path`: repeating the same `--path` with a different `--action` overwrites that entry's action; a different `--path` value appends a new entry. Use `clawker firewall remove api.example.com --path /v1` to drop a single path rule without removing the whole entry.

### DNS resolution failures

CoreDNS returns NXDOMAIN for any domain not in the allowlist. If an agent reports DNS failures for a domain it should be able to reach, verify the domain is in your rules with `clawker firewall list`.

Docker internal names (`host.docker.internal`, monitoring stack containers like `otel-collector`) are forwarded by CoreDNS back to Docker's embedded DNS and should resolve automatically. If they don't, check that CoreDNS is running on `clawker-net` with `clawker firewall status`.

### Certificate trust errors

If an agent reports TLS certificate errors (`CERTIFICATE_VERIFY_FAILED`, `unable to get local issuer certificate`):

1. **Check if the tool uses the system trust store.** Most tools (Go, curl, wget) do. Python tools installed via `uv`/`uvx` may not --- see [Tools with custom CA bundles](#tools-with-custom-ca-bundles) above.

2. **Rotate the CA** if the certificate is expired or corrupted:

```bash theme={"dark"}
clawker firewall rotate-ca
```

Then rebuild the image (`clawker build`) and restart containers.

### Stale `dns_cache` or `route_map` after upgrade

On startup the eBPF loader detects pinned maps whose key/value sizes have changed (for example, after a clawker upgrade that ships a new `route_key` layout) and removes them before reloading. If you still suspect a stale pin, bring the firewall fully down and back up:

```bash theme={"dark"}
clawker firewall down
clawker firewall up
```

This stops the firewall stack and flushes eBPF state, so the next bringup rebuilds the pinned maps from scratch.

### IPv6-only service unreachable

If a service you need is only reachable over native IPv6, the firewall will deny the connection. Most services also publish IPv4 (or dual-stack), which dual-stack clients pick up automatically. There is no opt-in setting for native IPv6 at this time.
