Troubleshooting
May 5, 2026 · View on GitHub
Nested Sandboxing Not Supported
Fence cannot run inside another sandbox that uses the same underlying technology.
macOS (Seatbelt): If you try to run fence inside an existing sandbox-exec sandbox (e.g., Nix's Darwin build sandbox), you'll see:
Sandbox: sandbox-exec(...) deny(1) forbidden-sandbox-reinit
This is a macOS kernel limitation - nested Seatbelt sandboxes are not allowed. There is no workaround.
Linux (Landlock): Landlock supports stacking (nested restrictions), but fence's test binaries cannot use the Landlock wrapper (see Testing docs).
"bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted" (Linux)
This error occurs when fence tries to create a network namespace but the environment lacks the CAP_NET_ADMIN capability. This is common in:
- Docker containers (unless run with
--privilegedor--cap-add=NET_ADMIN) - GitHub Actions and other CI runners
- Ubuntu 24.04+ with restrictive AppArmor policies
- Kubernetes pods without elevated security contexts
What happens now:
Fence automatically detects this limitation and falls back to running without network namespace isolation. The sandbox still provides:
- Filesystem restrictions (read-only root, allowWrite paths)
- PID namespace isolation
- Seccomp syscall filtering
- Landlock (if available)
What's reduced:
- Network isolation via namespace is skipped
- The proxy-based domain filtering still works (via
HTTP_PROXY/HTTPS_PROXY) - But programs that bypass proxy env vars won't be network-isolated
To check if your environment supports network namespaces:
fence --linux-features
Look for the Network namespace row. Status ok means bwrap --unshare-net works; status unavailable means Fence will continue without network namespace isolation.
Solutions if you need full network isolation:
-
Run with elevated privileges:
sudo fence <command> -
In Docker, add capability:
docker run --cap-add=NET_ADMIN ... -
In GitHub Actions, this typically isn't possible without self-hosted runners with elevated permissions.
-
On Ubuntu 24.04+, you may need to modify AppArmor profiles (see Ubuntu bug 2069526).
"bwrap: setting up uid map: Permission denied" (Linux)
This error occurs when bwrap cannot create user namespaces. This typically happens when:
- The
uidmappackage is not installed /etc/subuidand/etc/subgidare not configured for your user- bwrap is not setuid
Quick fix (if you have root access):
# Install uidmap
sudo apt install uidmap # Debian/Ubuntu
# Make bwrap setuid
sudo chmod u+s $(which bwrap)
Or configure subuid/subgid for your user:
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid
On most systems with package-manager-installed bwrap, this error shouldn't occur. If it does, your system may have non-standard security policies.
"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 deniedfence -d <command>to see full proxy and sandbox detailfence -m --fence-log-file /tmp/fence.log <command>if a fullscreen TUI redraws over Fence's own logstail -f /tmp/fence.login another terminal to watch those logs live
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) - Shell startup/env differences (default is deterministic
bash -c)
"command not found" for tools available in your shell
By default, fence runs commands in a deterministic shell mode (bash -c), not necessarily your current interactive shell.
If a tool is only available after your shell startup config runs (for example via zsh setup), use:
fence --shell user --shell-login -c "your-command"
Notes:
--shell useruses your validated$SHELLpath.--shell-loginuses-lcso login startup files are loaded.- If your required
PATHis already exported before launching fence,--shell userwithout--shell-loginmay be enough.
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:
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 allowLocalOutbound.
macOS: a boolean alone is enough; the sandbox can reach any host loopback port.
{
"network": { "allowLocalOutbound": true }
}
Linux: the sandbox runs in its own network namespace, so its 127.0.0.1 is not the host's 127.0.0.1. You must also list the exact host loopback ports to bridge. Fence starts per-port socat forwarders that relay sandbox 127.0.0.1:<port> back to host 127.0.0.1:<port>:
{
"network": {
"allowLocalOutbound": true,
"allowLocalOutboundPorts": [5432, 6379]
}
}
If allowLocalOutbound is true on Linux but allowLocalOutboundPorts is empty, Fence logs a warning and connections to 127.0.0.1 still fail with Connection refused — this is by design, so the boolean alone never silently exposes arbitrary host services.
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))
By default -p PORT binds the host-side listener on 127.0.0.1 only. To
expose the service on every interface (e.g. so other hosts on your LAN can
reach it) write -p 0.0.0.0:PORT instead. Specific addresses also work:
-p 192.168.1.10:PORT, -p [::1]:PORT, -p [::]:PORT.
WSL2: my dev server runs but http://127.0.0.1:PORT from a Windows browser doesn't load
WSL2 forwards a Windows-side localhost:PORT request to the WSL distro
when, and only when, a process inside the distro is bound on 127.0.0.1.
Listeners bound on 0.0.0.0 / * (all interfaces) often don't trigger
the automatic relay.
If you launch fence with fence -p PORT -- yourserver, the host-side
reverse bridge binds 127.0.0.1:PORT by default, which is what the WSL
relay expects, so the Windows browser will reach the server. If you wrote
fence -p 0.0.0.0:PORT -- yourserver to opt into LAN exposure, the
Windows-side localhost:PORT mapping may fail; reach the server via the
WSL VM's IP instead:
# Windows PowerShell or cmd
wsl.exe hostname -I # e.g. 172.20.143.42
# then point your browser at http://172.20.143.42:PORT/
If you also want Windows-side localhost:PORT to work in that case,
either bind on 127.0.0.1 instead, or enable WSL2 mirrored networking
([wsl2] networkingMode=mirrored in %UserProfile%\.wslconfig).
You can confirm fence's listener address with:
ss -ltnp | grep ':PORT ' # look at the Local Address column
127.0.0.1:PORT works through WSL's relay; *:PORT and 0.0.0.0:PORT
need the WSL VM IP route above.
Note
The sandboxed process itself can still bind any address it likes inside
the sandbox (127.0.0.1, 0.0.0.0, …). The bind address -p controls
is only the host-side reverse-bridge listener that relays inbound traffic
into the sandbox.
WSL2: "Permission denied" for wslpath or Windows .exe files
On WSL2, commands like wslpath or powershell.exe may fail with "Permission denied" inside the sandbox.
Common causes:
wslpathfails: Fence auto-detects WSL and allows/init(the binfmt_misc interpreter thatwslpathsymlinks to) viawslInterop. If this still fails, check thatwslInteropis not set tofalsein your config.- Windows
.exefiles fail: Paths under/mnt/are not auto-allowed. You must add the specific executables you need.
Fix: Add the Windows executables you need to allowExecute:
{
"extends": "code",
"filesystem": {
"allowExecute": [
"/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe"
]
}
}
Use allowExecute (not allowRead or allowWrite) for the tightest permissions. Only add the specific executables you need — avoid allowing all of /mnt.
Note
You do not need to add /init — it is handled automatically by wslInterop (enabled by default on WSL). Set "wslInterop": false to disable it.
See Linux Security Features > WSL2 for details.
"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:
{
"filesystem": {
"allowWrite": [".", "/tmp"],
"denyWrite": [".env", "*.key"]
}
}