Security
Security considerations for deploying LLMRTC in production environments.
API Key Management
Storage
Never hardcode API keys. Use environment variables or a secrets manager:
# Environment variables
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export ELEVENLABS_API_KEY=xi-...
// Load from environment
const server = new LLMRTCServer({
providers: {
llm: new OpenAILLMProvider({
apiKey: process.env.OPENAI_API_KEY! // Never hardcode
})
}
});
Secret Managers
For production, use a secrets manager:
// AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
async function getSecrets() {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: 'llmrtc/api-keys' })
);
return JSON.parse(response.SecretString!);
}
const secrets = await getSecrets();
const server = new LLMRTCServer({
providers: {
llm: new OpenAILLMProvider({ apiKey: secrets.OPENAI_API_KEY })
}
});
Key Rotation
When rotating keys:
- Deploy new key to secrets manager
- Restart server instances
- Revoke old key
Authentication
LLMRTC doesn't include authentication—implement it in your application:
JWT Authentication
import { LLMRTCServer } from '@llmrtc/llmrtc-backend';
import { verify } from 'jsonwebtoken';
const server = new LLMRTCServer({ providers });
const app = server.getApp();
// Middleware to verify JWT
app.use((req, res, next) => {
// Skip health check
if (req.path === '/health') {
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.substring(7);
try {
const payload = verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
});
WebSocket Authentication
WebSocket connections need special handling:
// Option 1: Query parameter token
// Client connects to: ws://server:8787?token=eyJ...
// Option 2: First message authentication
// Client sends auth message immediately after connect
// Option 3: Pre-authenticated ticket
app.post('/api/tickets', async (req, res) => {
const user = req.user;
const ticket = crypto.randomUUID();
await redis.setex(`ticket:${ticket}`, 30, JSON.stringify(user));
res.json({ ticket });
});
// Client connects with ticket, server validates on connect
Session Correlation
Map LLMRTC sessions to your user accounts:
const userSessions = new Map<string, string>(); // sessionId -> userId
server.on('connection', ({ id }) => {
// Look up pre-authenticated user from ticket or token
const userId = getUserFromConnection(id);
userSessions.set(id, userId);
});
server.on('disconnect', ({ id }) => {
userSessions.delete(id);
});
Network Security
TLS/HTTPS
Always use TLS in production:
# nginx.conf
server {
listen 443 ssl http2;
server_name voice.example.com;
ssl_certificate /etc/letsencrypt/live/voice.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/voice.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
}
CORS Configuration
Restrict allowed origins:
const server = new LLMRTCServer({
providers,
cors: {
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}
});
Firewall Rules
Only expose necessary ports:
# Allow HTTPS and WSS
ufw allow 443/tcp
# Allow WebRTC media (if not using TURN)
ufw allow 10000:20000/udp
# Block direct access to backend port
ufw deny 8787/tcp
Rate Limiting
Prevent abuse with rate limiting:
At the Gateway
# nginx.conf
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location / {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:8787;
}
}
Per-User Limits
import { RateLimiter } from 'limiter';
const userLimiters = new Map<string, RateLimiter>();
app.use(async (req, res, next) => {
const userId = req.user?.id;
if (!userId) return next();
let limiter = userLimiters.get(userId);
if (!limiter) {
limiter = new RateLimiter({
tokensPerInterval: 60,
interval: 'minute'
});
userLimiters.set(userId, limiter);
}
if (await limiter.removeTokens(1) < 0) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
});
Concurrent Session Limits
const activeSessions = new Map<string, Set<string>>(); // userId -> sessionIds
const MAX_SESSIONS_PER_USER = 3;
server.on('connection', ({ id }) => {
const userId = getUserId(id);
const sessions = activeSessions.get(userId) || new Set();
if (sessions.size >= MAX_SESSIONS_PER_USER) {
// Disconnect oldest or reject new connection
const oldest = sessions.values().next().value;
disconnectSession(oldest);
sessions.delete(oldest);
}
sessions.add(id);
activeSessions.set(userId, sessions);
});
Data Protection
PII in Transcripts
Transcripts may contain personally identifiable information:
// Pass hooks via constructor
const server = new LLMRTCServer({
providers,
hooks: {
onSTTEnd: async (ctx, result) => {
// Log transcript without PII
const sanitized = redactPII(result.text);
logger.info({ sessionId: ctx.sessionId, transcript: sanitized });
// Store securely if needed
if (shouldStore()) {
await encryptAndStore(ctx.sessionId, result.text);
}
}
}
});
Data Retention
Define retention policies:
// Automatic cleanup
setInterval(async () => {
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
await db.query('DELETE FROM transcripts WHERE created_at < ?', [cutoff]);
}, 24 * 60 * 60 * 1000); // Daily
Encryption at Rest
Encrypt sensitive data:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encrypt(text: string, key: Buffer): string {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString('base64');
}
Input Validation
Tool Arguments
Validate tool arguments before execution:
import { z } from 'zod';
const BookingSchema = z.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
time: z.string().regex(/^\d{2}:\d{2}$/),
partySize: z.number().int().min(1).max(20)
});
const bookingTool = defineTool({
name: 'book_table',
description: 'Book a table at a restaurant',
parameters: {
type: 'object',
properties: {
date: { type: 'string', description: 'Booking date (YYYY-MM-DD)' },
time: { type: 'string', description: 'Booking time (HH:MM)' },
partySize: { type: 'number', description: 'Number of guests' }
},
required: ['date', 'time', 'partySize']
}
}, async (args) => {
const validated = BookingSchema.parse(args);
return await bookTable(validated);
});
Prompt Injection
Guard against prompt injection:
function sanitizeUserInput(text: string): string {
// Remove potential injection patterns
return text
.replace(/\[SYSTEM\]/gi, '')
.replace(/ignore previous instructions/gi, '')
.slice(0, 1000); // Length limit
}
Audit Logging
Log security-relevant events:
const auditLog = {
logConnection(sessionId: string, userId: string, ip: string) {
logger.info({
event: 'connection',
sessionId,
userId,
ip,
timestamp: new Date().toISOString()
});
},
logToolExecution(sessionId: string, tool: string, args: unknown) {
logger.info({
event: 'tool_execution',
sessionId,
tool,
argsHash: hash(JSON.stringify(args)),
timestamp: new Date().toISOString()
});
},
logError(sessionId: string, error: Error) {
logger.error({
event: 'error',
sessionId,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
}
};
Related Documentation
- Deployment - Production deployment guide
- Configuration - Server options
- Observability & Hooks - Monitoring and logging