Skip to content

Configuration

l0rdg3x edited this page Jun 15, 2026 · 5 revisions

Configuration

A complete reference for every environment variable and runtime setting OPNGMS understands. Most variables live in your .env file (see Installation for the step-by-step); a handful of operational settings are configured in-app rather than via the environment. For upgrades and key rotation see Upgrading; for the log lake see Log-Lake; for security hardening see Security.


Contents


How to read this page

  • Variable — the exact environment-variable name (read from .env, the Compose environment: blocks, or env_file: .env).
  • Default — the value applied when the variable is unset. means there is no default; the variable is required.
  • Required? — whether the app refuses to start (or misbehaves) without an explicit value.
  • Variables backed by the application settings model are sourced from backend/app/core/config.py. Variables consumed only by Docker Compose or the frontend image are noted as such.

Note: Pydantic settings names are lowercase in code (e.g. database_url); the environment-variable form is uppercase (DATABASE_URL). Both refer to the same setting — set the uppercase form in .env.


Database & roles

OPNGMS uses two database identities so Postgres Row-Level Security can enforce tenant isolation: a non-superuser opngms_app role for the API (RLS-enforced) and the database owner for migrations and the worker (RLS-exempt, trusted backend only). This means there are two password pairs that must match — set them once and keep each pair aligned, or the app cannot connect.

Variable Default Required? Description
POSTGRES_USER opngms Yes Owner/superuser the TimescaleDB container creates on first boot. Consumed by the db service.
POSTGRES_PASSWORD Yes Password for POSTGRES_USER, created on first boot. Must equal the password embedded in ADMIN_DATABASE_URL.
POSTGRES_DB opngms Yes Database name created on first boot.
DATABASE_URL Yes Async DSN the API uses, connecting as the non-superuser opngms_app role (RLS enforced). Form: postgresql+asyncpg://opngms_app:<pw>@db:5432/opngms. Its embedded password must equal APP_ROLE_PASSWORD.
APP_ROLE_PASSWORD Yes Password the migrate job uses to CREATE/ALTER the opngms_app role (interpolated into SQL DDL — no single quotes). Must equal the password embedded in DATABASE_URL.
ADMIN_DATABASE_URL unset Yes (prod) Async DSN the worker and migrations use, connecting as the owner (DDL + fleet-wide writes; bypasses RLS). Its embedded password must equal POSTGRES_PASSWORD. Compose also exports this value as ALEMBIC_DATABASE_URL to the migrate service.
TEST_DATABASE_URL unset No Optional override used only by the test suite; never set in production.

The two pairs that must match:

Password in… …must equal Role it authenticates
DATABASE_URL APP_ROLE_PASSWORD Non-superuser opngms_app — API, RLS-enforced
ADMIN_DATABASE_URL POSTGRES_PASSWORD DB owner — migrations + worker, RLS-exempt

See the matching table and worked example in Installation; this page does not repeat the example.


Secrets

Generate fresh values for these on every install — never reuse the .env.example placeholders. The API fails closed at startup if any guarded secret still contains the literal substring change-me (the placeholder shipped in .env.example).

Variable Default Required? Description
SESSION_SECRET Yes Server-side session signing key. Generate with python -c "import secrets; print(secrets.token_urlsafe(48))". Rotating it invalidates all active sessions.
MASTER_KEY Yes Fernet key (urlsafe-base64) encrypting all secrets at rest — device API credentials, config snapshots, MFA TOTP secrets, the SMTP password, and the syslog CA key. Generate with python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Losing it makes all stored secrets undecryptable. Rotation procedure: see Upgrading.
MASTER_KEY_OLD_KEYS "" (empty) No Comma-separated retired Fernet keys, kept for decryption only during a key rotation. Leave empty on a fresh install; see the rotation procedure in Upgrading.

Fail-closed change-me guard

assert_secure_secrets() (in backend/app/core/config.py) rejects startup if any of these still contain change-me:

  • POSTGRES_PASSWORD
  • DATABASE_URL (its embedded password)
  • APP_ROLE_PASSWORD
  • ADMIN_DATABASE_URL (its embedded password)
  • SESSION_SECRET
  • MASTER_KEY

Real values — including dev, test, and CI — never contain the placeholder, so the guard is a no-op for them. See Security for hardening guidance on these secrets.


