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”API key (NB_API_KEY)
Section titled “API key (NB_API_KEY)”Set the NB_API_KEY environment variable to enable authentication. Every request (except /v1/health, /v1/bundles/health, and the login endpoint) must include this key as a Bearer token or session cookie.
export NB_API_KEY=change-me-to-a-strong-secret-at-least-32-charsKey requirements:
| Constraint | Behavior |
|---|---|
| Fewer than 8 characters | Server refuses to start with NB_API_KEY is too short (minimum 8 characters) |
| 8—15 characters | Server starts but logs a warning: WARNING: NB_API_KEY is short (<16 chars) |
| 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 the API key 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
Section titled “Key rotation”To rotate the API key:
-
Generate a new key:
Terminal window openssl rand -base64 32 -
Update the
NB_API_KEYenvironment variable in your deployment (.envfile or Docker Compose config). -
Restart the platform container. All existing sessions become invalid — users 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.
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/bash", "protected": true } ]}A protected bundle cannot be removed via nb__manage_bundle. 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 |
|---|---|
NB_API_KEY set to 32+ characters | Check your .env file |
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 |