Skip to content

Security

NimbleBrain exposes an HTTP API that controls an agent with tool execution capabilities. A misconfigured deployment can give unauthorized users shell access to your server. This page covers every security surface you should address before exposing NimbleBrain to a network.

NimbleBrain uses pluggable authentication adapters configured via instance.json in the work directory. The available adapters are:

AdapterUse caseConfiguration
devLocal developmentNo instance.json (default)
oidcEnterprise / SSOAuthorization-code flow against any OIDC provider
workosEnterprise / SSO via WorkOSAuthKit-hosted login (adapter: "workos")

The adapter is selected by the auth.adapter field in instance.json. With no instance.json present, the platform runs in dev mode.

For the OIDC and WorkOS adapters, sign-in is an authorization-code flow, not a login form that the platform posts credentials to. There is no POST /v1/auth/login endpoint. The browser flow is:

  1. The web client redirects to GET /v1/auth/authorize, which sends the user to the configured identity provider.
  2. The provider authenticates the user and redirects back to GET /v1/auth/callback with an authorization code.
  3. The callback exchanges the code for tokens and sets the nb_session cookie (see Session cookies below). It does not validate a shared secret — the cookie carries the provider-issued access token.
  4. Subsequent requests present either the nb_session cookie or an Authorization: Bearer <token> header. Programmatic callers (CLI, external MCP clients) use the bearer header; the browser uses the cookie.

POST /v1/auth/refresh rotates an expiring session, and POST /v1/auth/logout clears it. These four routes (authorize, callback, refresh, logout) are the entire auth surface — there is no login route and no NB_API_KEY.

Every failed authentication attempt is logged to stderr with the client IP in a structured field:

[auth] authentication failed { ip: "203.0.113.42" }

The IP comes from the X-Forwarded-For header (set by your reverse proxy) or "direct" for direct connections. Monitor these logs for brute-force attempts.

The OIDC/WorkOS callback (GET /v1/auth/callback) sets the nb_session cookie after a successful authorization-code exchange. The web UI uses this cookie for all subsequent same-origin requests. No login endpoint is involved — the cookie value is the provider-issued access token, not a platform-minted secret.

AttributeValuePurpose
Namenb_sessionIdentifies the session
HttpOnlyAlways setPrevents JavaScript access — mitigates XSS token theft
SameSiteLaxCookie sent on same-site requests and top-level navigations — mitigates CSRF
SecureSet when not localhostCookie only sent over HTTPS
Path/Available to all routes
Max-Age3600 (1 hour)Session expires after one hour; refreshed via POST /v1/auth/refresh

