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.
Authentication
Section titled “Authentication”Auth adapters
Section titled “Auth adapters”NimbleBrain uses pluggable authentication adapters configured via instance.json in the work directory. Three adapters are available:
| Adapter | Use case | Configuration |
|---|---|---|
| dev | Local development | No instance.json needed (default) |
| local | Single-server deployments | Per-user API keys with bcrypt hashing |
| oidc | Enterprise / SSO | JWT verification via WorkOS or any OIDC provider |
Local adapter key requirements
Section titled “Local adapter key requirements”When using the local auth adapter:
| Constraint | Behavior |
|---|---|
| Fewer than 8 characters | Server refuses to start |
| 8—15 characters | Server starts but logs a warning |
| 16+ characters | No warnings. Use at least 32 characters for production |
Generate a strong key:
openssl rand -base64 32How authentication works
Section titled “How authentication works”The server accepts credentials in two ways, checked in this order:
- Bearer token in the
Authorizationheader:Authorization: Bearer <key> - Session cookie named
nb_session, set by the login endpoint
The comparison uses a constant-time XOR loop to prevent timing attacks. Both the token length and content must match exactly.
Failed authentication logging
Section titled “Failed authentication logging”Every failed authentication attempt is logged to stderr with the client IP and timestamp:
[nimblebrain] AUTH FAIL ip=203.0.113.42 timestamp=2026-03-25T22:00:00.000ZThe 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.
Key rotation (local adapter)
Section titled “Key rotation (local adapter)”To rotate an API key:
-
Generate a new key:
Terminal window openssl rand -base64 32 -
Update the user’s key via
nb__manage_usersor directly in the local key store. -
Restart the platform container. All existing sessions for that user become invalid — they must log in again with the new key.
Session cookies
Section titled “Session cookies”The /v1/auth/login endpoint validates the API key and sets a session cookie for browser-based access. The web UI uses this cookie for all subsequent requests.
Cookie attributes
Section titled “Cookie attributes”| Attribute | Value | Purpose |
|---|---|---|
| Name | nb_session | Identifies the session |
HttpOnly | Always set | Prevents JavaScript access — mitigates XSS token theft |
SameSite | Strict | Cookie only sent on same-site requests — mitigates CSRF |
Secure | Set when not localhost | Cookie only sent over HTTPS |
Path | / | Available to all routes |
Max-Age | 604800 (7 days) | Session expires after 7 days |
The Secure flag is automatically omitted for localhost connections (where http:// is expected) and added for all other hosts.
Logout
Section titled “Logout”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:
Mode 1: No API key (development)
Section titled “Mode 1: No API key (development)”When NB_API_KEY is not set:
Access-Control-Allow-Origin: *- No credentials support
- Any origin can make requests
Mode 2: API key set, no ALLOWED_ORIGINS
Section titled “Mode 2: API key set, no ALLOWED_ORIGINS”When NB_API_KEY is set but ALLOWED_ORIGINS is not:
- No
Access-Control-Allow-Originheader is sent - Only same-origin requests work (browser enforces this)
- Cross-origin requests from any domain are blocked
Mode 3: API key + ALLOWED_ORIGINS (production)
Section titled “Mode 3: API key + ALLOWED_ORIGINS (production)”When both are set:
Access-Control-Allow-Originis set to the requesting origin only if it appears in the allow listAccess-Control-Allow-Credentials: trueenables cookie-based authVary: Originensures caches differentiate by origin
export ALLOWED_ORIGINS=https://nb.example.com,https://admin.example.comAllowed headers
Section titled “Allowed headers”These headers are always permitted in CORS requests:
Content-Type, Authorization, Mcp-Session-Id, Last-Event-ID, Mcp-Protocol-VersionThese headers are exposed to client JavaScript:
Mcp-Session-Id, Mcp-Protocol-VersionNetwork architecture
Section titled “Network architecture”Keep the platform internal
Section titled “Keep the platform internal”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
portsmapping. Only thewebcontainer (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/*Firewall rules
Section titled “Firewall rules”If your host is directly on the internet, restrict inbound traffic:
# Allow only HTTPS (443) and SSH (22)ufw allow 22/tcpufw allow 443/tcpufw deny 27246/tcp # Block direct access if behind a reverse proxyufw enableNimbleBrain does not terminate TLS itself. Use a reverse proxy for HTTPS:
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; }}nb.example.com { reverse_proxy 127.0.0.1:27246}Caddy handles TLS automatically via Let’s Encrypt.
MCP OAuth behind a reverse proxy
Section titled “MCP OAuth behind a reverse proxy”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/mcpWorks 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:
{ servers { trusted_proxies static private_ranges }}No additional config needed. Ensure the ALB targets nimblebrain-web, not the platform service directly.
If you skip the bundled web container and front the platform yourself, propagate the header explicitly:
location / { proxy_pass http://nimblebrain-platform:27247; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}{ servers { trusted_proxies static private_ranges }}
your-host.example.com { reverse_proxy nimblebrain-platform:27247}trusted_proxies is only required if another proxy sits upstream of Caddy — otherwise Caddy sets X-Forwarded-Proto itself based on its listener scheme.
Verify
Section titled “Verify”After deploying, confirm the advertised scheme matches the client-facing URL:
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.
Bundle trust
Section titled “Bundle trust”MCP bundles run as subprocesses with access to the filesystem and network inside the container. Evaluate bundles before installing them.
Trust score
Section titled “Trust score”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.
Protected bundles
Section titled “Protected bundles”Mark bundles as protected in your configuration to prevent the agent from uninstalling them:
{ "bundles": [ { "name": "@nimblebraininc/ipinfo", "protected": true } ]}A protected bundle cannot be removed via nb__manage_app. You must edit the configuration file and restart to remove it.
Bundle isolation
Section titled “Bundle isolation”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.ymlalready does this with:ro) - Set specific environment variables per bundle in
nimblebrain.jsonrather 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
Production checklist
Section titled “Production checklist”Use this checklist before exposing NimbleBrain to any network:
| Item | How to verify |
|---|---|
| Authentication configured | instance.json exists with auth.adapter set to local or oidc |
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 exposed | docker compose ps shows no host port mapping for platform service |
| TLS enabled | curl -I https://nb.example.com returns a valid certificate |
X-Forwarded-For header set by proxy | Check auth failure logs for real IPs, not "direct" |
| Bundle list reviewed | cat nimblebrain.json — only bundles you trust are listed |
Critical bundles marked protected | Check for "protected": true on bundles you don’t want the agent to remove |
| Firewall restricts inbound ports | Only 443 (HTTPS) and 22 (SSH) reachable from the internet |
| Backups configured | Test your volume backup and restore procedure |