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
- Set
NODE_ENV=production. It turns on the stricter rate limit and file logging. - Set
JWT_SECRETto a long random value (see JWT_SECRET) andchmod 600 .env. - Put LiteBot behind a TLS reverse proxy. Block
PORTin the firewall for everyone except the proxy. - Create a named database admin for each operator, then clear
ADMIN_PASSWORD_HASH. - Give each integration its own API key with the smallest role that works. Never give an integration an
adminkey. - Set
AUTO_DIALER_WEBHOOK_SECRETif the dialer is on. - Set
LOG_LEVEL=info(the built-in default isdebug, which logs message text). - On the proxy, refuse
/api/websocket/statsand/api/visual-builderfrom the internet, and restrict the dashboard and/api/adminto your office or VPN if you can. - Protect MongoDB and Redis with passwords, on loopback or a private network.
- 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
PORTdirectly. Without a proxy, a client setsX-Forwarded-Foritself 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 withDELETE /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 andMath.random, not a cryptographic generator. A caller may also supply its ownsessionId. If session ids must be unguessable, generate them yourself from a secure random source and pass them in. - The
contextobject a caller sends is stored in the session and visible to tools. Never let a tool trust a value fromcontext(a phone number, a customer id, a price) without checking it. - Messages longer than
MAX_MESSAGE_LENGTHcharacters (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_TTLseconds (default 60). Role changes take up toROLE_CACHE_TTLseconds (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_INshort (the default is24h) 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 theAuthorizationheader. - An API key with role
adminis unrestricted across all tenants. Give integrationstenant-adminor 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.envin 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 inALLOWED_TEMPLATE_ENV_VARS(defaultNODE_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/:campaignIdchecksX-Webhook-Signature, an HMAC-SHA256 of the JSON body withAUTO_DIALER_WEBHOOK_SECRET. If the secret is unset, the route accepts unsigned requests. Set it.POST /api/campaigns/callback/:campaignId/:contactId/:tokencarries an HMAC of the campaign and contact in the URL, keyed byAUTO_DIALER_WEBHOOK_SECRET, orJWT_SECRETif 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=falseif 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
- Users, roles and tenants: roles, API keys and tenant isolation.
- Operations: running the binary under pm2, logs and backups.
- Environment reference: every variable named here.
- Troubleshooting: 401, 403 and 429 answers.