Configuration¶
ELIDA is configured via configs/elida.yaml or environment variables.
Full YAML Reference¶
# Proxy listener
listen: ":8080"
# Single backend (use this OR the backends block, not both)
backend: "http://localhost:11434"
# Multi-backend routing (see docs/ROUTING.md for details)
backends:
ollama:
url: "http://localhost:11434"
type: ollama
default: true
openai:
url: "https://api.openai.com"
type: openai
models: ["gpt-*", "o1-*"]
api_key: "" # Optional: inject API key server-side (enables keyless clients)
anthropic:
url: "https://api.anthropic.com"
type: anthropic
models: ["claude-*"]
api_key: "" # Optional: inject API key server-side
groq:
url: "https://api.groq.com/openai/v1"
type: groq
models: ["llama-*", "mixtral-*"]
api_key: "" # Optional: use GROQ_API_KEY env var instead
routing:
methods:
- header
- model
- path
- default
# Session management
session:
timeout: 5m
header: "X-Session-ID"
generate_if_missing: true
store: "memory" # "memory" or "redis"
# Derive session identity from the request body when no X-Session-ID
# header is sent (see "Body-Derived Session Identity" below)
derive_from:
openai_user: true # use the OpenAI `user` field (default true)
body_path: "" # optional dot-path, e.g. "metadata.conversation_id"
# Kill block configuration
kill_block:
# "duration" — Block for a specific time after kill
# "until_hour_change" — Block until the clock hour changes
# "permanent" — Block until server restart
mode: "duration"
duration: 30m
# Control API and dashboard
control:
listen: ":9090"
enabled: true
auth:
enabled: true
api_key: "your-control-api-key" # Or use ELIDA_CONTROL_API_KEY env var
# Proxy authentication (optional - secures the proxy endpoint)
proxy:
auth:
enabled: true
api_key: "your-proxy-api-key" # Or use ELIDA_PROXY_API_KEY env var
trusted_networks: [] # CIDRs whose direct peers skip the API-key check
# Policy engine
policy:
enabled: true
mode: "enforce" # "enforce" or "audit"
preset: "standard" # "minimal", "standard", "strict", "mcp", or "coding-agent"
capture_flagged: true
rules:
- name: "high_request_count"
type: "request_count"
threshold: 100
severity: "warning"
suppress_rules: [] # Rule names to drop after merge (preset, custom, or generated)
# Storage (session history and capture)
storage:
enabled: true
capture_mode: "flagged_only" # "flagged_only" or "all"
max_capture_size: 10000 # Max bytes per request/response body
max_captured_per_session: 100 # Max captured pairs per session
# WebSocket / Voice
websocket:
enabled: false
voice_sessions:
enabled: true
max_concurrent: 5
protocols:
- openai_realtime
- deepgram
- elevenlabs
# TLS
tls:
enabled: false
cert_file: ""
key_file: ""
# Redis (when session store is "redis")
redis:
addr: "localhost:6379"
password: ""
db: 0
# OpenTelemetry
telemetry:
enabled: false
exporter: "otlp" # "otlp", "stdout", or "none"
endpoint: "" # OTLP endpoint (e.g., "localhost:4317")
service_name: "elida"
insecure: true
capture_content: "none" # "none", "flagged", or "all"
max_body_size: 4096 # Truncation limit for captured bodies
CLI Flags¶
| Flag | Default | Description |
|---|---|---|
-config |
configs/elida.yaml |
Path to config file |
-listen |
— | Override listen address (e.g. :8082) |
-validate |
false |
Validate config and exit |
-version |
false |
Print version and exit |
Priority: CLI flag > environment variable > config file.
Environment Variables¶
All configuration can be overridden with environment variables:
| Variable | Default | Description |
|---|---|---|
ELIDA_LISTEN |
:8080 |
Proxy listen address |
ELIDA_BACKEND |
http://localhost:11434 |
Backend URL |
ELIDA_CONTROL_LISTEN |
:9090 |
Control API address |
ELIDA_SESSION_STORE |
memory |
Session store: memory or redis |
ELIDA_SESSION_TIMEOUT |
5m |
Session timeout duration |
ELIDA_POLICY_ENABLED |
false |
Enable policy engine |
ELIDA_POLICY_MODE |
enforce |
Policy mode: enforce or audit |
ELIDA_POLICY_PRESET |
— | Policy preset: minimal, standard, strict |
ELIDA_STORAGE_ENABLED |
false |
Enable SQLite storage |
ELIDA_STORAGE_CAPTURE_MODE |
flagged_only |
Capture mode: flagged_only or all |
ELIDA_WEBSOCKET_ENABLED |
false |
Enable WebSocket proxy |
ELIDA_TLS_ENABLED |
false |
Enable TLS/HTTPS |
ELIDA_TLS_CERT_FILE |
— | Path to TLS certificate |
ELIDA_TLS_KEY_FILE |
— | Path to TLS private key |
ELIDA_TELEMETRY_ENABLED |
false |
Enable OpenTelemetry |
ELIDA_REDIS_ADDR |
localhost:6379 |
Redis address |
ELIDA_REDIS_PASSWORD |
— | Redis password |
ELIDA_REDIS_DB |
0 |
Redis database number |
ELIDA_CONTROL_API_KEY |
— | API key for control API auth (auto-enables auth) |
ELIDA_PROXY_API_KEY |
— | API key for proxy auth (auto-enables auth) |
Proxy Authentication¶
ELIDA supports optional API key authentication on the proxy endpoint to prevent unauthorized access.
Configuration¶
Supported Auth Methods¶
| Method | Header | Example |
|---|---|---|
| ELIDA API Key | X-Elida-API-Key |
X-Elida-API-Key: your-secret-key |
| Bearer Token | Authorization |
Authorization: Bearer your-secret-key |
Security Features¶
- Constant-time comparison — Uses
crypto/subtle.ConstantTimeCompareto prevent timing attacks - Header stripping —
X-Elida-API-Keyis stripped before forwarding to backend (not leaked) - Health bypass —
/health,/healthz,/ready,/readyzbypass auth for load balancer probes
Trusted Networks (proxy.auth.trusted_networks)¶
A CIDR allowlist whose direct peers skip the API-key check entirely. This lets un-keyed auxiliary agent calls (compression, title generation) work on a trusted network while the wider LAN still needs the key.
proxy:
auth:
enabled: true
api_key: "your-proxy-api-key"
trusted_networks:
- "127.0.0.1/32" # loopback
- "::1/128" # loopback (IPv6)
- "172.16.0.0/12" # e.g. a private/container network
- Direct peer only — the trust decision looks at the TCP connection's remote address only. It never consults
X-Forwarded-FororX-Real-IP, so a client can't spoof its way past auth by setting those headers. - Fail closed at startup — an invalid CIDR in
trusted_networksfails config validation rather than being silently skipped. - Empty list = no bypass — if
trusted_networksis unset or empty, nobody is exempt; auth behaves exactly as if the feature didn't exist. - Docker gateway gotcha — a containerized ELIDA sees requests from a host-side client through the Docker bridge, so
r.RemoteAddris the bridge gateway (typically172.17.0.1), not the real client. Include the bridge subnet (e.g.172.17.0.0/16) if you want host-side calls exempted — but that also exempts every other container on the same bridge network. - Reverse-proxy deployment caveat — trust is decided by the direct peer. If ELIDA sits behind a local reverse proxy (nginx, Envoy, a load balancer), all external traffic arrives at ELIDA with the reverse proxy's source IP — trusting loopback (or that proxy's IP) would exempt every external client, not just internal callers. Only use
trusted_networkswhen ELIDA itself is the network-facing listener; behind a reverse proxy, rely onproxy.auth.api_keyinstead (and have the proxy enforce access control upstream).
A per-request slog.Debug line ("proxy auth bypassed for trusted network client") is emitted whenever a request actually uses the bypass, so the effect is visible when debug logging is enabled.
Backend API Key Injection (Keyless Clients)¶
ELIDA can inject API keys server-side, enabling keyless clients (SBC pattern):
backends:
openai:
url: "https://api.openai.com"
type: openai
api_key: "sk-..." # Injected into requests automatically
Clients connect to ELIDA without any API key. ELIDA injects the correct auth header based on backend type:
- Anthropic: x-api-key: <key>
- OpenAI/Groq/Mistral: Authorization: Bearer <key>
This is useful for: - Public demos with rate limiting - Internal services without credential distribution - Multi-tenant setups with per-backend keys
Policy Direction Split¶
ELIDA splits content policy rules by direction to prevent false positives from LLM conversation history while still catching real threats.
How It Works¶
| Direction | Severity | Action | Purpose |
|---|---|---|---|
| Response (AI output) | Critical | Block/Terminate | AI generating dangerous content is a real threat |
| Request (user input) | Critical | Flag | Conversation history may contain matching patterns; risk ladder escalates |
Request-side flags score 10.0 points (critical severity) on the risk ladder. Repeated violations escalate automatically:
| Risk Score | Action |
|---|---|
| 5 | Warn |
| 15 | Throttle |
| 30 | Block |
| 50 | Terminate |
Rule Suppression and Observe Mode¶
Two knobs shape which rules run and how much weight they carry, on top of
the local-overrides-default merge described above (a custom rule with the
same name as a preset rule replaces it).
| Field | Type | Description |
|---|---|---|
policy.suppress_rules |
[]string |
Rule names to drop after the merge. Works on preset rules, custom rules, and generated circuit-breaker rules alike. |
rules[].observe |
bool |
Marks a single rule observe-only: it still flags and captures, but its action is forced to flag and it contributes nothing to the risk ladder. |
policy:
preset: standard
suppress_rules: [destructive_file_ops, compound_anomaly]
rules:
- name: shell_execution
type: content_match
target: response
patterns: ["bash\\s+-c\\s+"]
severity: warning
action: flag
observe: true # flag + capture only, never escalates the risk ladder
mode: audit is a true dry run on top of this: rule actions don't enforce
and the risk ladder is clamped to observe/warn, so audit mode can never
throttle, block, or terminate — even if individual rules or the risk
ladder would otherwise escalate.
The coding-agent Preset¶
policy.preset: coding-agent is tuned for trusted coding agents (Claude
Code, Hermes, Cursor) whose legitimate output contains bash -c, sudo,
rm -rf, curl | sh, and whose tool loops look like high-rate bursts to
anomaly detectors. Structural rules (dangerous tool names/arguments,
credential-access tool calls, rate limits) enforce; content and
statistical heuristics (shell/privilege/destructive/exfil patterns,
prompt injection, PII, rate_anomaly, compound_anomaly) run in observe
mode. Nothing in the preset terminates a session. See
docs/policy-rules-reference.md for the full
rule list.
policy:
preset: coding-agent
circuit_breaker:
enabled: true
max_tool_fanout: 100 # agents legitimately expose 30+ tools
Allowlisted Tools¶
Tools that bypass request-side content scanning. When the latest assistant message contains only allowlisted tools, the request skips policy checks entirely.
Tools like Bash are intentionally excluded — they can execute dangerous commands and should be scanned.
Session ID Behavior¶
ELIDA resolves a session ID per request, in order of precedence:
X-Session-IDheader — if present, used verbatim.- Body-derived identity (
session.derive_from) — see below. - Client-IP + backend fallback —
client-<hash>-<backend>; requests from the same client to the same backend are grouped into one session automatically when neither of the above applies.
# Use explicit session ID
curl -H "X-Session-ID: my-agent-task-123" http://localhost:8080/api/generate ...
# Response includes the session ID
< X-Session-ID: my-agent-task-123
Body-Derived Session Identity (session.derive_from)¶
When no X-Session-ID header is sent, ELIDA can derive a stable session ID from the JSON request body, so one conversation keeps one session even if routing sends its requests to different backends (failover, load balancing, retries). Derived IDs deliberately contain no backend component — failover never splits a conversation into separate sessions, and the kill-switch stays per-conversation instead of per-host.
session:
derive_from:
openai_user: true # derive from the OpenAI `user` field (default: true)
body_path: "" # optional dot-path, e.g. "metadata.conversation_id"
Precedence when deriving from the body:
session.derive_from.body_path, if configured and the path resolves to a non-empty string in the body — takes precedence over theuserfield.session.derive_from.openai_user— the standard OpenAIuserfield, on by default.- Neither applies (or both are disabled) — falls through to the client-IP + backend fallback above.
Derived values are formatted as user-<value> when the value is short and contains only [A-Za-z0-9._:-], or user-<16 hex chars> (a SHA-256-based hash) otherwise — so arbitrary or long user/body values never leak verbatim into the session ID.
Set openai_user: false and leave body_path empty to disable body-derived identity entirely and always use the client-IP + backend fallback when no X-Session-ID header is sent.
Security note: the OpenAI user field is client-controlled input — ELIDA trusts whatever value the caller puts in the request body. Session identity is not a cosmetic label: it's the key the kill-switch, risk ladder, and forensic capture all index by. In a single-tenant deployment — one trusted agent stack talking to its own ELIDA instance, ELIDA's common case — this is fine, and it's the whole point of the feature: the agent's own conversation ID keeps its session coherent across failover. In a multi-tenant, shared-key, or unauthenticated deployment, it's a liability: any client that knows or guesses another client's user value joins that client's session, and can then trigger a targeted session-kill, poison its risk score toward (or away from) enforcement, or pollute its forensic capture with noise. If clients are mutually untrusted, set derive_from: {openai_user: false} and either leave body_path empty or point it at a value the clients cannot guess or set for each other.
Settings Hierarchy (Layered Configuration)¶
ELIDA uses a VS Code-style layered settings system. Settings are merged in order, with later layers overriding earlier ones:
┌─────────────────────────────────────────────────────────┐
│ Layer 3: settings.yaml (UI overrides) — highest │
│ Hot-reloaded, no restart needed │
├─────────────────────────────────────────────────────────┤
│ Layer 2: Environment Variables │
│ Override YAML at startup │
├─────────────────────────────────────────────────────────┤
│ Layer 1: elida.yaml (base config) — lowest │
│ Loaded at startup │
└─────────────────────────────────────────────────────────┘
How It Works¶
configs/elida.yaml— Base configuration loaded at startup- Environment variables — Override YAML values (e.g.,
ELIDA_POLICY_MODE=audit) configs/settings.yaml— UI overrides, created when you save settings in the dashboard
Example¶
# configs/settings.yaml (UI override, auto-generated)
policy:
mode: enforce # Overrides env var back to enforce
custom_rules:
- name: block_competitor_mentions
type: content_match
patterns: ["competitor-name"]
action: block
Result: Policy enabled, enforce mode (UI wins), standard preset, plus custom rule.
Dynamic Reload (Hot-Reload)¶
Changes made via the Settings UI are applied instantly — no restart required. The policy engine reloads its configuration atomically while preserving active session state.
# Save settings via API
curl -X PUT http://localhost:9090/control/settings \
-H "Content-Type: application/json" \
-d '{"policy":{"mode":"audit"}}'
# Response
{"status":"saved","message":"Settings applied instantly (no restart required)"}
Custom Rules¶
Custom rules defined in the UI are appended to the preset rules (they don't replace them). Rules use RE2 regex syntax.