# Security Policy ## Supported Versions | Version | Supported | |---------|--------------------| | main | :white_check_mark: | ## Reporting a Vulnerability If you discover a security vulnerability in ToolMesh, please report it responsibly. **Do NOT open a public GitHub issue for security vulnerabilities.** Instead, please email: **security@dunkel.cloud** You will receive an acknowledgment within 48 hours, and we aim to provide a detailed response within 5 business days. ### What to include - Description of the vulnerability - Steps to reproduce - Potential impact - Suggested fix (if any) ### Scope The following are in scope: - Authentication/authorization bypass - Credential leakage through the Output Gate - Injection attacks via tool parameters - Policy engine (goja) sandbox escapes The following are out of scope: - Vulnerabilities in upstream dependencies (report to the dependency maintainer) - Vulnerabilities in external MCP servers connected via MCPAdapter - Denial of service through legitimate API usage ## Security Architecture ToolMesh follows a security-by-default design: - **Fail-Closed:** Credential lookup failures abort the request - **Output Gate:** Every tool result passes through the policy engine - **Credential Isolation:** Secrets are injected at runtime, never exposed in prompts - **Audit Trail:** Every tool execution is recorded via Temporal workflow history - **Authorization on by default:** Every tool call is checked by OpenFGA, built into ToolMesh (user, role, plan or company -> tool). Switching it off takes `TOOLMESH_AUTHZ_MODE=disabled`, which is reported at every start and recorded in the audit trail. - **Authentication required:** With no `TOOLMESH_AUTH_PASSWORD`/`TOOLMESH_API_KEY` (or `users.yaml`/`apikeys.yaml`), every request is rejected — the server is never open by default. - **Login throttling:** Failed password logins are limited per account and per client address, logged, and counted; an unknown username costs the same work and gets the same answer as a wrong password. - **Bounded password hashing:** The number of bcrypt comparisons running at once is limited for the whole process. API keys are found by lookup and an access token never costs a comparison. Only API key entries that are configured as a bcrypt hash and have not been used since startup are still compared, one request at a time; configuring them with `key_sha256` removes that. - **Unauthenticated requests are answered `401`** with a `WWW-Authenticate` challenge on `/mcp`, logged, and counted. - **Safe defaults:** Logging defaults to `info` (no request payloads); the unauthenticated metrics port is bound to loopback by the Compose file; caller-supplied `file_url` fetches fail closed against private/internal addresses. - **Composite children checked:** Nested `api.*` calls inside a composite go through authorization and the pre-execution gate; a REST adapter without that guard refuses them, also after a reload of `backends.yaml`. - **Credentials bound to their backend:** A DADL reads only the credentials in the namespace of its `backends.yaml` entry (the longest matching entry name owns a credential; plus what the entry lists) and sends them only to its `base_url` host and port, its registrable domain and the entry's `allowed_hosts`, over https when `base_url` is https; a DADL that reaches further is refused at load, and every request is checked again. - **Credentials stay with their backend:** A redirect to another origin never carries injected credentials or the `Referer`, and is never followed from `https` to `http`; token and login requests follow no redirects and use the backend's connection rules; tool paths and the session login URL cannot leave the host and path of `base_url`. - **No secrets in error texts:** Transport errors name only scheme and host of a request, so a query-injected credential reaches neither the audit log nor a tool result. - **Typed arguments before the gate:** Parameters of REST tools are checked against their declared DADL types before any policy sees them. - **Explicit OAuth consent:** The login page names the host that receives the authorization and requires a confirmation; `TOOLMESH_OAUTH_REDIRECT_HOSTS` can restrict redirect hosts. Token refresh and every access token are resolved against the current user configuration, and `/revoke` revokes tokens (RFC 7009). - **Client address from trusted proxies only:** `X-Forwarded-For` is honored only from `TOOLMESH_TRUSTED_PROXIES`; failed API keys and client registrations are limited per address, also without Redis. - **Bounded code execution:** `execute_code` runs are limited in time, tool calls, stack depth and number in progress (`TOOLMESH_CODE_MAX_CONCURRENT`, at most half of it per user). Known limit: the JavaScript runtime has no memory limit of its own, so a single run can use as much memory as the process may; run ToolMesh with a container memory limit. ### Startup security posture No default silently relaxes a control. At boot ToolMesh logs a single **security-posture summary** listing every control still in a relaxed state (missing auth credential, authorization disabled or open to everyone, open CORS, debug tools, login limits switched off) with a remediation hint. These are logged at `WARN` in the default production posture; set `TOOLMESH_DEV=true` on a development machine to report the same facts once at `INFO`. `TOOLMESH_DEV` changes only the log level of this summary — it never relaxes a setting. For details, see [docs/architecture.md](docs/architecture.md).