9.5 KiB
ODS Security Guide
Security best practices for running ODS.
⚠️ Before You Start
- Run
./install.sh— generates secure random secrets automatically - Never use default passwords — if you see "changeme", change it
- Keep the default localhost binding — opt into LAN exposure only when you understand the firewall and authentication tradeoffs
Secrets Management
Generated Secrets
The installer auto-generates these in .env:
| Secret | Purpose |
|---|---|
WEBUI_SECRET |
Session signing for Open WebUI |
N8N_PASS |
Admin password for n8n |
LITELLM_KEY |
API key for LiteLLM gateway |
Verify no defaults remain:
grep -E "(PASSWORD|SECRET|KEY)" .env | grep -i changeme
Manual Secret Rotation
# Generate new secrets
NEW_WEBUI=$(openssl rand -hex 32)
NEW_N8N=$(openssl rand -base64 16)
NEW_LITELLM="sk-ods-$(openssl rand -hex 16)"
# Update .env
sed -i "s/WEBUI_SECRET=.*/WEBUI_SECRET=$NEW_WEBUI/" .env
sed -i "s/N8N_PASS=.*/N8N_PASS=$NEW_N8N/" .env
sed -i "s/LITELLM_KEY=.*/LITELLM_KEY=$NEW_LITELLM/" .env
# Restart services
docker compose down && docker compose up -d
Network Security
Default: Localhost Only
All services bind to 127.0.0.1 — accessible only from the local machine.
Quick LAN Access
For headless servers accessible from other machines on the same network:
./install.sh --lan
Or change BIND_ADDRESS to 0.0.0.0 in the Dashboard Settings tab, then restart:
ods restart
This binds all services to all network interfaces. Use firewall rules to restrict access to your local subnet:
sudo ufw allow from 192.168.0.0/24 to any port 3000 # WebUI
sudo ufw allow from 192.168.0.0/24 to any port 3001 # Dashboard
sudo ufw allow from 192.168.0.0/24 to any port 8080 # LLM API
Host Agent Network Binding
The host agent (bin/ods-host-agent.py) has its own bind address, separate from the Docker services above. It is controlled by ODS_AGENT_BIND in .env:
| Platform | Default | Behavior |
|---|---|---|
| macOS / Windows | 127.0.0.1 |
Docker Desktop routes container traffic via loopback — loopback is sufficient |
| Linux | auto-detected | Detects the ods-network gateway IP (e.g. 172.18.0.1) so containers can reach the agent; LAN devices cannot. Falls back to the default Docker bridge gateway (e.g. 172.17.0.1) for partial/older installs, then 127.0.0.1 if detection fails. |
To override the default, set ODS_AGENT_BIND in .env:
# Restrict to loopback only (e.g. no-Docker Linux or extra hardening)
ODS_AGENT_BIND=127.0.0.1
# Bind to a Docker network gateway only (explicit Linux default)
ODS_AGENT_BIND=172.17.0.1
# Bind to all interfaces — exposes the host agent API on LAN (not recommended)
ODS_AGENT_BIND=0.0.0.0
Note: If you bind to
0.0.0.0, ensureODS_AGENT_KEYis set in.env— it protects the extension management endpoints with Bearer token authentication.
Exposing to Internet (Not Recommended)
If you must expose publicly, use a reverse proxy with TLS:
Caddy (simple):
# /etc/caddy/Caddyfile
yourdomain.com {
reverse_proxy localhost:3000
}
nginx (with rate limiting):
limit_req_zone $binary_remote_addr zone=ai:10m rate=10r/m;
server {
listen 443 ssl;
server_name ai.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/ai.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ai.yourdomain.com/privkey.pem;
auth_basic "AI Server";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
limit_req zone=ai burst=5;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Consider VPN (Tailscale, WireGuard) instead of public exposure.
Container Security
Resource Limits
Prevent runaway containers:
services:
llama-server:
deploy:
resources:
limits:
memory: 32G
reservations:
memory: 16G
Principle of Least Privilege
The docker-compose files use:
- Non-root users where possible
- Read-only volumes where appropriate
- GPU access only for services that need it
Data Security
What's Stored
| Directory | Contents | Sensitive? |
|---|---|---|
data/open-webui/ |
Chat history, user accounts | Yes |
data/n8n/ |
Workflows, credentials | Yes |
data/qdrant/ |
Vector embeddings | Maybe |
data/whisper/ |
Model cache | No |
models/ |
Downloaded model weights | No |
Encryption at Rest
Docker volumes aren't encrypted by default. For sensitive deployments:
- Use LUKS encrypted filesystem
- Or encrypted Docker volumes
Backup Security
Backups contain sensitive data — encrypt them:
# Create encrypted backup
tar -cz data/ | gpg -c > ods-backup-$(date +%Y%m%d).tar.gz.gpg
# Restore
gpg -d ods-backup-YYYYMMDD.tar.gz.gpg | tar -xz
Model Download Integrity
The installer verifies GGUF model downloads using SHA256 checksums to prevent:
- Corrupted downloads from network issues
- Truncated files from interrupted transfers
- Potential supply chain attacks
How it works:
- Before installation: checks existing model files against known checksums
- After download: verifies freshly downloaded models
- On mismatch: removes corrupt file and prompts for re-download
Verification happens automatically during installation. If a model fails verification:
# The installer will show:
# ✗ Downloaded file is corrupt (SHA256 mismatch)
# Expected: 9f1a24700a339b09c06009b729b5c809e0b64c213b8af5b711b3dbdfd0c5ba48
# Got: [actual hash]
# Corrupt file removed. Re-run installer to download again.
# Simply re-run the installer:
./install.sh
Manual verification:
# Check a model file manually
sha256sum data/models/Qwen3.5-9B-Q4_K_M.gguf
# Compare against expected hash in installers/lib/tier-map.sh
grep -A 2 "Qwen3.5-9B" installers/lib/tier-map.sh | grep GGUF_SHA256
Note: Some models (like qwen3-coder-next) don't have checksums yet. The installer will skip verification for these but still download them successfully.
Network Timeout Hardening
All network operations (downloads, health checks, API calls) include timeout protection to prevent indefinite hangs.
Important semantic note:
curl --max-timeis a total wall-clock timeout for the entire request.wget --timeout/--read-timeoutare per-connection / idle (no-progress) timeouts.
Because of this difference, large model downloads must not use a low curl --max-time, or they will abort on slow-but-progressing links.
Timeout policy:
- Health checks / small API calls: use
curl --connect-timeout+--max-time(short total timeout) - Script downloads / small metadata: use
curl --connect-timeout+--max-time(bounded total time) - Large model downloads: fail fast on unreachable servers, and fail on stalled transfers, but do not impose a low total wall-clock cap
Why this matters:
- Prevents installer hangs on slow/unresponsive networks
- Keeps slow-but-progressing multi-GB downloads running
- Provides predictable failure modes instead of indefinite blocking
Examples:
# Health check (total timeout is OK)
curl -fsS --connect-timeout 3 --max-time 10 http://localhost:8080/health
# Script download (bounded total timeout is OK)
curl -fsSL --connect-timeout 10 --max-time 300 https://get.docker.com -o script.sh
# Large download (stall detection; no low total max-time)
# - speed-limit/time = "consider it stalled if below 10KiB/s for 30s"
curl -C - -L --progress-bar --connect-timeout 10 \
--speed-time 30 --speed-limit 10240 \
-o model.gguf.part https://example.com/model.gguf
On Linux, wget -c with --timeout/--read-timeout provides similar stall protection semantics for large downloads.
All timeout values are tuned for typical network conditions while allowing for slower connections.
API Security
Recommended Architecture
Client → LiteLLM (with API key) → llama-server (localhost only)
llama-server has no authentication by default. Use LiteLLM as your authenticated gateway for remote access.
Service-Specific
| Service | Auth | Notes |
|---|---|---|
| Open WebUI | Built-in | Change admin password, disable signups |
| n8n | Basic auth | Use strong password, enable 2FA |
| llama-server | None | Keep localhost-only, use LiteLLM for remote |
| LiteLLM | API key | Set LITELLM_KEY in .env |
Monitoring
# Watch for errors
docker compose logs -f llama-server | grep -i error
# Monitor resource usage
watch -n 5 'nvidia-smi; docker stats --no-stream'
Set up alerts for:
- High GPU/CPU usage (possible abuse)
- Failed auth attempts
- Unusual network traffic
Updates
# Pull latest images
docker compose pull
# Recreate containers
docker compose up -d
Watch for security updates to: llama-server, Open WebUI, n8n, base images.
Pre-Deployment Checklist
- Ran installer (secrets generated)
- No default passwords remain
- Firewall configured
- TLS enabled (if network-accessible)
- Rate limiting configured
- Backups scheduled
- Credentials documented securely
Reporting Security Issues
Found a vulnerability?
- Do NOT open a public issue
- Email: security@lightheartlabs.com
- Include: description, reproduction steps, potential impact
We'll respond within 48 hours.
Security is a shared responsibility. When in doubt, keep it local.