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 --privileged or --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:

  1. Run with elevated privileges:

    sudo fence <command>
    
  2. In Docker, add capability:

    docker run --cap-add=NET_ADMIN ...
    
  3. In GitHub Actions, this typically isn't possible without self-hosted runners with elevated permissions.

  4. 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 uidmap package is not installed
  • /etc/subuid and /etc/subgid are 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 denied
  • fence -d <command> to see full proxy and sandbox detail
  • fence -m --fence-log-file /tmp/fence.log <command> if a fullscreen TUI redraws over Fence's own logs
  • tail -f /tmp/fence.log in 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 user uses your validated $SHELL path.
  • --shell-login uses -lc so login startup files are loaded.
  • If your required PATH is already exported before launching fence, --shell user without --shell-login may 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:

  • wslpath fails: Fence auto-detects WSL and allows /init (the binfmt_misc interpreter that wslpath symlinks to) via wslInterop. If this still fails, check that wslInterop is not set to false in your config.
  • Windows .exe files 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"]
  }
}