Skip to content

Security Hardening

ScenarioRecommended config
Local only (single machine)Default — no changes needed
LAN access (home/office)Bind to LAN IP, firewall to local subnet
Internet-facingReverse proxy with HTTPS, firewall, IP allowlist

Never expose the Snippbot port directly to the internet. Use nginx or Caddy to terminate TLS:

Terminal window
# Snippbot listens on localhost only
snippbot start --host 127.0.0.1 --port 18781

Your reverse proxy handles HTTPS and forwards to localhost:18781. See Reverse Proxy.

Allow only trusted IPs to reach the proxy:

Terminal window
# Ubuntu — ufw
ufw allow from YOUR_IP_HERE to any port 443
ufw deny 443
# Block direct access to Snippbot ports
ufw deny 18781
ufw deny 18790

The channel adapter (port 18790) needs to accept webhook traffic from specific platform IPs:

  • Slack: see Slack IP ranges
  • Discord: no fixed IP ranges — accept from anywhere, verify signatures
  • Telegram: 149.154.160.0/20 and 91.108.4.0/22

Always use HTTPS for internet-facing deployments:

Terminal window
# With Caddy (automatic Let's Encrypt)
caddy run --config Caddyfile
# With nginx + certbot
certbot --nginx -d snippbot.yourdomain.com

Redirect all HTTP to HTTPS — never serve on plain HTTP in production.

  1. Use strong API keys — the auto-generated snip_ keys are cryptographically random (256 bits). Don’t create short or guessable keys.

  2. Rotate keys regularly — revoke old keys and issue new ones:

    Terminal window
    # Revoke via UI: Settings → API Keys → Revoke
    # Or via API:
    curl -X DELETE http://localhost:18781/api/auth/keys/key_abc123 \
    -H "Authorization: Bearer $CURRENT_KEY"
  3. Use one key per client — don’t share keys between scripts, CI/CD, and users. This way you can revoke one without disrupting others.

  4. Store keys securely — use environment variables or a secrets manager. Never commit keys to version control.

Add rate limiting at the reverse proxy layer:

# nginx — limit to 30 requests/second per IP
limit_req_zone $binary_remote_addr zone=snippbot:10m rate=30r/s;
server {
location /api/ {
limit_req zone=snippbot burst=60 nodelay;
proxy_pass http://localhost:18781;
}
}
~/.snippbot/config.toml
# Require auth for all endpoints (default: true)
api_key_required = true
# Shorter session expiry for shared machines
session_expiry = 3600 # 1 hour
# Shorter inactivity timeout
inactivity_timeout = 900 # 15 minutes

The code-execution sandbox is not configured with environment variables. It is managed at runtime through the Sandbox settings page (or the PUT /api/sandbox/config API) and persisted to ~/.snippbot/sandbox_config.json.

To harden it, set the Isolation Profile to Strict, which containerizes all commands, mounts the workspace read-only, and disables the container network (zero outbound egress). Equivalently, via the API:

Terminal window
curl -X PUT http://127.0.0.1:18781/api/sandbox/config \
-H "Authorization: Bearer $SNIPPBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "all", "workspace_access": "ro", "network": {"enabled": false}}'

Cap per-container memory with the Resources fields on that page (or the resources.memory_mb config field). Ensure a real container runtime is present (Docker or Podman) rather than the process fallback — set runtime_preference to prefer docker/podman, and consider block_when_no_runtime: true so a missing runtime fails closed (blocks) instead of falling back to the host.

