LiteBot

Security

This chapter lists what LiteBot exposes, what each part trusts, and the settings that lock it down. Work through Hardening checklist on every new install, then read the sections for the parts you use. Several gaps are noted honestly; plan around them until they are fixed.

What is exposed

LiteBot listens on one TCP port (PORT; the shipped .env.example sets 3345, the built-in fallback is 3000) on all interfaces. There is no setting to bind it to one address, so the firewall and the reverse proxy decide who reaches it.

Path Auth Who must reach it
POST /api/chat, POST /api/chat-simple None Your website, app, WhatsApp bridge or voice gateway
GET / DELETE /api/chat/session/:id None The same callers, to read or end a session
WebSocket /chat None Web chat widgets, the dashboard
/health, /health/ready None Your monitoring and pm2 checks
GET /api/websocket/stats None Nobody outside: connection counts only
/api/visual-builder/* (when ENABLE_VISUAL_EDITOR=true) None Nobody outside: templates and flow validation only
POST /api/campaigns/webhook/:campaignId, /api/campaigns/callback/... (when ENABLE_AUTO_DIALER=true) Signature Your voice gateway
WebSocket /voice JWT or API key in the URL Browsers running voice sessions
The dashboard (/) and every other /api/* route JWT or API key Operators and integrations

LiteBot also connects out to your LLM providers, embedding provider, MongoDB, Redis (if used), Milvus (if used), and the URLs your tools and session-end hooks call.

Hardening checklist

  1. Set NODE_ENV=production. It turns on the stricter rate limit and file logging.
  2. Set JWT_SECRET to a long random value (see JWT_SECRET) and chmod 600 .env.
  3. Put LiteBot behind a TLS reverse proxy. Block PORT in the firewall for everyone except the proxy.
  4. Create a named database admin for each operator, then clear ADMIN_PASSWORD_HASH.
  5. Give each integration its own API key with the smallest role that works. Never give an integration an admin key.
  6. Set AUTO_DIALER_WEBHOOK_SECRET if the dialer is on.
  7. Set LOG_LEVEL=info (the built-in default is debug, which logs message text).
  8. On the proxy, refuse /api/websocket/stats and /api/visual-builder from the internet, and restrict the dashboard and /api/admin to your office or VPN if you can.
  9. Protect MongoDB and Redis with passwords, on loopback or a private network.
  10. Rotate provider keys and API keys when someone who knew them leaves.

Check from outside after you finish:

nmap -p 3345 <litebot-public-ip>
curl -s https://bots.example.com/api/websocket/stats

The port should show as filtered, and the stats call should be refused by the proxy.

Reverse proxy and TLS

LiteBot speaks plain HTTP and WebSocket. Terminate TLS in a reverse proxy (nginx, Caddy, a cloud load balancer or a tunnel) and forward to 127.0.0.1:3345. The proxy must pass WebSocket upgrades for /chat and /voice. Browsers need https:// for the microphone, so voice in the dashboard needs TLS anyway.

LiteBot trusts exactly one proxy hop (trust proxy is fixed at 1). It reads the client address from the last entry of X-Forwarded-For, and uses that address for rate limits and the API-key failure counter. Two consequences:

  • Run exactly one proxy in front. Behind two, every client looks like your first proxy and shares one rate-limit bucket.
  • Never expose PORT directly. Without a proxy, a client sets X-Forwarded-For itself and picks a fresh address for every request, which defeats the rate limit.

Public chat endpoints

POST /api/chat and the /chat WebSocket are public by design, so a website can talk to a bot without a key. Understand what that allows:

  • Any caller can talk to any active bot by naming its botId, in any tenant. Bot ids are not secrets. Do not rely on an unpublished bot id to hide a bot; set the bot inactive instead.
  • Any caller who knows a session id can read that session's summary and context with GET /api/chat/session/:id, and end it with DELETE /api/chat/session/:id, which runs its session-end hooks. Treat session ids as secrets: do not put them in URLs that get shared or logged.
  • Session ids generated by LiteBot (sess_...) are built from the time and Math.random, not a cryptographic generator. A caller may also supply its own sessionId. If session ids must be unguessable, generate them yourself from a secure random source and pass them in.
  • The context object a caller sends is stored in the session and visible to tools. Never let a tool trust a value from context (a phone number, a customer id, a price) without checking it.
  • Messages longer than MAX_MESSAGE_LENGTH characters (default 4000) get 400.

If your bots serve only your own systems, put an authenticating proxy rule in front of /api/chat and /chat (an IP allow-list or a shared header your proxy checks).

Rate and size limits

Limit Value Setting
Requests per client address on /api/* 100 per 15 minutes in production; 500 per 5 minutes otherwise API_RATE_LIMIT sets the count; the window is fixed
Exempt from that limit /api/collections/*, /api/documents/*; /health and WebSockets are outside it none
Messages per /chat connection 30 per minute fixed
JSON request body 10 MB fixed
Document upload, campaign CSV 10 MB per file fixed
/chat WebSocket frame 1 MB fixed
/voice WebSocket frame 4 MB fixed
Bad API keys per address 20 per 60 s, then refused API_KEY_FAIL_LIMIT, API_KEY_FAIL_WINDOW_MS

A rate-limited request gets 429 with RateLimit-* headers. Login has no lockout of its own; it shares the /api rate limit. The /chat limit is per connection, so a client can reconnect to reset it: add a per-address connection limit in the proxy if web chat is public.

Remember that the limit counts every API call, including the dashboard's. Raise API_RATE_LIMIT if a busy operator's dashboard starts getting 429.

Browser headers and CORS

LiteBot sends security headers through helmet, including a Content-Security-Policy that allows scripts, styles and connections from its own origin only, plus ws: and wss:. Add origins the dashboard must connect to in CSP_CONNECT_ORIGINS (comma-separated).

CORS is open: any origin may call the API from a browser. There is no setting to restrict it. Because authenticated calls carry a bearer token or API key rather than a cookie, another site cannot borrow a signed-in operator's session; the open CORS matters mainly for the public chat endpoints. If you need an origin list, enforce it in the proxy.

/api/* responses are sent with Cache-Control: no-store.

JWT_SECRET

JWT_SECRET signs every dashboard token. LiteBot refuses to start if it is unset or shorter than 32 characters. Generate one with:

openssl rand -hex 32

It also does more than sign tokens:

Uses JWT_SECRET Effect of changing it
Access and refresh tokens Everyone is logged out
Encryption key for tenant LLM keys Every stored tenant key becomes unreadable; set them again
Auto-dialer callback signatures, when AUTO_DIALER_WEBHOOK_SECRET is unset Callback URLs of calls in progress stop verifying

So rotate it deliberately: plan to re-enter tenant provider keys afterwards, and set AUTO_DIALER_WEBHOOK_SECRET separately so the dialer does not depend on it. Anyone holding JWT_SECRET can mint an admin token: keep it out of tickets, chat and backups that leave your control.

Tokens and API keys

  • API keys and passwords are stored as scrypt hashes; a database dump does not reveal them. Keys are shown once, at creation (API keys).
  • A revoked key keeps working for up to API_KEY_CACHE_TTL seconds (default 60). Role changes take up to ROLE_CACHE_TTL seconds (default 30).
  • API-key expiry (expiresAt) is stored but not enforced. Revoke keys by hand.
  • A JWT stays valid until it expires even if you deactivate the user. Keep JWT_EXPIRES_IN short (the default is 24h) if you must be able to cut access quickly.
  • HTTP routes also accept a JWT as ?token= (for event streams). The request log records full URLs, so a token passed this way lands in the log. Prefer the Authorization header.
  • An API key with role admin is unrestricted across all tenants. Give integrations tenant-admin or a custom role.

Voice WebSocket

/voice opens a Gemini Live session, which costs money per minute, so it requires a credential before the upgrade completes:

wss://bots.example.com/voice?token=<token>
wss://bots.example.com/voice?api_key=lbk_...

A missing or invalid credential gets HTTP 401 and no session is started. Limits:

Variable Default Meaning
MAX_VOICE_SESSIONS_CONCURRENT 10 Voice sessions per process
MAX_VOICE_SESSIONS_PER_USER 1 Concurrent sessions per dashboard user (JWT)
API_KEY_DEFAULT_MAX_VOICE_CONCURRENT 50 Concurrent sessions per API key. With a key, one caller (userId) may hold one session at a time
MAX_VOICE_SESSION_DURATION_MS 900000 Longest session (15 minutes)
VOICE_SESSION_IDLE_TIMEOUT_MS 300000 Session closed after this long without frames

Known gap: /voice checks that the credential is valid but not that the bot belongs to the credential's tenant. Any valid key can open a voice session with any voice-enabled bot. Until this is fixed, do not give tenants API keys on a server where another tenant's voice bots must stay private.

A credential in a URL can appear in proxy access logs. Turn off query-string logging for /voice on the proxy, and use short-lived JWTs in browsers rather than embedding an API key in a public page.

Encryption at rest

Data Stored as
Dashboard passwords, API keys scrypt hash (32-byte salt, 64-byte key)
Tenant LLM provider keys AES-256-GCM, key derived from JWT_SECRET. Only the last 4 characters are ever returned
Platform provider keys Plain text in .env
Bot configurations, prompts, tool source Plain text in MongoDB and config/
Conversations, traces, leads, tickets Plain text in MongoDB

Protect .env, config/ and the MongoDB data and backups with file permissions and disk encryption where your rules require it.

Tools

Tools are code that runs inside the LiteBot process. Anyone who can write a tool can affect every bot on the server.

Kind Where What it can reach
Disk tool config/tools/*.js Everything the process can: files, network, process.env. Written by a global admin
Global database tool (*) tools collection Runs in a restricted context, but receives the database connection, knowledge base, vector database, axios, crypto, the logger and the LLM through context.modules
Tenant database tool tools collection Restricted context with axios, crypto, the logger, the tenant's own knowledge base and the LLM (tenant-pinned)

Database tools are compiled in a separate context with no process, fs, child_process or global. require works only for axios, joi, crypto, https, http, url, querystring, uuid and crypto-js, and loading the file must finish within 1 second. This guards against careless code, not hostile code: it is not a security boundary. Give tools:create and tools:update only to people you would trust with the server.

Other rules:

  • Do not hard-code secrets you cannot rotate easily. Tool source is stored in plain text, is visible to everyone who can read tools, and is included in bot exports for tools in the exporter's scope. Keep a webhook token in a file constant only when it is scoped to that one webhook and easy to change.
  • Do not read process.env in tools. Tenant tools cannot, and a global tool that does could leak platform keys into a reply.
  • Any signed-in user can run a tool through POST /api/tools/:name/test. A tool that writes data (bookings, tickets, messages) will write it from a test call too.
  • Bot templates can read environment variables with {{ENV.NAME}} only for names in ALLOWED_TEMPLATE_ENV_VARS (default NODE_ENV,APP_NAME,SITE_URL,SUPPORT_EMAIL). Never add a key name to that list.

Admin endpoints

/api/admin/* and /health/detail require the admin role. They can hot-swap provider keys, read the bot, prompt and tool files, re-initialise providers and show the masked environment. Bot and tool reloads (POST /api/bots/reload, POST /api/tools/reload) are admin-only too.

Keep the number of admin accounts small, and prefer tenant-admin for customers' staff. If you can, allow /api/admin only from your office or VPN at the proxy.

Auto-dialer callbacks

When ENABLE_AUTO_DIALER=true, two public routes accept calls from your voice gateway:

  • POST /api/campaigns/webhook/:campaignId checks X-Webhook-Signature, an HMAC-SHA256 of the JSON body with AUTO_DIALER_WEBHOOK_SECRET. If the secret is unset, the route accepts unsigned requests. Set it.
  • POST /api/campaigns/callback/:campaignId/:contactId/:token carries an HMAC of the campaign and contact in the URL, keyed by AUTO_DIALER_WEBHOOK_SECRET, or JWT_SECRET if that is unset.

See Auto-dialer.

Data LiteBot keeps

Data Where Holds How long
Conversations conversations Full transcripts, session context, tool results, caller ids and phone numbers Until you delete them
Request traces request_traces Per-turn tool arguments and results, LLM timings 30 days, once the traces endpoint has been used (see below)
Analytics session_analytics, request_analytics Usage, cost, timings per session and request Until you delete them
Leads, tickets leads, support_tickets What your tools write: names, phones, emails Until you delete them
Knowledge base knowledge_documents, vector files or Milvus Uploaded document text Until you delete them
Live sessions Redis, if used Active session state REDIS_SESSION_TTL seconds (default 3600)
Logs Console (pm2 logs); with NODE_ENV=production also LOG_FILE Request lines with URL and client address; at debug, message previews Your log rotation

Only request_traces has automatic expiry, and its 30-day index is created the first time someone opens a session's traces (GET /api/conversations/:id/traces or the Traces tab). Open one once after install to create it. Nothing else expires on its own.

To reduce what is kept:

  • Delete old conversations from the Conversations page or with POST /api/conversations/bulk-delete, on a schedule that matches your data rules.
  • Set ENABLE_REQUEST_TRACING=false if you do not need per-turn traces.
  • Rotate pm2 logs and log files, and restrict who can read them.
  • Do not put data you do not need into session context; it is stored with the conversation.

Caller text goes to your LLM provider, and uploaded documents go to your embedding provider. Each provider's terms decide how long it keeps them. A local Ollama model keeps conversations on hardware you control.

Where to go next