Skip to content

Security: tsouth89/toolport

SECURITY.md

Security

Toolport is a local-first MCP gateway and manager. This document describes how it handles secrets, network boundaries, and trust across each way it runs, what it records locally, and how to report a vulnerability.

Reporting a vulnerability

Please report security issues privately, not as a public issue:

Please include reproduction steps and the affected version/commit. We aim to acknowledge within a few days. Coordinated disclosure is appreciated; we will credit reporters who want it.

Supported versions

Toolport ships fixes on the latest release only. Always run the newest version from the Releases page.

How Toolport runs, and where the boundaries are

Toolport's core is a local gateway that AI clients reach; the desktop app is a manager around it. What network surface exists depends on the mode you run:

  • Desktop app + stdio gateway (the default). Each AI client spawns the gateway as a local child process and talks to it over stdio (standard input/output). In this mode the gateway binds no listening network port at all. The only socket it ever opens is a transient 127.0.0.1 loopback listener during an OAuth callback, which closes as soon as that flow completes.
  • Local HTTP / OpenAPI bridge (TOOLPORT_HTTP, legacy CONDUIT_HTTP). For clients that speak HTTP or OpenAPI instead of stdio, the gateway can bind a listener. It defaults to 127.0.0.1:8765 (loopback only), configurable with TOOLPORT_HTTP_HOST and the port. Every bind requires HTTP authentication: either a bearer token (TOOLPORT_HTTP_TOKEN; legacy CONDUIT_HTTP_TOKEN still accepted) or at least one registered HTTP client. With neither, the process prints a line to stderr and exits 1, including a hand-launched loopback listener. A registered HTTP client is a scoped bearer you create in the desktop app under Settings, "Open WebUI / HTTP endpoint", "Scoped clients": each entry gets its own token and its own set of servers, and the app stores only the token's SHA-256 in the http_clients list in registry.json in Toolport's local data directory (the plaintext token is shown once, at creation, and is not stored). One or more of those entries counts as authentication configured, exactly like an env token. When the desktop app starts this bridge it auto-generates a token. A request without a valid token gets 401, and any cross-site browser request is rejected with 403 regardless of token. For isolated local development only, --insecure-loopback permits an unauthenticated loopback listener, but only when no token is set and http_clients is empty. Registered clients left over from earlier use keep the bind authenticated, so the listener still answers 401 to every request without a matching bearer; revoke those entries first if you want the open listener. An empty http_clients list only counts when the registry was actually read: a registry recovered from a backup, or one the gateway could not read at all, is empty for a reason that says nothing about what is configured, so the flag does not open a listener over it. The flag never permits an open non-loopback bind.
  • Headless / Docker. The published image runs the HTTP bridge and sets TOOLPORT_HTTP_HOST=0.0.0.0 so it is reachable off-host. Every bind, including loopback, requires TOOLPORT_HTTP_TOKEN (legacy CONDUIT_HTTP_TOKEN) or a registered HTTP client: without one the gateway refuses to start. --insecure-loopback cannot open a non-loopback address. Put it behind your own TLS/ingress; the gateway serves plain HTTP and expects the operator to terminate TLS.
  • Sharing and Teams (opt-in, hosted). These make explicit, user-initiated requests to hosted services and are covered under Telemetry and hosted services below.

Secrets

API keys and OAuth tokens are never written to plaintext config files and never written to logs. Where they live depends on the backend, selected by environment:

  • OS keychain (desktop default). With no secret-related env var set, secrets go to the platform keychain: the macOS data-protection keychain (under a team-scoped access group shared with the signed gateway), Windows Credential Manager, or the Linux Secret Service.
  • Encrypted file backend (TOOLPORT_SECRET_KEY, legacy CONDUIT_SECRET_KEY). Setting this env var switches storage to an encrypted secrets.enc file (XChaCha20-Poly1305) in Toolport's data directory, keyed from the passphrase. This is the backend for headless and containerized deployments where no OS keychain is available. The passphrase is the whole key: choose a high-entropy value and keep it out of shell history and image layers. If secrets.enc leaks together with a weak passphrase, the secrets are recoverable, so treat the file as sensitive.
  • Environment injection (headless). A secret can also be supplied directly as TOOLPORT_SECRET_<KEY> (legacy CONDUIT_SECRET_<KEY>), or as a bare <KEY> only when TOOLPORT_ALLOW_BARE_SECRET_ENV (legacy CONDUIT_ALLOW_BARE_SECRET_ENV) is set. These are read from the process environment in cleartext, so they are only as protected as the environment that holds them.

Authentication and OAuth 2.1