Image & version

Variable Default Required? Description
OPNGMS_VERSION latest No Pins the ghcr.io/l0rdg3x/opngms-{backend,frontend} image tag the Compose files pull. latest resolves to the newest semver release (images are published only from release tags, never from dev commits). Pin a specific tag (e.g. 0.1.0) for reproducible deploys. See Upgrading for the bump-and-redeploy flow.

Timezone

Variable Default Required? Description
TZ UTC No IANA timezone name (e.g. Europe/Rome, America/New_York) applied to the db, api, worker, and frontend containers so logs read in local time. All data is stored in UTC (timestamptz), and scheduled-report hours are interpreted in UTC by design — changing TZ does not shift when a report fires, only how timestamps appear in container logs.

TLS & frontend exposure

OPNGMS supports four TLS models, selected by which Compose overlay you stack on top of docker-compose.prod.yml (full walkthrough in Installation). Each variable below applies to specific models.

Variable Default Required? Used by Description
FRONTEND_BIND 127.0.0.1 No Model 1 (base) Host bind address of the plain-HTTP frontend port. Keep on localhost unless an upstream proxy on another machine needs 0.0.0.0 — and only if something terminates TLS in front of it.
FRONTEND_HTTP_PORT 8080 No Model 1 (base) Host port the base frontend listens on (mapped to container port 80). Your upstream proxy forwards here.
TLS_MODE off No Frontend image Frontend image switch: off = terminate TLS upstream (base); the built-in-TLS overlay flips this to builtin. Set by the Compose overlays — you normally do not set it by hand.
SERVER_NAME _ (Compose) / opngms.example.com (.env.example) No Models 1 & 2 Your hostname, used as nginx server_name for the built-in TLS server; informational otherwise.
CERT_DIR ./certs No Model 2 Host directory mounted read-only into nginx containing fullchain.pem + privkey.pem. If absent, a self-signed cert is generated at startup so the container still boots.
HTTP_PORT 80 No Models 2, 3a, 3b Published HTTP host port (used for the HTTP→HTTPS redirect and ACME HTTP-01 challenge).
HTTPS_PORT 443 No Models 2, 3a, 3b Published HTTPS host port.
DOMAIN opngms.example.com Yes (3a/3b) Models 3a (Caddy), 3b (Traefik) Public hostname Let's Encrypt issues a certificate for. The Caddy/Traefik overlays require it (${DOMAIN:?…}) — DNS must resolve to this host and ports 80/443 must be internet-reachable.
ACME_EMAIL you@example.com Yes (3a/3b) Models 3a, 3b ACME account email for Let's Encrypt. Required by the Caddy/Traefik overlays (${ACME_EMAIL:?…}).

Note: HTTPS is mandatory in production — browsers drop Secure session cookies over plain HTTP on a real domain, which breaks login. With Model 1, your upstream proxy must forward X-Forwarded-Proto: https. See Installation for per-model setup and Security for TLS hardening.


Broker & sessions

Variable Default Required? Description
REDIS_URL redis://localhost:6379 (code) / redis://redis:6379 (.env.example) No Redis connection URL used as the broker/cache. The bundled redis service is reachable at redis://redis:6379 inside the Compose network.
SESSION_TTL_HOURS 12 No Absolute session lifetime — a session is invalid after this many hours regardless of activity.
SESSION_IDLE_MINUTES 120 No Sliding/idle timeout — a session expires after this many minutes of inactivity, alongside the absolute SESSION_TTL_HOURS.
CORS_ALLOW_ORIGINS "" (empty) No Comma-separated list of allowed CORS origins. Empty = CORS disabled (same-origin only), which is correct for the standard single-origin deployment. Set only if you serve the SPA from a different origin than the API.

Login & MFA

Variable Default Required? Description
LOGIN_MAX_ATTEMPTS 5 No Failed-login attempts allowed before lockout.
LOGIN_LOCKOUT_WINDOW_SECONDS 900 No Lockout window in seconds (default 15 minutes) over which failed attempts are counted.
MFA_PENDING_TTL_MINUTES 5 No Lifetime of the short-lived mfa_pending challenge — the window after a correct password during which the TOTP code must be submitted (POST /api/login/mfa).

The MFA enforcement policy (off / all / privileged) is set in-app under Two-factor auth, not via environment. See Security.


