Skip to content

Troubleshooting

Terminal window
wardcell doctor

It answers the question most failures turn out to be about — whether this machine would enforce a run at all — and names where the records are. Exit 0 means nothing it checks is wrong; 1 prints each problem with the command that fixes it. On macOS, run it before the first real run on a new machine: an unapproved or reboot-pending system extension leaves the machine on a weaker floor with nothing saying so. See wardcell doctor.

The proxy refused a request. The run’s summary names each blocked destination with the exact DSL line that would allow it; the full record is in the log:

Terminal window
wardcell logs show <id> --category net # add -f to watch a run still going

Copy the unblock hint into your policy and re-run. Common causes:

  • Host allowed, port not. A bare allow_http "x" covers 80 and 443 only; for anything else write allow_http "x:8443".
  • Host allowed, request not. The allow_http block needs an allow_requests rule matching this method and path.
  • A deny rule matched. Deny always wins, from any fragment. wardcell policy explain -- <command> shows which fragment it came from.

See Network Access.

Its log is still there:

Terminal window
wardcell logs list --all # newest first, with run id, status and command
wardcell logs show <id> # a short id prefix works, like a git short hash

A crashed status means the process is gone with no clean exit — the log holds whatever it managed to write. See Viewing Logs.

On macOS, allow_tcp works through SOCKS5 via ALL_PROXY, and clients that do not speak SOCKS — libpq, psycopg, most MySQL and Redis drivers — just sit there. Use a SOCKS-capable client, or turn on transparent capture, which needs no client cooperation.

On Linux, interception is transparent, so a hang is more likely a refused connection with no reset — check stderr for the denial.

macOS capture: a host you allowed is denied by its address

Section titled “macOS capture: a host you allowed is denied by its address”
wardcell: denied network-outbound on '203.0.113.10:443'

A program doing its own DNS (a Go binary built CGO_ENABLED=0, anything linked against c-ares) connects to a bare address, so macOS attaches no hostname and allow_http has nothing to match. Allow the address instead:

net { allow_tcp ["203.0.113.10:443"] } # or a CIDR: "203.0.113.0/24:443"

This applies only under transparent capture; see Platform Support for why.

wardcell: denied network-outbound on 'dns resolver 10.0.0.53:53'

Lookups reach the resolvers in /etc/resolv.conf without a rule, but any other resolver — usually a VPN’s split-DNS one, which macOS keeps elsewhere — must be named:

net {
allow_udp ["10.0.0.53:53"]
allow_tcp ["10.0.0.53:53"]
}

A filesystem denial names a path you did not expect

Section titled “A filesystem denial names a path you did not expect”

Usually a symlink: rules resolve against the real path, so ~/.config/foo pointing into ~/Dropbox shows up as the Dropbox path. Also common is a tool writing to a temp directory it derived at runtime — grant $TMPDIR rather than the specific path.

macOS: a run failed and named no path at all

Section titled “macOS: a run failed and named no path at all”

The kernel writes filesystem denials to the unified log and nowhere else, so an enforcing wardcell run prints nothing about a refused path — the only symptom is whatever the tool says downstream (a JVM PKIX failure, a build tool that cannot create a cache dir). The denials are still there; re-run under wardcell policy draft, which reads that log and renders the grants that would have covered them. wardcell doctor also prints a log show query that works after the run has finished.

bwrap: No permissions to create new namespace
unshare: write failed /proc/self/uid_map: Operation not permitted

wardcell builds every sandbox in an unprivileged user namespace. The first message is a kernel with them off outright (sysctl user.max_user_namespaces should be > 0; some hardened distros disable them permanently). The second is Ubuntu 23.10+, which require an AppArmor profile granting userns — the .deb installs it; otherwise it is a four-line file, with the recipe in Linux (Bubblewrap).

Resist sysctl -w kernel.apparmor_restrict_unprivileged_userns=0: it lifts the restriction for every process on the machine to solve it for one.

bwrap: command not found

bwrap and iptables must be on PATH. The apt/rpm package depends on both; a source build needs them installed by hand.

Noise. The API is still the supported one for this purpose and wardcell uses it deliberately.

A connection the policy allows still fails

Section titled “A connection the policy allows still fails”

Not every failure is a refusal — a grant gets the connection as far as the proxy, which can still fail to carry it, with no denial printed because nothing was denied. The proxy’s per-request diagnostics are written only at trace, so opt in and re-read:

/etc/wardcell/config.toml
[logs]
file_min_level = "trace"
Terminal window
wardcell logs show <id> --source mitm

That is a record per request with the URI and the reason the proxy gave up on any connection it could not complete — the firehose, meant for exactly this: a client that retries, downgrades or reports a cut stream while wardcell says nothing is wrong. See [logs].

Include the version, the resolved policy, the rendered profile, and the run’s log:

Terminal window
wardcell --version
wardcell policy explain -- <command> --output json
wardcell policy render -- <command>
wardcell logs show <id> --output json > run.jsonl

render is the actual profile handed to the backend — that plus the log is usually enough to reproduce. Where the failure is a connection that was allowed, add the proxy’s own view (--source mitm, with logs.file_min_level = "trace").