Full Configuration Reference
Every field validated by Pydantic at startup, with its default. See Configuration for a faster path to a working setup, or Environment Variables for the override list.
Full reference
Section titled “Full reference”storage: backend: "mongodb" # mongodb | memory mongodb: uri: "mongodb://user:pass@host:27017/db?authSource=db" database: "mcp_analyzer" # collection names below all have defaults — override only if you need to
cors: allow_origins: ["https://your-app.example.com"] # ["*"] for dev allow_credentials: true allow_methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] allow_headers: ["*"]
auth: api_key: "change-me-to-a-strong-secret" # null → disabled (anonymous default-org owner)
jwt: # secret: "set-me-in-production" # random per-process if omitted refresh_ttl_days: 14 # reserved; refresh-token rotation not yet implemented
# session.access_ttl_minutes isn't set here — it's platform-managed, see below
two_factor: # secret_encryption_key: "set-me-in-production" # random per-process if omitted
rate_limit: enabled: true # the thresholds are NOT set here — they're platform-managed, see below
llm: # enabled is NOT set here — it's platform-managed, see below # credential_encryption_key: "set-me-in-production" # random per-process if omitted
notifications: # Defaults are usually right: any public address, no private networks. Set # allow_private_networks when self-hosting and notifying something on your own network. egress: enabled: true allow_private_networks: false allowed_hosts: [] max_channels_per_org: 20 timeout_seconds: 5.0
deployment: mode: "self_hosted" # self_hosted | cloud — see belowThat’s the entire file — logging, registration, social_login, server, session, mcp, telemetry,
retention, jobs, analysis, org_offboarding, and github are deliberately absent, and
rate_limit appears with only its on/off switch; see below.
The example configs shipped in each repo (OpenTremor-core/configs/opentremor-core*.yaml,
OpenTremor-platform/configs/opentremor-platform*.yaml) point here rather than restating any of
it — this page is the single source of truth for which sections a config file can set.
Platform settings — admin-editable subset
Section titled “Platform settings — admin-editable subset”logging, registration, social_login, server, session, mcp, telemetry, retention, jobs,
analysis, feedback, org_offboarding, and github are not read from this file or from any
environment variable. Their only possible baseline is a fixed, hardcoded default
(the Pydantic field default shown in each section below) — create_app() resets these sections
to that default the moment the config is loaded, discarding whatever the file/env contained for
them, before anything else in the process reads them. The only way to change one is
GET/PATCH /admin/settings (superadmin only — the dashboard’s Platform Admin → Settings
page).
This is deliberate, not a limitation: previously, an admin-editable field with no override yet simply reflected whatever the local YAML happened to contain — which meant two instances that had never been touched via the admin UI could show different “effective” values purely because they shipped with different config files, with no way to tell a deliberate choice from incidental yaml drift. Resetting these sections to one fixed, documented default removes that ambiguity by construction: there is exactly one baseline, and exactly one way to change it.
Covered — hardcoded default + live database override, no restart, ever: logging,
registration, social_login, server, session, rate_limit (thresholds only), retention, jobs,
analysis, feedback, org_offboarding, github. A PATCH
takes effect for the very next request (for session.access_ttl_minutes, the very next
login/session-switch — it doesn’t retroactively extend a cookie a user is already holding; for
retention.telemetry_days/retention.audit_days, immediately — pushed live onto the running
MongoDB TTL indexes via collMod, not just re-read on the next request). See
Data Retention for retention’s own field-by-field detail,
Platform Admin — Running a stuck-job sweep manually
for jobs’s, and Deleting an organization for
org_offboarding’s — all three are covered here only at the same summary depth as every other
section.
Excluded — genuinely file/env-driven (unaffected by any of the above):
| Section | Why |
|---|---|
storage | The MongoDB connection string can’t be stored in the database it points to. A live cutover to a different target is possible, just through a dedicated mechanism instead: POST /admin/storage/switch — see Platform Admin — Switching the storage backend live. |
cors | Security-critical — changing it live from an authenticated session is exactly what shouldn’t be possible. |
auth | api_key is a bootstrap credential that must work independently of any stateful system, including this API; default_admin_email/default_admin_password only take effect on first boot anyway. Ongoing auth management happens through POST /admin/users/{id}/superadmin, org invites/roles, SSO connections, and service accounts instead — see Authentication. |
jwt | Rotating the session-signing secret invalidates every existing session across every replica. (Session lifetime is a separate, covered section — see session below; only the signing secret itself is excluded here.) |
two_factor | Rotating the secret encryption key makes every already-enrolled user’s TOTP secret unreadable without a re-encryption step. |
rate_limit.enabled | One field, not a section — the rest of rate_limit is editable. Whether brute-force protection runs at all is the single change a stolen superadmin session would most want to make, and the one an operator couldn’t undo from inside a compromised deployment. |
deployment | Identifies which deployment this process is (the operator’s cloud instance vs. a customer’s self-hosted one), not a business setting an authenticated admin of either kind should be able to flip — see deployment below. |
Excluded — fixed at the hardcoded default, not configurable by any means (not file, not env, not the admin API either):
| Section | Why |
|---|---|
mcp | Read once, synchronously, inside create_app() itself — the MCP ASGI app is mounted (or not) before the FastAPI app object even exists for a lifespan to attach to, let alone run an async database read. There is no point in the process’s life at which a database override could reach this section before it’s already acted on. |
telemetry | Same reason as mcp — TelemetryMiddleware’s enabled flag is constructed from it before an override could ever apply. (The TTL/retention duration for the spans it produces is a separate, covered section — see retention below and Data Retention; only the on/off switch itself is excluded here.) |
See Platform Admin — Settings for the full endpoint reference,
Platform Admin — Preparing MongoDB for the
POST /admin/storage/prepare-mongodb action that replaces tools/init_mongo.py with a
dashboard button, and Platform Admin — Switching the storage backend live
for actually cutting the running process over to a prepared target.
Fields
Section titled “Fields”logging
Section titled “logging”| Key | Default | Description |
|---|---|---|
level | info | Log verbosity: debug, info, warning, error |
Not set in this file — level above is the fixed baseline; change it live via
PATCH /admin/settings (logging_level) — see Platform settings.
storage
Section titled “storage”| Key | Default | Description |
|---|---|---|
backend | memory | mongodb or memory |
mongodb.uri | — | Full MongoDB connection URI (required when backend is mongodb) |
mongodb.database | — | Target database |
| Key | Default | Description |
|---|---|---|
allow_origins | ["*"] | Allowed origins — restrict to known callers in production |
allow_credentials | true | Allow cookies / auth headers |
allow_methods | ["*"] | Allowed HTTP methods |
allow_headers | ["*"] | Allowed request headers |
| Key | Default | Description |
|---|---|---|
api_key | null | Bootstrap admin key. null disables authentication entirely |
default_admin_email | null | Human superadmin account, created once at startup if set |
default_admin_password | null | Password for default_admin_email (min. 8 characters — shorter values are silently skipped, not rejected) |
auth.api_key is the bootstrap key — it always resolves to owner of the default org (and platform superadmin — see Platform Admin) and is never stored in the database. Additional keys and human accounts are created at runtime (POST /auth/keys, POST /auth/register) and scoped to whichever org created them — see Authentication.
auth.default_admin_email/default_admin_password create a ready-to-use human login the first time the server starts with them set — idempotent (only acts if no user with that email exists yet, so it’s safe to leave in a config that’s applied on every restart). This is the declarative counterpart to grant_superadmin.py: convenient for a fresh install where running a separate script against a live database isn’t the point.
When auth is enabled, every endpoint except GET /health requires either:
X-API-Key: <key>or a session cookie / bearer token from POST /auth/session.
registration
Section titled “registration”| Key | Default | Description |
|---|---|---|
allow_public_org_creation | true | Whether anyone can create a new organization unilaterally |
terms_version | unset | The version of your terms every new account must accept (1–32 chars) |
terms_url | unset | Where those terms are published (http(s)://) |
privacy_url | unset | Your privacy policy, linked beside the terms — optional |
When set to false, two things 403 for everyone except a superadmin: POST /auth/register with
org_name set (and no invite_token), and POST /orgs (an already-logged-in user creating an
additional org). Two things are unaffected either way, since neither creates a new
organization: registering with neither org_name nor invite_token (an org-less account — see
Invitations), and registering with invite_token (joining an
existing org an invite grants access to). Use this to force invite-only growth on a
publicly-reachable instance without touching who can register an account at all.
Not set in this file — change it live via PATCH /admin/settings
(registration_allow_public_org_creation), no restart — see
Platform settings above.
The dashboard hides what the setting forbids: with it false, the sign-up page drops the “create
an organization” choice and its org-name field, and the onboarding page an org-less account lands
on offers only the invite route. Both read the value from the public
GET /auth/registration-options on load —
presentation only, with the two endpoints above still doing the enforcing.
Requiring your terms at sign-up
Section titled “Requiring your terms at sign-up”An operator that publishes terms for its service sets terms_version and terms_url (both, or
nothing happens). From then on every self-service sign-up must accept that exact version:
- the sign-up page shows a required checkbox linking to
terms_url(andprivacy_url), which also gates the “Continue with …” buttons; POST /auth/register— in every mode, invitations included — refuses a body whoseaccepted_terms_versionisn’t the current version (400), and a social-login sign-in that would create an account fails withoauth_error=terms_required;- the account records
terms_accepted_versionandterms_accepted_at.
Changing terms_version applies from the next sign-up: a form opened before the change is refused
and has to be reloaded. Accounts that already exist are not asked again. Accounts that an
organization provisions through its own SSO connection or SCIM are never asked — they are that
organization’s users. OpenTremor ships no terms of its own; unset, the default, sign-up asks for
nothing. Set all three live with PATCH /admin/settings (registration_terms_version,
registration_terms_url, registration_privacy_url) or from Platform Admin → Settings.
social_login
Section titled “social_login”Platform-wide Continue with Google / Microsoft on the login and register pages — one OAuth client per provider for the whole deployment. Not an organization’s SSO connection, which always takes precedence for the email domains it claims. See Quick Start: Sign in with Google or Microsoft.
| Key | Default | Description |
|---|---|---|
enabled | false | Master switch. Off hides every button and refuses every provider sign-in, but keeps the stored credentials |
allow_signup | true | Whether a sign-in matching no account creates one (org-less, continuing on onboarding) |
allow_auto_link | false | Whether a sign-in whose verified email matches an existing account is attached to it automatically. Off: the user links it from their Account page after a password sign-in |
google_enabled | false | Offer Google |
google_client_id | null | Google Cloud OAuth client ID (type Web application) |
google_client_secret | null | Write-only — accepted by PATCH, never returned; secrets_set says whether one is stored |
microsoft_enabled | false | Offer Microsoft |
microsoft_client_id | null | Entra app registration’s application (client) ID |
microsoft_client_secret | null | Write-only, same as Google’s |
microsoft_tenant | common | common, organizations, consumers, or one tenant ID — whose accounts are admitted |
A provider is offered only when enabled, its own *_enabled, and both its client ID and secret
are set. Redirect URIs to register: {server.public_base_url}/auth/oauth/google/callback and
{server.public_base_url}/auth/oauth/microsoft/callback. The Microsoft app must add the optional
ID-token claims email and xms_edov, or every Microsoft sign-in is refused.
Not set in this file — change it live via PATCH /admin/settings (social_login_*), no restart.
The client secrets are stored in the settings document the same way the GitHub App’s private key
is, and are subject to the same at-rest caveat.
server
Section titled “server”Not set in this file at all — every field below is a fixed baseline, changed only via
PATCH /admin/settings (server_* fields), no restart. On a fresh deployment that needs
dashboard_base_url/public_base_url set from day one (see the reverse-proxy note below), that
means one PATCH /admin/settings call right after first boot instead of a config-file line.
| Key | Default | Description |
|---|---|---|
max_input_size_mb | 10 | Maximum ingest/analyze body size (1–500 MB) |
sync_analysis_max_units | 5 | POST /{analyzer}/{namespace}/analyze runs inline (sync, 200) at or below this many ingested units; above it, a background job is created instead (202) |
public_base_url | null | Public URL used for external links in reports (e.g. the HTML report link in markdown_light), and to build the GitHub App manifest flow’s webhook/redirect URLs (see GitHub App Integration). When null, report links fall back to X-Forwarded-Proto/X-Forwarded-Host headers, then request.base_url — but the manifest flow has no such fallback (there’s no in-flight request to derive it from at redirect time) and returns 501 until this is set. |
dashboard_base_url | null | Where the dashboard is reachable — used only to build the redirect target after the GitHub App manifest flow’s callback. Falls back to public_base_url when unset, which is correct for the default same-origin, path-prefixed deployment; only set this explicitly if the dashboard is on a separate Service/ingress path. |
telemetry
Section titled “telemetry”Controls the built-in metrics and tracing system.
| Key | Default | Description |
|---|---|---|
enabled | true | Collect and expose telemetry spans |
When enabled, every HTTP request is recorded as a span, and enriched domain spans are recorded on ingest and analysis submission. Spans are queryable via GET /metrics/* (org-admin only, org-scoped).
retention
Section titled “retention”TTL/purge policy for telemetry spans, the audit log, findings, and raw analyzed resource content.
Covered by PATCH /admin/settings — thin summary table only; see
Data Retention for the full policy this backs (why each field has the
lifecycle it does, how TTL-based vs. sweep-based fields differ in when they take effect, and the
severity/status tiering for findings).
| Key | Default | Bounds |
|---|---|---|
telemetry_days | 30 | 1–365 |
audit_days | 730 | 30–3650 |
finding_low_medium_days | 180 | 1–3650 |
finding_high_critical_days | 730 | 1–3650 |
resource_content_days | 90 | 1–3650 |
feedback_days | 730 | 1–3650 |
sweep_enabled | true | — |
sweep_interval_hours | 24 | 1–168 |
telemetry_days/audit_days are pushed live onto the running MongoDB backend’s TTL indexes via
collMod the moment they’re PATCHed — no restart, no re-prepare. The other fields govern the
retention sweep (POST /admin/retention/run), normally triggered by
OpenTremor-task-scheduler’s heartbeat rather than this endpoint’s
value alone. Ignored by the in-memory backend the same way every other Mongo-specific setting is.
Timeout/cadence policy for the stuck-job sweep. Platform-owned: job tracking is generic
infrastructure, not analysis-specific. There is no durable job queue (an async .../analyze
job is a plain in-process background task), so a process crash or restart mid-run otherwise
leaves a job frozen at queued/running forever. Covered by PATCH /admin/settings;
see Platform Admin — Running a stuck-job sweep manually
for the full endpoint reference.
| Key | Default | Bounds |
|---|---|---|
stuck_timeout_minutes | 60 | 5–1440 |
sweep_enabled | true | — |
sweep_interval_hours | 1 | 1–168 |
stuck_timeout_minutes is how long a job can go without a progress update (updated_at, stamped
on every status transition and progress report) before the sweep (POST /admin/jobs/sweep)
presumes it orphaned and marks it failed — resumable afterward via POST /jobs/{job_id}/retry. sweep_enabled/sweep_interval_hours govern the sweep itself, normally
triggered by OpenTremor-task-scheduler’s heartbeat rather than this
endpoint’s value alone, same shape as retention above.
analysis
Section titled “analysis”How the server-side analysis loop drives the LLM — the only section owned outright by the
analysis engine rather than the platform. (llm, below, is about access: whether the feature
is on and what key encrypts per-org credentials. That’s generic; this isn’t.) Not set in the
config file — every field is a fixed baseline changed via PATCH /admin/settings
(analysis_* fields), no restart, which matters because these are exactly the knobs you reach
for while a provider is misbehaving. See
Architecture — the drain loop for what each one bounds.
| Key | Default | Bounds | Description |
|---|---|---|---|
max_concurrent_units | 4 | 1–32 | Units analyzed in parallel within one run |
llm_timeout_seconds | 120.0 | 5–600 | Per-call deadline handed to the provider SDK |
llm_max_attempts | 3 | 1–6 | Total attempts per unit, not retries on top of one — 1 disables retrying |
llm_retry_backoff_seconds | 1.0 | 0.1–30 | Base delay for exponential backoff with full jitter |
llm_max_output_tokens | 4096 | 512–32000 | Ceiling on one unit’s structured response, handed to the provider SDK |
injection_scan_enabled | true | — | Run agent-injection detection on every change |
injection_scan_semantic | false | — | Tier 2, the LLM scorer — off by default |
injection_scan_gate | "review" | off | review | fail | What an injection finding does to a pull request’s commit status |
llm_max_output_tokens is a correctness bound, not a cost one — the organization’s
monthly LLM budget is what caps spend. It matters because
every provider truncates rather than errors when a response outgrows its cap: set it too low
and findings are silently lost off the end of a long analysis. The backends detect the overflow
and fail the unit with a message naming the cap, rather than reporting the half-written result as
a schema violation, so “raise this” is an obvious fix rather than a guess. The default holds a
unit with a handful of findings comfortably; a unit that reliably overflows it is usually one that
should have been split.
max_concurrent_units is a per-run bound: N concurrent analyze jobs can have N × this many
calls in flight. That’s deliberate — the backstop against runaway spend is the organization’s
monthly LLM budget, which the loop honours per unit. Raising it
past what your provider’s rate limit allows makes 429s (retried, but not free in wall clock) more
likely before it makes anything faster.
llm_timeout_seconds replaces the provider SDK’s own default, which is far longer than a job’s
useful deadline — Anthropic’s is 10 minutes. A timed-out call is treated as transient and
retried, not fatal.
The three injection_scan_* fields are free on every plan and on both deployment modes. The
deterministic tiers make no provider call at all, so leaving detection on costs a few milliseconds
of matching per changed file — it is neither metered nor counted against an organization’s demo
quota. injection_scan_semantic is the exception and the reason it defaults off: it is the one
tier that spends the organization’s own LLM budget, and the one that reads the payload it is
judging. injection_scan_gate is review by default, which holds a pull request at pending
rather than failing it — see
the gate.
Only transient failures are retried: 408/409/429 and 5xx, plus timeouts and dropped connections. An invalid API key, a rejected schema or an unparseable response fails the run on the first attempt, because asking again reproduces it exactly.
feedback
Section titled “feedback”What reviewer verdicts are allowed to change about later analyses. Covered by
PATCH /admin/settings — thin summary table only; see
Review Feedback for what each field is defending against, and
Review Feedback Loop for the design.
| Key | Default | Bounds |
|---|---|---|
calibration_enabled | true | — |
min_samples | 3 | 1–100 |
max_entries_per_rule | 5 | 1–50 |
max_chars | 4000 | 200–40000 |
severity_ceiling | HIGH | CRITICAL/HIGH/MEDIUM/LOW |
trusted_only | true | — |
github_comments_enabled | true | — |
command_prefix | @opentremor | 2–64 chars |
proposal_rejection_threshold | 0.6 | 0.0–1.0 |
proposal_disable_threshold | 0.9 | 0.0–1.0 |
proposal_confirm_threshold | 0.9 | 0.0–1.0 |
Capture has no switch here on purpose — recording a triage decision is inert and always worth
doing. Every field above governs use: whether verdicts reach the analysis prompt, how much of
them, and what they can never touch. severity_ceiling and min_samples are the two that stop
the loop from quietly suppressing a real finding; trusted_only: false lets any commenter on a
public pull request write into every later prompt for the org, and should stay on unless every
repository is private.
org_offboarding
Section titled “org_offboarding”The grace period between DELETE /admin/organizations/{org_id} scheduling an organization’s
deletion and it becoming permanent. Covered by PATCH /admin/settings; see
Platform Admin — Deleting an organization for the
full endpoint reference and exactly what does and doesn’t survive a purge.
| Key | Default | Bounds |
|---|---|---|
grace_period_days | 30 | 1–365 |
Read fresh per request, same as every other covered section — a change applies to any
still-pending organization’s computed purge_after immediately, and to
POST /admin/organizations/purge-pending’s next run.
Controls the FastMCP endpoint mounted alongside the REST API.
| Key | Default | Description |
|---|---|---|
enabled | true | Mount the MCP server at mount_path |
base_url | http://localhost:8000 | URL FastMCP uses for internal HTTP calls back to FastAPI |
mount_path | /mcp | Path prefix for MCP endpoints (/mcp/sse, POST /mcp/) |
base_url is always http://localhost:8000 in every environment (bare-metal, Docker,
Kubernetes) — FastMCP calls back to the same process via loopback, no service-level routing
needed, so this never needs to be anything else.
Signs human-user session tokens (POST /auth/session, POST /auth/register).
| Key | Default | Description |
|---|---|---|
secret | random per-process | HS256 signing secret. Must be set explicitly (and shared across every replica) in any real deployment via JWT_SECRET — otherwise sessions won’t validate across pods, and every restart invalidates all logged-in sessions |
refresh_ttl_days | 14 | Reserved — refresh-token rotation isn’t implemented yet |
session
Section titled “session”Session token lifetime — kept separate from jwt so it can be platform-managed (live-editable via
PATCH /admin/settings) without also exposing jwt.secret, whose rotation invalidates every
existing session across every replica.
Not set in this file or via env vars — change it live via PATCH /admin/settings
(session_access_ttl_minutes), no restart — see
Platform settings above. Takes effect for newly
issued sessions only (login, register, session-switch); doesn’t retroactively extend a session a
user is already holding.
| Key | Default | Description |
|---|---|---|
access_ttl_minutes | 480 | Session token lifetime (1–1440) |
A platform section despite the name — see Platform architecture — LLM. It carries the base only: whether the deployment offers LLM-backed features, and the key their secrets are encrypted under. The provider clients belong to the product.
| Key | Default | Description |
|---|---|---|
enabled | true | Admin-editable — Platform Admin → Settings → General → LLM Service, or PATCH /admin/settings {"llm_enabled": false}. Whether this deployment offers LLM-backed features at all. Like every admin-editable field, a value in the config file is discarded at boot; the API is the only way to change it |
credential_encryption_key | random per-process | File/env only, and never returned by the API. Encrypts secrets at rest (Fernet): SSO connection client secrets, per-org LLM connection API keys and CA bundles, and the GitHub App private key. Same must-be-set-in-production rule as jwt.secret via LLM_CREDENTIAL_KEY — otherwise stored secrets become unreadable across restarts/replicas |
custom_endpoints | see below | File/env only, and never returned by the API. Where a customer-configured outbound request may go — LLM connection base URLs, and the legacy needs_review webhook. Notification channels use their own notifications.egress policy instead. Same reasoning as the key above: an admin who could widen this from inside the product could widen it to the cloud instance metadata endpoint, which makes it a privilege boundary rather than a setting |
llm.custom_endpoints
Section titled “llm.custom_endpoints”| Key | Default | Description |
|---|---|---|
enabled | false | Whether a connection may name a custom base_url at all. false — the safe multi-tenant default — restricts every organization to the hosted providers’ own endpoints, and makes provider_kind: "openai_compatible" unusable since it has no default address. Self-hosted deployments ship true |
allow_private_networks | false | Whether a custom address may resolve into RFC1918/loopback/unique-local space. true is correct, and necessary, for a self-hosted deployment whose model runs in its own datacentre |
allowed_hosts | [] | Exact hostnames, or *.suffix patterns. Empty means “any host the address rules permit” — a restriction on where, not on who. *.corp.example matches llm.corp.example but not corp.example |
Two rules hold whatever this section says, because no deployment ever means to permit them:
link-local is always blocked (169.254.0.0/16, fe80::/10 — cloud instance metadata), and
the resolved address is checked, not the hostname, since a policy that trusts the name is
defeated by any name whose A record points at 127.0.0.1. Every address a name resolves to must
pass, and the check runs again each time a connection is used — a stored row outlives its
validation. See LLM Providers and Connections.
llm is the one section split across the two regimes: enabled is admin-editable, the key is
not. That is a per-field distinction, not a per-section one — the key is registered nowhere, so
it is absent from the PATCH body model (sending llm_credential_encryption_key is a 422),
absent from effective in GET /admin/settings, and left at its file/env value rather than
being reset to a fresh per-process default at boot.
The key encrypts storage, not any particular provider — it’s shared across whichever of
anthropic/openai/mistral/openai_compatible an org stores a connection for. See
Server-Side Analysis and
LLM Providers and Connections.
With enabled: false, these return 501: POST /{analyzer}/{namespace}/analyze,
POST /{namespace}/analyze/auto, POST /orgs/{org_id}/llm-credentials and
POST /orgs/{org_id}/llm-connections. Listing and deleting stored connections deliberately keep
working, so an operator who turns the feature off can still see and clean up what organizations
left behind. Ingest, analyzer listing and the
client-led analysis workflow are unaffected — none of them call a provider.
notifications
Section titled “notifications”Outbound notification delivery — see Notifications for the
guide. Excluded whole from the admin API, unlike llm which exposes its harmless half:
every field here is either a privilege boundary or a limit protecting the server rather than
the organization.
| Key | Default | Description |
|---|---|---|
egress | see below | File/env only. Where a notification channel may be pointed |
max_channels_per_org | 20 | Refused at create time with a 409. A soft ceiling on fan-out, not an entitlement — notifications are free on every deployment mode |
timeout_seconds | 5.0 | Per-delivery HTTP timeout, 1.0–30.0. Deliveries never block a response, but they do hold a background task |
notifications.egress
Section titled “notifications.egress”Same EgressPolicy shape as llm.custom_endpoints above — enabled,
allow_private_networks, allowed_hosts — and the same two unconditional rules: link-local is
always blocked, and the resolved address is what gets checked. Only the defaults differ.
| Key | Default | Description |
|---|---|---|
enabled | true | false disables notification channels outright. Note this differs from llm.custom_endpoints.enabled, which defaults to false: a notification channel is a customer-named address by definition, so false here is “the feature is off”, not “use the built-in endpoints” |
allow_private_networks | false | Set true on a self-hosted deployment notifying an internal Mattermost or webhook |
allowed_hosts | [] | Empty means any host the address rules permit |
Deliberately a separate policy from llm.custom_endpoints. The two answer different
questions and deployments routinely want opposite answers: a cloud instance ships
llm.custom_endpoints.enabled: false — every org uses the hosted providers’ own endpoints —
while still needing to reach hooks.slack.com. Sharing one policy would couple “I run my own
model” to “I can send a finding to an address inside my network”.
A destination is checked twice: at save time, so an admin gets an explanatory 422 rather than
a channel that stores fine and silently never delivers, and again on every delivery, because a
hostname that resolved to a permitted address when it was saved can resolve elsewhere later.
two_factor
Section titled “two_factor”| Key | Default | Description |
|---|---|---|
secret_encryption_key | random per-process | Encrypts enrolled users’ TOTP secrets at rest (Fernet). Same must-be-set-in-production rule as jwt.secret and llm.credential_encryption_key via TWO_FACTOR_SECRET_KEY — otherwise every already-enrolled secret becomes unreadable across restarts/replicas |
File/env only, like the other two encryption keys — see the exclusion table in
Platform settings above. Note this is only the
encryption key: whether an org requires 2FA is per-org data
(PATCH /admin/organizations/{org_id}/2fa-policy), not config. See
Two-Factor Authentication.
rate_limit
Section titled “rate_limit”Attempt budgets for the endpoints that verify a credential.
This section is split: the five thresholds are admin-editable, enabled is file/env only.
Change the thresholds at Platform Admin → Settings → Security (or PATCH /admin/settings) —
they apply to the very next request, no restart, and anything a config file says about them is
discarded at boot like every other admin-editable section. enabled is the opposite: it is read
from the file, is never returned by GET /admin/settings, and PATCH rejects it with a 422.
The asymmetry is the point. Tightening or loosening a threshold is an ordinary operational
decision, and the bounds below cap how loose “loose” can get. Turning the protection off
altogether is the single change a stolen superadmin session would most want to make, and the one
an operator couldn’t undo from inside a compromised deployment — so it requires reaching the
deployment’s config, alongside the ingress rules that are the other half of the same protection.
(The llm section is split the same way and in the opposite direction, for the mirror-image
reason — there the toggle is the safe live decision and the secret is the dangerous one.)
| Key | Default | Editable | Description |
|---|---|---|---|
enabled | true | file/env only | Whether the application layer runs at all. Only sensible to turn off when something in front already does this (an API gateway, WAF, Cloudflare) — off means unlimited failed attempts |
login_max_attempts | 10 | ✅ | Failed logins per account (1–1000) before POST /auth/session stops checking the password at all. Cleared by any successful login |
login_window_minutes | 15 | ✅ | The window that budget is measured over (1–1440) |
mfa_max_attempts | 5 | ✅ | Second-factor attempts per account (1–100), over a fixed 15-minute window. Counts correct codes too — a challenge ends either way. Cleared by a successful second factor |
ip_max_attempts | 60 | ✅ | Failed credential presentations per source IP (1–10000), across every credential type at once: login, API key, SCIM bearer token, invite and public-report link tokens |
ip_window_minutes | 15 | ✅ | The window for the per-IP budget (1–1440) |
Exceeding a budget answers 429 with a Retry-After header. The response to the attempt that
crosses a threshold is unchanged (a 401 stays a 401), so nothing in the API reveals where the
line sits — only the next attempt is turned away.
Two budgets, keyed differently, on purpose. The per-account ones are the half a distributed password-spray cannot dodge by rotating source addresses; the per-IP one is the catch-all for the guessing surfaces that have no account to key a budget to. A success clears an account’s budget but never the IP’s — that one is keyed by something many unrelated users share (a NAT, an office egress), so letting a success reset it would hand an attacker a free reset by authenticating to their own account.
github
Section titled “github”Configures the platform’s built-in GitHub App integration — the fallback identity used by orgs that install the shared App rather than registering their own via the manifest flow (POST /orgs/{org_id}/integrations/github/manifest). Orgs using the platform App “install” it and register their own installation_id separately (POST /orgs/{org_id}/integrations/github); orgs using their own App need none of this section set at all.
Not set in this file or via GITHUB_* env vars — change it live via PATCH /admin/settings
(github_app_id/github_private_key/github_webhook_secret), no restart — see
Platform settings above.
| Key | Default | Description |
|---|---|---|
app_id | null | GitHub App ID |
private_key | null | App’s PEM private key (RS256), used to sign App-level JWTs. Write-only |
webhook_secret | null | HMAC-SHA256 secret configured on the App’s webhook — verifies X-Hub-Signature-256. Write-only |
deployment
Section titled “deployment”| Key | Default | Description |
|---|---|---|
mode | self_hosted | self_hosted or cloud |
File/env only (DEPLOYMENT_MODE) — never admin-API-editable, even though nothing
structurally prevents it the way mcp/telemetry are prevented; it identifies which
deployment this process is, not a business setting an authenticated admin should
ever flip live.
self_hosted(the default — every OSS/quickstart/Helm config leaves this unset) — every org is unconditionally entitled to run every analyzer, regardless ofplan_tier/demo_analyses_used.cloud(set only on the operator’s own SaaS config) — an org needsplan_tier == "paid", or is still within its one-time, lifetime 100-analysis free demo. See Billing / quota enforcement for the full mechanism (controllers/entitlements.py).