A marketplace package asks for network access in its manifest (the hosts it calls and why). You answer when you install it, and you can change the answer later in Packages → the package → Permissions. The answer is per installed version.

  • Packages that were never asked. A package installed by a path that shows no permission dialog (the CLI, a bundle, an update) and that declares network shows up under Needs your answer at the top of the Packages page, with a count on the Packages menu entry. Until you answer, Snippbot treats it as “no”. Allow grants the hosts its manifest lists; Don’t allow records a “no” so it isn’t asked again.

  • Turning network off for a package records a “no” for the installed version.

  • What is enforced. Snippbot checks a package’s HTTP tools against this permission: a host you didn’t allow, cloud-metadata addresses and the domains you block under Security are refused before anything is sent.

  • The operating-system lock (macOS, and containers). A package with no network permission (said no, not answered yet, or allowed only this computer) runs its programs (Python and bash tools, page backends, stdio MCP servers) with no internet: they can reach this computer (localhost) only, including Snippbot itself and local services such as ComfyUI. Names other than localhost don’t resolve inside the lock, and Unix sockets (for example Docker’s or ssh-agent’s) are refused, so Unix-socket IPC such as Python’s multiprocessing.Manager doesn’t work either. Locked programs can’t hand work to other apps: open can’t pass a link to the browser, Apple Events and new launchd jobs are refused, and Snippbot’s own code and Python environment are read-only to them. In a container (Safe or Strict isolation) such a package gets no network. When a locked package’s tool fails, the error says it has no network permission and where to allow it. On Linux the lock is not built yet, and Windows has no package sandbox, so there the permission is a record of what you agreed to, not a lock.

  • Watching first. The lock ships in shadow mode: nothing is blocked, and the daemon log notes each package program that would have run offline (package network (shadow): would run offline: …, plus a daily summary). Settings → Security → Package network lists those packages, so you can allow the ones that need the internet before turning the lock on with SNIPPBOT_PACKAGE_NETWORK_OS=enforce. A page backend or MCP server restarts when a permission change turns its network on or off.

  • Host lists (the egress proxy). A package you allowed some hosts (rather than any) runs its programs with HTTP_PROXY/HTTPS_PROXY set to a proxy inside the Snippbot daemon, on 127.0.0.1 and a port picked at start. Each program run gets its own password for it, which stops working when the run ends. For every new connection, the proxy asks the package’s current permission:

    • It connects to a host you allowed. *.example.com covers example.com and its subdomains, and a host without a port means ports 80 and 443.
    • It refuses anything else with 403.
    • It refuses cloud-metadata addresses and your blocked domains, even after a name resolves.

    The proxy reads the host and port only, never the content, so HTTPS stays end-to-end and certificate pinning keeps working. It also accepts SOCKS5 on the same port (SNIPPBOT_SOCKS_PROXY). Each blocked connection appears in the activity feed, at most once per host every 10 minutes, and in ~/.snippbot/logs/egress/blocked-YYYY-MM-DD.jsonl (hosts only). When you remove a host from the permission, the proxy closes that package’s open connections to it.

    With SNIPPBOT_PACKAGE_EGRESS_PROXY=enforce on macOS, the program is also locked to this computer like a package with no network permission, so the proxy is its only way out. Clients that ignore proxy variables then fail: ssh, smtplib, Node before v24, gRPC, and aiohttp without trust_env=True. In containers a host list is not enforced (on/off only).

  • SSH from a package. ssh ignores proxy variables, so a host-list package sets SNIPPBOT_CONNECT as its SSH ProxyCommand: ssh -o "ProxyCommand=$SNIPPBOT_CONNECT %h %p" user@host. The helper tunnels the connection through the proxy, so the host is checked (and a refusal is shown) like any other. A host needs its port in the permission (host.example:22), since a host without a port means 80 and 443 only. SNIPPBOT_CONNECT is set only for host-list packages, so a package should use it when it is present.

  • A package that asks for more hosts. Snippbot enforces the hosts you allowed, never the manifest’s list. When a package’s manifest names hosts its permission doesn’t have (an update that calls a new CDN, for example), it shows up under Asks for more hosts at the top of the Packages page, with a count on the Packages menu entry. Until you answer, the new hosts aren’t part of its permission. Allow adds exactly the hosts shown; Keep current leaves the permission as it is and records them as declined, so they aren’t asked again. An update that asks for the hosts you already answered (or fewer) keeps your answer.

  • Watching the proxy first. The proxy also ships in shadow mode. Host-list packages get the proxy variables, but nothing is blocked: the proxy forwards what it would refuse and logs package network (shadow): would block <package> → <host:port> (and the jsonl line, with "shadow": true). Programs that ignore proxy variables aren’t seen in shadow mode.

  • Settings → Security → Package network says which applies on this computer, and the install dialog labels the Network permission the same way.

  • Shell permission lists the programs a package says it runs. It is disclosure only: Snippbot doesn’t stop the package running other programs.

Three environment variables control the checks. Each accepts off, shadow (log only) and enforce; an unknown value enforces.

VariableWhat it controlsDefault
SNIPPBOT_PACKAGE_NETWORK_HTTPThe check on package HTTP toolsenforce
SNIPPBOT_PACKAGE_NETWORK_OSThe operating-system network lock for package programs with no network permission (macOS and containers)shadow
SNIPPBOT_PACKAGE_EGRESS_PROXYThe egress proxy for packages allowed a host list. off means a host list allows any host, as beforeshadow

SNIPPBOT_PACKAGE_OS_SANDBOX=off turns off the whole package sandbox (the file protections for package tools, page backends and MCP servers too), and with it the network lock and the egress proxy; Settings warns when it is set.

Enable debug logging for the auth subsystem:

Terminal window
SNIPPBOT_DEBUG=1 snippbot start 2>&1 | grep -i 'auth\|unauthorized\|failed'

Review the access log periodically:

Terminal window
grep '401\|403' ~/.snippbot/logs/daemon.log | tail -50