Downstream OAuth is done conservatively: dynamic client registration, PKCE (S256), a CSRF state check, a loopback (127.0.0.1) redirect, RFC 8707 resource binding, and offline_access refresh tokens. Discovered authorization, token, and registration endpoints must be https:// (loopback http is allowed only for local development).

Tool governance and enforcement

Toolport is detection-first by default, but it is not detection-only: several controls actively gate or block a call before it reaches an upstream server.

  • Destructive-tool policy. With the destructive-tool deny policy on, tools judged destructive (by their destructiveHint, or a write-verb name heuristic) are hidden from the catalog and blocked before a client can call them.
  • Confirm-before-destructive. A softer mode intercepts the first call to a destructive tool, returns a preview, and only routes it after an explicit toolport_confirm.
  • Quarantine on drift. When an already-approved tool changes in a high-risk way (poisoned description, or a destructive/annotation downgrade), it is quarantined and blocked until you re-approve it.
  • Human-in-the-loop approval. When enabled, destructive tools and tools from untrusted-provenance servers (shared/registry sources) require an explicit human approval. This gate is fail-closed: a denied, timed-out, or unreachable decision blocks the call, which returns an error and never routes.
  • Content provenance labeling. Flagged tool results and resource reads are wrapped with a provenance marker telling the model the block is external data, not instructions. This labels and fences the content; by design it does not drop or block it, so the model still receives it, clearly marked.

Which of these are active depends on your settings and, for Teams, your org policy. Debug logging is off by default, gated behind TOOLPORT_DEBUG (legacy CONDUIT_DEBUG), and never records tokens or full authorization URLs.

Telemetry and hosted services

In local use, Toolport sends no telemetry. Routine tool traffic is only between the gateway and the upstream MCP servers you configure. Data leaves your machine only through features you explicitly turn on:

  • Teams usage reporting (opt-in, only when joined to a team). When connected to a Toolport Teams organization, a periodic sync reports aggregate usage for team-provided servers to the team server URL you joined: per-server row of call count, tokens saved, and estimated cost. It never sends tool names, arguments, results, or anything about your personal (non-team) servers.
  • Shared setup links. Creating a share link POSTs a secret-stripped server setup to https://toolport.app/api/share, and only when you take that action. Importing a shared link fetches it from the same endpoint.
  • Team server URL. You provide the Teams server address when you join; there is no hardcoded default, and non-loopback team URLs must be https://.

What Toolport records locally, and how to clear it

Everything below stays in Toolport's local data directory (%APPDATA%\Toolport on Windows, the platform config dir elsewhere; override with TOOLPORT_DATA_DIR, legacy CONDUIT_DATA_DIR) and never leaves your device on its own. Each log is capped and trims oldest-first:

Local record File Retained Contains
Audit log audit.jsonl last ~5,000 entries (or 4 MB) tool calls and approval/policy decisions
Discovery search trace search-trace.jsonl last ~500 lazy-discovery searches the agent ran
Live inspector inspect.jsonl last ~50 (opt-in, off by default) captured call arguments and results
Savings log savings.jsonl last ~2,000, plus a carry-forward total token-savings tallies (a running aggregate survives trimming)

Registry config, secrets, tool catalogs, and these logs never leave the device except through the opt-in hosted features above.

Clear controls: the Activity view has a confirmed Clear retained activity action that deletes all four logs; the live inspector also clears itself when you turn live inspection off. Both are local, irreversible deletes.

Trust model and your responsibilities

Toolport proxies to whatever MCP servers you add. It does not vet the behavior of third-party servers: an upstream server you configure runs as a local process (for stdio servers) and/or receives the credentials you give it. Only add servers you trust. Lazy discovery reduces how much surface is exposed to the agent, and the governance controls above can gate a call, but a tool you approve still executes upstream.

When you run Toolport outside the desktop app, some safety defaults become your responsibility: configure HTTP authentication for every bind, including loopback, by setting TOOLPORT_HTTP_TOKEN (legacy CONDUIT_HTTP_TOKEN) or by registering an HTTP client in the desktop app (both are described under How Toolport runs above), then choose a high-entropy TOOLPORT_SECRET_KEY (legacy CONDUIT_SECRET_KEY) for the encrypted file backend, and put the HTTP bridge behind your own TLS. --insecure-loopback waives the requirement for isolated local development only, and only while neither a token nor a registered client exists.

Known issues

See the Known issues section of the README for current advisories, including the Linux-only glib RUSTSEC-2024-0429 soundness issue inherited transitively from Tauri's Linux webview stack.

There aren't any published security advisories