Worker cadences & cron

These tune the background worker's polling and scheduled jobs. Defaults are sensible for most fleets; change them only with cause.

Variable Default Required? Description
POLL_INTERVAL_SECONDS 60 No Base device-polling interval in seconds.
INGEST_EVERY_MINUTES 5 No Event-ingest cadence (Suricata alerts, DNS queries) in minutes; valid range 1–30.
CONFIG_BACKUP_HOUR 3 No UTC hour (0–23) of the daily device config backup.
SWEEP_EVERY_MINUTES 5 No Cadence of the orphaned-scheduled-action sweeper in minutes; valid range 1–30.
ORPHAN_GRACE_MINUTES 5 No How long a scheduled row may be overdue before the sweeper touches it.
MAX_REENQUEUE_ATTEMPTS 5 No Give up on an orphaned action after this many device-free re-enqueues.
SESSION_CLEANUP_MINUTE 0 No Minute-of-hour the hourly expired-session cleanup runs.
REPORT_WEEKDAY mon No Legacy default weekly-report day (monsun), superseded by per-schedule weekday/hour configured in-app.
REPORT_HOUR 4 No Legacy default weekly-report UTC hour (0–23), superseded by per-schedule settings configured in-app.
DEVICE_CERT_DAYS 90 No Lifetime in days of a device's syslog-forwarding certificate (short lifetime bounds a stolen-key window). Log lake only.
CERT_RENEWAL_WINDOW_DAYS 30 No Renew a forwarding cert when its not_after falls within this many days. Log lake only.
CERT_RENEWAL_HOUR 3 No UTC hour the daily cert-renewal cron runs. Log lake only.
SILENT_ALERT_ENABLED true No Master switch for the silent-tenant detector cron (alerts when a tenant stops shipping logs).
SILENT_ALERT_AFTER_HOURS 6 No Alert when a tenant has been silent longer than this many hours (the UI badge uses 1h).
SILENT_ALERT_CRON_MINUTE 0 No Minute of each hour the silent-tenant detector runs.

Boot-time tuning

These variables tune performance and concurrency limits that are read once at process startup. Changing one requires a restart of the affected service (docker compose up -d to recreate the container). Each default reproduces the prior, pre-v0.8.0 behaviour, so an existing deployment needs no changes unless you are deliberately tuning it.

Variable Default Required? Description
WORKER_MAX_JOBS 10 No Maximum number of jobs the ARQ worker runs concurrently. Operations on different devices run in parallel up to this cap; a single device always serializes its operations behind a per-device advisory lock, so raising this never causes two concurrent writes to the same box. Raise it for larger fleets, lower it to cap worker load.
DB_POOL_SIZE 5 No SQLAlchemy connection-pool size, applied to both the API engine (opngms_app, RLS-enforced) and the worker engine (owner, RLS-exempt).
DB_MAX_OVERFLOW 10 No SQLAlchemy pool overflow — extra connections opened beyond DB_POOL_SIZE under burst load, also applied to both engines. Effective ceiling per engine is DB_POOL_SIZE + DB_MAX_OVERFLOW; size your Postgres max_connections to cover both engines plus headroom.
OPNSENSE_HTTP_TIMEOUT 10.0 No Default per-request timeout, in seconds, for outbound calls from the worker to managed OPNsense boxes (a deploy/network characteristic). Raise it for slow links or heavily loaded boxes; lower it to fail faster.

Note: The repo ships a comprehensive .env.example organised into three commented sections — Required secrets · Boot-time tuning (requires restart) · Runtime defaults — so you can see every tunable, its default, and which tier it belongs to in one place. The boot-time variables above live in the middle section.


Runtime settings (in-app)

A set of operational settings can be changed live, without a restart, by a superadmin under System → Runtime settings. These are layered over the application's key/value store as a generic settings registry: the value from .env (or the code default) is the default, and a database override — when set — wins. Each setting shows its default in the UI and can be reset back to it.

Reads and writes go through GET / PUT /api/admin/settingssuperadmin-gated, CSRF-protected, and audited. Because the default is sourced from the environment, you can pre-seed a value in .env (the Runtime defaults section of .env.example) and still override it later in the UI without a redeploy.

The runtime-editable settings:

Setting Default Description
firmware_max_status_polls 30 How many times the worker polls a box for firmware-update status before giving up.
firmware_poll_interval_seconds 10 Seconds between firmware status polls. Together with firmware_max_status_polls this bounds the total firmware-poll budget.
catalog_auto_fetch true Fetch and cache version-aware config catalogs on a cache miss. The runtime mirror of CATALOG_AUTO_FETCH; set false for cache-only (air-gapped) operation.
geoip_auto_fetch true Fetch and cache the offline GeoIP database on a cache miss. Set false for air-gapped operation, where the database must be pre-seeded.
silent_alert_enabled true Master switch for the silent-tenant detector (alerts when a tenant stops shipping logs). The runtime mirror of SILENT_ALERT_ENABLED.
silent_alert_after_hours 6 Alert when a tenant has been silent longer than this many hours. The runtime mirror of SILENT_ALERT_AFTER_HOURS.
login_max_attempts 5 Failed-login attempts allowed before lockout. The runtime mirror of LOGIN_MAX_ATTEMPTS.
login_lockout_window_seconds 900 Lockout window in seconds (default 15 minutes) over which failed attempts are counted. The runtime mirror of LOGIN_LOCKOUT_WINDOW_SECONDS.
session_ttl_hours 12 Absolute session lifetime in hours, regardless of activity. The runtime mirror of SESSION_TTL_HOURS.
session_idle_minutes 120 Sliding/idle session timeout in minutes. The runtime mirror of SESSION_IDLE_MINUTES.
perimeter_retention_days 30 Global default retention, in days, for the perimeter rollup (failed logins + firewall blocks). Per-tenant-overridable.
events_retention_days 90 Global default retention, in days, for the events hypertable (IDS / DNS). Per-tenant-overridable.
metrics_retention_days 30 Global default retention, in days, for device-health metrics. Per-tenant-overridable.
log_lake_retention_days 30 Global default retention, in days, for the log lake (OpenSearch). Bridges the existing LOG_RETENTION_DAYS. Per-tenant-overridable. See Log-Lake.

Note: Where a setting mirrors an environment variable (the table above), the .env value is only the default: once a superadmin saves an override in Runtime settings, the override takes precedence until it is reset. To change one of these permanently for a fresh install set it in .env; to change one now on a running deployment use the System page.

Do these survive a restart?

Yes. Every in-app override — the global Runtime settings and each tenant's per-tenant retention — is stored in Postgres (app_setting.runtime_config for the global values; tenant_retention for the per-tenant ones), which lives in the opngms_pg named volume. So they persist across a restart, an image upgrade, and docker compose down followed by up — restarting the stack does not reset settings to the .env/built-in defaults. The .env value is read at startup only as the fallback for a setting that has no override yet, and editing .env after you've overridden a setting in the UI has no effect (the DB override wins — change it in the UI instead).

Action Effect on settings (and data)
docker compose restart / stop+start / down then up Volume intact → all overrides (and data) persist. No reset.
docker compose down -v The -v destroys the named volumes → Postgres is wiped, all data and overrides are lost, and the stack starts again from the .env/built-in defaults.

The same persistence applies to the underlying data: metrics / events / the perimeter rollup live in opngms_pg; the log lake's indices live in the opngms_os volume — both survive a restart and are only lost by removing the volume.

Per-tenant data retention (global default + per-tenant override)

The four *_retention_days settings above are the global defaults for how long OPNGMS keeps the data behind dashboards and reports — across four stores: the perimeter rollup, the events hypertable (IDS / DNS), device metrics, and the log lake (OpenSearch). Tenant-aware worker purge jobs own deletion.

Each default can be overridden per tenant from that tenant's settings page (a "Retention" card; tenant_admin / superadmin, via GET / PUT /api/tenants/{id}/retention). The effective retention for a store is per-tenant override ?? global default — an override may be longer or shorter than the global default.

Note: Retention is consistency-checked against reports — a report cannot be configured to cover more days than the tenant's effective retention for the stores its enabled sections use. Over-long on-demand and scheduled reports are blocked; lowering retention is allowed but surfaces a warning (and, for a global lowering, the list of impacted tenants). See Reporting and Log-Lake.


Live config push

Variable Default Required? Description
LIVE_PUSH_ENABLED false No Master switch for real configuration push to managed OPNsense boxes. Default false runs every apply as a dry-run; set true only when you intend the worker to write live changes. See Configuration-Editor.

