Environment variables override their corresponding rolter.toml values, and CLI flags override both. Use environment variables for secrets and for anything that changes between deployments — such as ports, database URLs, and provider API keys — so you can keep a single rolter.toml across environments without embedding sensitive values in it.
--help lists the variable behind each flag as [env: NAME] but never prints the value of a secret or a URL that can carry a password: the admin and internal tokens, and the database, Redis, ClickHouse, snapshot, admin and gateway URLs. It is safe to run in an environment that has them set, and to paste into an issue.
Gateway variables#
These variables control the gateway process (rolter-gateway).
| Variable | Purpose |
|---|---|
ROLTER_CONFIG | Path to the configuration file. Defaults to rolter.toml in the current directory. |
ROLTER_CHECK_ROLE | Which process rolter check validates the environment of: control (the default, every rule) or gateway (only the rules that apply to a data-plane pod). The same as --role. The Helm chart passes it to each preflight init container. |
ROLTER_HOST | Listen host. Overrides [server] host in rolter.toml. rolter easy-up binds its gateway to it as well, and its control plane unless ROLTER_CONTROL_HOST is set, defaulting to 127.0.0.1. The published image’s default command passes --host 0.0.0.0 so its published ports answer; with no ROLTER_ADMIN_TOKEN easy-up then refuses to start unless ROLTER_ALLOW_OPEN_MODE=1. |
ROLTER_PORT | Listen port. Overrides [server] port in rolter.toml. |
ROLTER_KEY_PEPPER | Secret mixed into virtual-key digests. Keeps plaintext keys out of gateway memory; a leaked digest is useless without it. Overrides [server] key_pepper. |
ROLTER_CA_BUNDLE | Path to a custom CA certificate bundle in PEM format for TLS verification of upstream providers. |
ROLTER_SNAPSHOT_URL | Control plane snapshot endpoint URL (e.g. http://control:4001/internal/snapshot) used by the gateway to hot-reload configuration in database mode. |
ROLTER_SNAPSHOT_POLL_SECS | Interval in seconds between gateway snapshot polls. Default 5. Set the same value on the control plane: it does not poll, but the Playground sizes its wait for a freshly minted key from it. |
ROLTER_ADMIN_URL | Control plane management URL used when the gateway proxies /admin/* management traffic to a co-hosted control plane. |
ROLTER_GUARDRAIL_TOKEN | Bearer token used for webhook authentication with external guardrail services when configured via token_env. |
ROLTER_PLUGIN_TOKEN | Bearer token sent to external plugin endpoints when plugin authorization uses token_env. |
OPENROUTER_HTTP_REFERER | HTTP-Referer header value attached to outbound OpenRouter requests for ranking/attribution. |
OPENROUTER_X_TITLE | X-Title header value attached to outbound OpenRouter requests for app name attribution. |
CLICKHOUSE_URL | ClickHouse HTTP interface URL, e.g. http://localhost:8123. Read by both the gateway (which writes request, usage and cost rows) and the control plane (which reads them back for the dashboard), so set it on both. A [logging] clickhouse_url in the gateway’s own config file wins over it; a gateway with neither inherits the control plane’s value from /internal/snapshot. Leave it unset everywhere and the Usage, Costs and Logs screens stay empty however much traffic is served. |
Control plane variables#
These variables control the control-plane process (rolter-control), which manages the dashboard, management API, and config propagation.
| Variable | Purpose |
|---|---|
ROLTER_CONTROL_HOST | Listen host for the control-plane HTTP server. Default 127.0.0.1, or ROLTER_HOST under rolter easy-up. Inside a container, set 0.0.0.0 so a published port reaches it. A non-loopback host with no ROLTER_ADMIN_TOKEN refuses to start unless ROLTER_ALLOW_OPEN_MODE=1. |
ROLTER_CONTROL_PORT | Listen port for the control-plane HTTP server. Default 4001. |
ROLTER_PUBLIC_URL | Externally reachable base URL of the control plane (default http://localhost:4001), used for constructing invitation links and OIDC SSO redirect URIs. Reported by GET /api/v1/public-url; the dashboard’s Single Sign-On screen warns while it is unset. |
ROLTER_SMTP_URL | Mail relay that invitation links are emailed through: smtp://[user:password@]host[:port] (STARTTLS when offered; ?tls=required insists) or smtps://host. Can carry a password, so keep it in a secret. Unset (the default) means invitations are not emailed and the link is only returned to the admin. Held to the egress policy. See Emailing invitations. |
ROLTER_SMTP_FROM | Sender of those emails, noreply@example.com or Rolter <noreply@example.com>. Required with ROLTER_SMTP_URL. |
ROLTER_GATEWAY_URL | Base URL of the data-plane gateway (default http://localhost:4000) used by the control plane to reverse-proxy dashboard Playground requests. |
ROLTER_ADMIN_TOKEN | Bearer token protecting the management API and snapshot endpoints on the control plane and gateway. At least 32 characters after trimming (openssl rand -hex 32 prints 64): the control plane and rolter easy-up refuse to start with a shorter one, and rolter check reports it, because whoever presents the token is superadmin. A blank value counts as unset. |
ROLTER_ALLOW_OPEN_MODE | Boolean flag allowing the control plane to serve an unauthenticated management API on a non-loopback address when ROLTER_ADMIN_TOKEN is unset. |
ROLTER_INTERNAL_ADDR | Address to bind a dedicated socket for /internal/* routes (e.g. 127.0.0.1:4002), separating internal traffic from the public/admin surface. |
ROLTER_INTERNAL_TOKEN | Bearer token required for /internal/* endpoints when separated from ROLTER_ADMIN_TOKEN. |
ROLTER_CORS_ALLOW_ORIGINS | Comma-separated list of origins allowed for CORS preflight requests on the control-plane management API. |
ROLTER_SESSION_PEPPER | Secret pepper mixed into the stored digests of sign-in sessions, invitation links, SCIM tokens and MFA recovery codes, like ROLTER_KEY_PEPPER for virtual keys, and into the failed-login throttle’s Redis keys. Without it, anyone who can write to the database can plant a session for a token they chose. Set the same value on every control-plane replica. Changing it, or setting it on a deployment that ran without one, ends every session and voids open invitations, SCIM tokens and recovery codes. rolter init generates it, and rolter check warns while it is unset. |
ROLTER_MFA_ISSUER | Issuer name an authenticator app shows beside the account during TOTP setup (default rolter). Set it to tell several deployments apart. |
ROLTER_UI_DIR | Filesystem path to the built UI assets served by the control plane. Default ui/dist. |
ROLTER_UI_DOCS_BASE_URL | Base URL of the documentation site the dashboard deep-links into, e.g. https://docs.example.com. Unset by default, which suppresses every documentation link in the dashboard — see Documentation links. |
ROLTER_UI_OTEL_ENDPOINT | OpenTelemetry OTLP/HTTP traces endpoint (e.g. http://localhost:4318/v1/traces) injected into the dashboard for browser tracing. Unset by default. |
ROLTER_UI_OTEL_SERVICE_NAME | Service name reported by dashboard browser OTel spans. Default rolter-ui. |
ROLTER_KEK | AES-256-GCM master key used to encrypt provider credentials at rest in PostgreSQL. It is not part of a database dump — see Backup, restore and KEK rotation. |
ROLTER_KEK_OLD | Read only by rolter kek rotate: the KEK being rotated away from. Never set it on a running gateway or control plane. |
ROLTER_DATABASE_URL | PostgreSQL connection string, e.g. postgres://user:pass@localhost:5432/rolter. |
ROLTER_DB_MAX_CONNECTIONS | Ceiling on open PostgreSQL connections. Default 10. |
ROLTER_DB_MIN_CONNECTIONS | Connections kept open while idle. Default 0. |
ROLTER_DB_ACQUIRE_TIMEOUT_SECS | How long a request waits for a free connection before failing. Default 30. |
ROLTER_DB_STATEMENT_TIMEOUT_SECS | Longest one statement may run before PostgreSQL cancels it and the request answers 504 with code store_timeout. 0 waits forever. Default 30. |
ROLTER_DB_LOCK_TIMEOUT_SECS | Longest a statement waits for a lock another transaction holds, so a write stuck behind a row lock gives up with 504 and code store_timeout instead of hanging. 0 waits forever. Default 10. |
ROLTER_REQUEST_TIMEOUT_SECS | Longest the control plane works on one API request before answering 504 with code request_timeout. 0 disables the bound. The /gw/* playground proxy streams model output and is not bound. Default 60. |
ROLTER_DB_IDLE_TIMEOUT_SECS | Close a connection idle for longer than this. 0 means never. Default 600. |
ROLTER_DB_MAX_LIFETIME_SECS | Retire a connection older than this regardless of use. 0 disables. Default 1800. |
ROLTER_LOGIN_MAX_FAILURES | Failed sign-ins one account tolerates before it locks. Default 5. |
ROLTER_LOGIN_IP_MAX_FAILURES | Failed sign-ins one client address tolerates across every account it tries. Default 50. |
ROLTER_LOGIN_FAILURE_WINDOW_SECS | How long a failure counter survives without a new failure. Default 900. |
ROLTER_LOGIN_LOCK_SECS | The first lock once the budget is spent; each further failure doubles it. Default 60. |
ROLTER_LOGIN_MAX_LOCK_SECS | Ceiling on that doubling, so a lock is always a bounded outage. Default 900. |
ROLTER_LOGIN_MAX_DELAY_MS | Ceiling on the delay added to a rejected sign-in. Default 2000. |
ROLTER_LOGIN_TRUST_FORWARDED_FOR | Count failures against the X-Forwarded-For client instead of the socket peer. Default off. |
ROLTER_LOGIN_THROTTLE_DISABLED | Turn failed-login throttling off entirely. Default off. |
ROLTER_UPDATE_CHECK | Set to false to stop the control plane and the rolter CLI from asking GitHub whether a newer stable release exists. Default on; see Update check. |
Sizing the connection pool#
One pool serves everything the control plane does: /internal/snapshot — which every gateway in your fleet polls — plus the whole dashboard CRUD surface, SCIM, SSO, MCP, and the RBAC guard’s per-request membership lookups. Left too small, that budget silently becomes an acquire-timeout queue.
Size ROLTER_DB_MAX_CONNECTIONS against your database’s max_connections, not against gateway count:
Leave headroom for migrations, backups, and any other client of the same database. Gateway count is a weak input here — snapshot polls are short and answer 304 Not Modified most of the time, so a large fleet does not need a proportionally large pool.
Raise the ceiling when all three of these hold at once: rolter_db_pool_acquire_ms shows waits, rolter_db_pool_idle sits at zero, and rolter_db_pool_connections is pinned to rolter_db_pool_max. That combination means the ceiling is the bottleneck. Acquire waits without a pinned pool mean the database itself is slow, and a bigger pool will make that worse.
Keep ROLTER_DB_ACQUIRE_TIMEOUT_SECS bounded. It is what turns pool exhaustion into a fast, attributable error instead of an opaque stall that looks like a database outage.
Bounding writes#
Nothing waits forever on the control plane’s database. ROLTER_DB_LOCK_TIMEOUT_SECS (default 10) and ROLTER_DB_STATEMENT_TIMEOUT_SECS (default 30) are set on every pooled connection, so a write that sits behind another transaction’s row lock, or a statement that runs away, ends with a 504 in the usual error envelope and the code store_timeout. ROLTER_REQUEST_TIMEOUT_SECS (default 60) is the outer bound for everything else a request can wait on; it answers 504 with the code request_timeout. The dashboard learns this bound from the page the control plane serves and gives up on a call 5 seconds after it, so a connection that never answers at all ends with the same message instead of a spinner that never stops. Keep the three in that order, lock below statement below request, so the database answers first and names the cause. Schema migrations run on a connection without the two database bounds, since an index build can legitimately outlast them.
Throttling failed sign-ins#
POST /api/v1/auth/login is an unauthenticated endpoint that runs an argon2id verification for every request. That cost is what makes password guessing expensive — and it is also what makes an unthrottled login endpoint a cheap way to saturate the control plane’s CPU. Rolter therefore counts rejected sign-ins and refuses further ones before any password work happens.
Two counters run independently:
- Per account.
ROLTER_LOGIN_MAX_FAILURESrejected attempts insideROLTER_LOGIN_FAILURE_WINDOW_SECSlock the account forROLTER_LOGIN_LOCK_SECS. Each further failure doubles the lock, up toROLTER_LOGIN_MAX_LOCK_SECS. - Per client address.
ROLTER_LOGIN_IP_MAX_FAILUREScovers spraying one guess across many accounts, which a per-account counter never catches. The budget is higher because one office egress address is a whole floor of legitimate typos.
Before the lock engages, each rejected attempt is answered a little more slowly than the last, up to ROLTER_LOGIN_MAX_DELAY_MS. A locked attempt is answered 429 with a Retry-After header, so a client waits rather than polling.
Every counter is keyed on the submitted address, never on an account that was found, so throttling reveals nothing about which addresses are registered. Failed attempts, engaged locks and sign-ins that follow one are written to the audit log, and rolter_control_login_attempts counts them by outcome so a credential-stuffing run is visible on your dashboards.
Locks always expire on their own. A lock is a timer, not a flag an administrator has to clear — a lock somebody else can set would be a way to shut a colleague out of their own account. The worst an attacker achieves is a bounded outage capped by ROLTER_LOGIN_MAX_LOCK_SECS.
Where the counters live. With ROLTER_REDIS_URL set, they live in Redis and every control-plane replica counts against the same budget. Without it they are process-local, which is correct for a single-node deployment: with N replicas and no Redis, each replica holds its own budget, so an attacker effectively gets N times the allowance. The control plane logs which mode it is in at startup.
Behind a reverse proxy, every request reaches Rolter from the proxy’s address, so the per-address budget becomes a shared one. Either raise ROLTER_LOGIN_IP_MAX_FAILURES or set ROLTER_LOGIN_TRUST_FORWARDED_FOR=1 — but only set it if every request really does arrive through a proxy that overwrites X-Forwarded-For. An unverified forwarded header hands an attacker an unlimited supply of budgets and the ability to spend somebody else’s. The per-account counter is unaffected either way.
ROLTER_KEK is the most sensitive secret in your Rolter deployment. Any party who holds this key can decrypt every stored provider credential. Generate a strong value before first run and store it in a secrets manager:
Once the database is populated, rotating this key requires re-encrypting all stored credentials. Treat it with the same care as a private CA key.
Update check#
The control plane asks https://api.github.com/repos/rolter-ai/rolter/releases/latest once at boot and every six hours whether a newer stable release exists, and serves the answer from GET /api/v1/version so the dashboard footer can show a small v0.2.0 available hint. The rolter CLI runs the same check beside whatever command you start, caches the answer in ~/.cache/rolter/update-check.json for 24 hours, and prints one line to stderr:
What the check does and does not do:
- One request, a 5-second timeout, carrying nothing but a
User-Agent: rolter/<version>header. No installation id, no configuration, no credentials. - It never blocks a command, never changes an exit status, and never logs above
debugwhen it fails. Offline, rate-limited and malformed answers all look the same: no hint, no notice. - The CLI remembers a failed attempt too: a host that cannot reach
api.github.comtries once a day, not on every command, and keeps showing the last release it learned of. - Pre-release tags are never advertised. A pre-release build (
0.2.0-rc.1) is told about the stable release it precedes; a build newer than the latest release stays quiet. rolter easy-upsupervises both planes in one process and prints the notice once.
ROLTER_UPDATE_CHECK=false (also 0, no, off) turns it off for both the control plane and the CLI. Set it in an air-gapped deployment, or in any script that must make no outbound call at all; the footer then shows the running version alone and /api/v1/version reports "enabled": false.
Shared variables#
These variables apply to both the gateway and the control plane.
| Variable | Purpose |
|---|---|
ROLTER_REDIS_URL | Redis/Valkey connection string. Used by the gateway for rate limits and budget tracking, and by the control plane for config-version pub/sub. |
ROLTER_ENVIRONMENT | Deployment environment name (for example production or staging), exported as the OpenTelemetry deployment.environment.name resource attribute. |
ROLTER_NODE_ID | Unique node identity string identifying a cluster replica instance. |
ROLTER_TELEMETRY_ENABLED | Kill switch for every telemetry export. 0, false, no or off builds no exporter whatever OTEL_* endpoints are set; unset means allowed. It cannot turn telemetry on by itself. |
RUST_LOG | Log filter string passed to the tracing subscriber, e.g. info or rolter_gateway=debug,rolter_control=info. |
OTEL_EXPORTER_OTLP_ENDPOINT | Default OpenTelemetry OTLP endpoint for traces, metrics and logs, e.g. http://otel-collector:4317. A signal-specific endpoint overrides it. With no endpoints set, logs stay on stdout and metrics remain available at /metrics. |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | OpenTelemetry OTLP endpoint for trace export, overriding OTEL_EXPORTER_OTLP_ENDPOINT for traces. |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | OpenTelemetry OTLP endpoint for metric export, overriding OTEL_EXPORTER_OTLP_ENDPOINT for metrics. (Prometheus metrics also stay on the /metrics scrape path.) |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | OpenTelemetry OTLP endpoint for log export, overriding OTEL_EXPORTER_OTLP_ENDPOINT for logs. Logs are additive to stdout and require an active trace exporter (OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT). |
OTEL_EXPORTER_OTLP_PROTOCOL | OTLP transport: grpc (default) or http/protobuf. Match your collector’s receiver port (gRPC 4317, HTTP 4318). |
OTEL_EXPORTER_OTLP_HEADERS | OTLP authentication headers as a comma-separated key=value list, e.g. Authorization=Bearer <token>. |
OTEL_SERVICE_NAME | The service name reported in OTel spans (default rolter), e.g. rolter-gateway or rolter-control. |
Provider key variables#
Provider credentials are not hard-coded variable names — you choose the name and reference it in rolter.toml. Any variable name you set in a provider’s api_key_env field becomes a provider key variable for that deployment.
A variable that holds a deployment secret (ROLTER_KEK, ROLTER_DATABASE_URL, PGPASSWORD, any other ROLTER_ name except ROLTER_GUARDRAIL_TOKEN) cannot be a provider key. The dashboard refuses it with a 400. A gateway that meets such a name in a snapshot serves that provider without a credential and lists the row on /config/problems; a file config that names one fails validation, and rolter-seed --import stops before it writes anything. The same names are not sent as a guardrail webhook or plugin credential either.
Common provider key variables in use:
| Variable | Typical provider |
|---|---|
OPENAI_API_KEY | OpenAI |
ANTHROPIC_API_KEY | Anthropic |
TOGETHER_API_KEY | Together AI |
GROQ_API_KEY | Groq |
Always use api_key_env rather than writing your provider key inline in rolter.toml. Inline keys end up in version control, Docker image layers, and config backups. With api_key_env, your rolter.toml is safe to commit; only the environment running the gateway ever sees the actual key.