The Secure flag is automatically omitted for localhost connections (where http:// is expected) and added for all other hosts.

POST /v1/auth/logout clears the cookie by setting Max-Age=0. The client should discard any cached auth state.

Cross-Origin Resource Sharing controls which domains can make API requests from a browser. NimbleBrain has three CORS modes depending on your configuration. The deciding factor is whether auth is configured (an instance.json adapter is present), not any single environment variable:

When no instance.json adapter is configured (dev mode):

  • Access-Control-Allow-Origin: *
  • No credentials support
  • Any origin can make requests

Mode 2: Auth configured, no ALLOWED_ORIGINS

Section titled “Mode 2: Auth configured, no ALLOWED_ORIGINS”

When an auth adapter is configured but ALLOWED_ORIGINS is not:

  • No Access-Control-Allow-Origin header is sent
  • Only same-origin requests work (browser enforces this)
  • Cross-origin requests from any domain are blocked

Mode 3: Auth configured + ALLOWED_ORIGINS (production)

Section titled “Mode 3: Auth configured + ALLOWED_ORIGINS (production)”

When both are set:

  • Access-Control-Allow-Origin is set to the requesting origin only if it appears in the allow list
  • Access-Control-Allow-Credentials: true enables cookie-based auth
  • Vary: Origin ensures caches differentiate by origin
Terminal window
export ALLOWED_ORIGINS=https://nb.example.com,https://admin.example.com

These headers are always permitted in CORS requests:

Content-Type, Authorization, Mcp-Session-Id, Last-Event-ID, Mcp-Protocol-Version, X-Workspace-Id

These headers are exposed to client JavaScript:

Mcp-Session-Id, Mcp-Protocol-Version

The platform service (port 27247) should never be directly exposed to the internet. The platform runs on an internal Docker network:

  • The platform has no ports mapping. Only the web container (Caddy) is exposed on port 27246. Caddy proxies /v1/* to the platform.
Internet → Reverse Proxy (TLS) → Web (27246) → Platform (27247, internal)
Serves UI + proxies /v1/*

If your host is directly on the internet, restrict inbound traffic:

Terminal window
# Allow only HTTPS (443) and SSH (22)
ufw allow 22/tcp
ufw allow 443/tcp
ufw deny 27246/tcp # Block direct access if behind a reverse proxy
ufw enable

NimbleBrain does not terminate TLS itself. Use a reverse proxy for HTTPS:

nginx.conf
server {
listen 443 ssl http2;
server_name nb.example.com;
ssl_certificate /etc/ssl/certs/nb.example.com.pem;
ssl_certificate_key /etc/ssl/private/nb.example.com-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# HSTS — only enable once you confirm TLS works
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:27246;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for SSE streaming
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
}
}

The /mcp endpoint and /.well-known/oauth-* endpoints advertise a resource URL in their OAuth metadata. Clients validate this value against the URL they connected to and reject responses whose scheme doesn’t match (e.g. client connected via https://…/mcp but the server returned resource: http://…).

NimbleBrain derives the scheme from the X-Forwarded-Proto header, falling back to the raw request scheme. When you terminate TLS at an upstream proxy, that proxy must set and preserve X-Forwarded-Proto: https end-to-end — otherwise OAuth discovery silently advertises http:// and modern MCP clients fail with:

Protected resource http://your-host does not match expected https://your-host/mcp

Works out of the box. The ALB sets X-Forwarded-Proto based on the actual client→ALB connection scheme, and the bundled nimblebrain-web Caddy container trusts forwarded headers from RFC 1918 sources:

web/Caddyfile (shipped)
{
servers {
trusted_proxies static private_ranges
}
}

No additional config needed. Ensure the ALB targets nimblebrain-web, not the platform service directly.

After deploying, confirm the advertised scheme matches the client-facing URL:

Terminal window
curl -s https://your-host.example.com/.well-known/oauth-protected-resource | jq .resource
# must be "https://your-host.example.com"

If it still returns http://, walk the proxy chain to find where X-Forwarded-Proto is being stripped or overwritten.

MCP bundles run as subprocesses with access to the filesystem and network inside the container. Evaluate bundles before installing them.

Each bundle in the mpak registry has an MTF (mpak Trust Framework) trust score from 0 to 100. The score is stored in nimblebrain.json alongside the bundle entry and displayed in the web UI’s app list.

Bundles run as child processes inside the platform container. They share the container’s filesystem, network namespace, and user. To limit blast radius:

  • Use read-only bind mounts for configuration files (the default docker-compose.yml already does this with :ro)
  • Set specific environment variables per bundle in nimblebrain.json rather than passing broad credentials to the platform container
  • Review a bundle’s manifest before installing it — check what tools it exposes and whether it requires network access

Use this checklist before exposing NimbleBrain to any network:

ItemHow to verify
Authentication configuredinstance.json exists with auth.adapter set to oidc or workos
ALLOWED_ORIGINS set to your domain(s)curl -v with an Origin header and confirm the response includes Access-Control-Allow-Origin
Platform port (27247) not exposeddocker compose ps shows no host port mapping for platform service
TLS enabledcurl -I https://nb.example.com returns a valid certificate
X-Forwarded-For header set by proxyCheck auth failure logs for real IPs, not "direct"
Bundle list reviewedcat nimblebrain.json — only bundles you trust are listed
Firewall restricts inbound portsOnly 443 (HTTPS) and 22 (SSH) reachable from the internet
Backups configuredTest your volume backup and restore procedure