Catalog distribution

These control where the version-aware config editor fetches versioned OPNsense catalogs (see Configuration-Editor).

Variable Default Required? Description
CATALOG_RELEASE_BASE_URL https://github.com/l0rdg3x/OPNGMS/releases/download/catalogs No Base URL the app fetches versioned catalogs from on a cache miss.
CATALOG_AUTO_FETCH true No Fetch and cache catalogs on a cache miss. Set false for cache-only (air-gapped) operation, where catalogs must be pre-seeded.

Log lake

The log lake is an opt-in overlay (docker-compose.logs.yml, docker-compose.logs.multinode.yml, or bundled in docker-compose.full.yml). These variables are unused unless an overlay is active. See Log-Lake for the full bring-up, network requirements, and the multi-node HA option.

Variable Default Required? Description
SYSLOG_RECEIVER_HOST logs.opngms.local (code) / logs.opngms.example (.env.example) No Public name/IP that managed devices ship logs to over mTLS syslog. Must be reachable by the devices.
SYSLOG_TLS_PORT 6514 No TLS syslog (RFC 5425) listener port published by the syslog-ng receiver; must be reachable by managed devices.
OPENSEARCH_URL http://opensearch:9200 (single-node) / http://opensearch-n1:9200 (multi-node) No Internal URL the backend and receiver use to reach OpenSearch. OpenSearch is internal-only (plain HTTP, not published).
LOG_RETENTION_DAYS 30 No Global default retention horizon in days for ingested logs, bridged by the log_lake_retention_days runtime setting and per-tenant-overridable (Retention card). Enforced by the purge_log_lake worker job, not an ISM policy. See Log-Lake.
LOG_SEARCH_MAX_SIZE 200 No Maximum hits returned per log-search request.
LOG_SEARCH_MAX_RANGE_DAYS 31 No Maximum time span (days) a single log search may cover.
LOG_FLEET_TERMS_SIZE 10000 No Max tenants in the MSP log-fleet terms aggregation (sized to avoid silent truncation).

Note: The multi-node overlay (docker-compose.logs.multinode.yml) sets OpenSearch clustering directly in the Compose file — cluster.name, discovery.seed_hosts, cluster.initial_cluster_manager_nodes, node.name, and OPENSEARCH_JAVA_OPTS (heap). These are container settings, not .env variables; point OPENSEARCH_URL at the first node (opensearch-n1). See Log-Lake.


Where configuration lives

OPNGMS configuration comes from three places:

  1. .env (gitignored). The single file that holds all secrets and host-specific values. Copy it from .env.example, fill it in, and never commit it. The api, worker, and migrate services load it via env_file: .env; the db and frontend services read individual ${VAR} interpolations. This is where every variable in the tables above belongs.

  2. Compose environment: blocks. A few values are derived or fixed inside the Compose files rather than typed into .env — e.g. ALEMBIC_DATABASE_URL (set to ${ADMIN_DATABASE_URL} for the migrate service) and TLS_MODE (flipped to builtin by the TLS overlay). The multi-node OpenSearch clustering keys live entirely in docker-compose.logs.multinode.yml. You normally do not edit these.

  3. In-app settings (not environment variables). Some operational configuration is intentionally stored in the database and managed through the admin UI, with secrets encrypted at rest using MASTER_KEY:

    • SMTP delivery — relay host, port, security, username/password, and default From address. Configured under Admin → SMTP delivery; there are no SMTP variables in .env. See Reporting.
    • Per-tenant report settings & schedules — report title, logo, language, white-label sender, and weekly/monthly/on-demand schedules with recipient lists. Configured per tenant in the console. See Reporting.
    • MFA enforcement policy and per-user TOTP enrolment — configured under Two-factor auth. See Security.
    • Runtime settings — the live-editable tunables described in Runtime settings (in-app), under System → Runtime settings (including the four global retention defaults, each per-tenant-overridable from a tenant's Retention card). For these the .env/code value is only the default; a superadmin's database override wins until reset, no restart required.

For the install-time walkthrough of .env and TLS, see Installation. For rotating MASTER_KEY / SESSION_SECRET and bumping OPNGMS_VERSION, see Upgrading.

Clone this wiki locally