Skip to content

Security

l0rdg3x edited this page Jul 29, 2026 · 10 revisions

Security

The security model and hardening guide for OPNGMS — how tenant isolation, secrets-at-rest, authentication, MFA, transport security, and the supply chain are enforced, plus an operator checklist. For TLS deployment models see Installation; for key rotation see Upgrading; for the log lake see Log-Lake; for every environment variable see Configuration; for how the pieces fit together see Architecture.

Two-factor authentication settings & policy

Contents


Tenant isolation (Postgres RLS)

OPNGMS is a multi-tenant console: one deployment serves many customer tenants, and a row belonging to tenant A must never be visible to tenant B. This is enforced at the database layer with PostgreSQL Row-Level Security (RLS), not only in application code — so even a logic bug in a query cannot leak across tenants.

Two database roles

Role Used by RLS status Created by
opngms_app (non-superuser) The API (DATABASE_URL) EnforcedNOSUPERUSER NOBYPASSRLS Migration 0003
owner (e.g. opngms) migrate and worker (ADMIN_DATABASE_URL) Exempt — Postgres superuser bypasses RLS The TimescaleDB image (POSTGRES_USER)

The opngms_app role is created (or re-aligned) by the migration with exactly:

CREATE ROLE opngms_app LOGIN PASSWORD '...'
    NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;

PostgreSQL superusers always bypass RLS — even with FORCE ROW LEVEL SECURITY — so the API must connect as a non-superuser, NOBYPASSRLS role for isolation to mean anything. The worker and migrations connect as the owner because they perform fleet-wide writes and DDL (creating roles, tables, and policies) that legitimately span all tenants; they are trusted backend processes that never serve untrusted request input.

Note: The password embedded in DATABASE_URL must equal APP_ROLE_PASSWORD, and the password in ADMIN_DATABASE_URL must equal POSTGRES_PASSWORD. See Installation for the two password pairs.

Per-request tenant context

Every tenant-scoped table has the same policy, applied with FORCE ROW LEVEL SECURITY and named tenant_isolation:

CREATE POLICY tenant_isolation ON <table>
    USING      (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
    WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);

The policy reads a session variable, app.current_tenant. On each authenticated, tenant-scoped request — after the membership and authorization checks pass — the API sets it transaction-locally:

await session.execute(
    text("SELECT set_config('app.current_tenant', :tid, true)"),
    {"tid": str(tenant_id)},
)

The third argument (true) scopes the setting to the current transaction, equivalent to SET LOCAL, so it cannot leak into a later request that reuses the pooled connection.

The design is fail-closed: NULLIF(current_setting('app.current_tenant', true), '')::uuid turns a missing or empty context into NULL, which the predicate never matches — so a request that forgets to set the tenant sees zero rows, never all rows. RLS currently covers the per-tenant tables (devices, metrics, alerts, events, config snapshots/changes, report settings/schedule/generated reports, firmware actions, template overrides, log forwarding, and the revoked-syslog-cert ledger), each extended in its own migration as the schema grew.

Note: A whole-project audit by six independent reviewers found no cross-tenant access or auth-bypass issues. RLS isolation is also exercised directly by the backend test suite (test_rls_isolation.py and per-domain test_*_rls_api.py), and production and test policies share a single source module so they cannot drift apart.


Perimeter threat visibility

The Security / Perimeter feature (shipped in v0.9.0) surfaces who is knocking on the edge of each managed firewall. It polls two threat signals per device through the existing SSRF-guarded OpnsenseClient — the same hardened connector used for every outbound call to a managed box (HTTPS only, no redirects, loopback/link-local blocked) — so no new outbound path is introduced.

Two signals

Signal OPNsense source What is extracted
Failed logins The audit logPOST /api/diagnostics/log/core/audit, process audit The attacker source IP + the attempted username.
Firewall blocks The structured firewall log/api/diagnostics/firewall/log, filtered to action=block The attacker source IP + the targeted port/interface.

Each attacker IP is then resolved to a country via the existing offline GeoIP layer — the same MaxMind-style local database used by the attacker-countries view — so attribution needs no third-party lookup at request time.

Note: OPNsense's diagnostics-log API is POST, not GET, even for a read. The line parser is fail-safe: an unrecognized log line is skipped rather than aborting the ingest, so a format change on a single line never poisons the whole poll.

Bounded rollup, not per-packet

Storage is deliberately light. OPNGMS does not store per-packet or per-event rows; instead it keeps a bounded perimeter_attacker rollup keyed by (device, kind, source-IP), carrying a count, first_seen, last_seen, and a small detail payload. Because the key is the distinct attacker IP, total storage scales with the number of distinct attackers, not with traffic volume — a box under a sustained scan adds rows only for new source IPs, not for every probe. A daily retention sweep prunes rows not seen within the window (~30 days), so the table self-bounds over time.

Ingest runs inside the existing per-device events cron, wrapped in a database SAVEPOINT: if a perimeter poll hiccups (an unreachable box, a malformed log page), the savepoint rolls back only the perimeter write — the surrounding metrics/events ingest commits normally and is never lost.

Multi-tenancy & review

perimeter_attacker is a tenant-scoped table carrying tenant_id and the same fail-closed tenant_isolation RLS policy as every other tenant table (see Tenant isolation). The split-role rule holds: the worker writes as the owner (RLS-exempt trusted infra, ADMIN_DATABASE_URL) during the cron, while the API reads as opngms_app (RLS enforced) under the per-request tenant context. Both the ingest path and the read endpoint were security-reviewed before shipping.

Where it shows in the UI

  • Overview — two summary cards (failed logins, firewall blocks) for the tenant's fleet.
  • Perimeter page — a dedicated view ranking attackers per source IP, with a 24h / 7d / 30d window selector.
  • PDF report — the two perimeter PDF sections are toggled with all the other report sections (Report settings → Report sections; per-tenant default + optional per-schedule override), exactly like the Executive summary, Attacks, and Attacker-countries sections.

Secrets at rest

Sensitive credentials are encrypted before they touch the database using a Fernet key supplied as MASTER_KEY (generate one with python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())").

Encrypted at rest with MASTER_KEY:

  • Device API credentials — each managed OPNsense box's API key and secret are stored only as api_key_enc / api_secret_enc and decrypted transiently, in memory, solely to talk to that box.
  • SMTP password — the relay password configured in Admin → SMTP delivery is stored as password_enc.
  • Config snapshots — captured config.xml is gzipped then Fernet-encrypted.
  • Syslog CA private key — the internal CA that signs the log-lake's per-device mTLS certs. Beyond Fernet encryption, it is held least-privilege: the encrypted key lives in its own owner-only table (syslog_ca_key) that the opngms_app API role cannot SELECT at all (the blanket table grant is revoked). The cert-signing path reaches it only through a single SECURITY DEFINER accessor function (opngms_syslog_ca_key(), EXECUTE granted to opngms_app), so a generic read primitive (SQLi / mass-export) can no longer exfiltrate the key alongside everything else — it must invoke that one named function. CA creation is owner-only (the bootstrap/worker), and the worker reads the key owner-side to sign the revocation CRL (see Log-Lake → Certificate revocation).

These secrets are never returned by the API. The device response schema deliberately omits the encrypted fields (they are write-only), the SMTP response exposes only a has_password: bool, and the audit log explicitly records details={} for credential operations so secrets never reach a log line.

Rotation

MASTER_KEY rotation is supported via MASTER_KEY_OLD_KEYS — a comma-separated list of retired keys. The crypto layer builds a MultiFernet whose first key (the current MASTER_KEY) performs all new encryption while any listed key can still decrypt older ciphertext. A re-key script then re-encrypts existing rows under the new primary key, after which the old key can be dropped. See Upgrading for the full rotation procedure.

Note: Re-keying connects as the RLS-exempt owner (ADMIN_DATABASE_URL) because it must touch every tenant's rows, and it fails loudly if that URL is not configured rather than silently re-keying nothing.


Authentication & sessions

Sessions are server-side and database-backed, not encoded into the cookie. The cookie carries only an opaque random token (secrets.token_urlsafe(32)); the server stores a keyed HMAC-SHA256 of that token using SESSION_SECRET. A database dump therefore yields only keyed hashes, and rotating SESSION_SECRET invalidates every existing session at once.

The session cookie is issued with hardening flags:

Flag Value Why
HttpOnly true JavaScript cannot read the session token (XSS containment).
Secure true The cookie is only sent over HTTPS.
SameSite lax Limits cross-site submission.

A companion CSRF cookie (opngms_csrf) is intentionally readable by the SPA (HttpOnly false, still Secure) for a double-submit check: state-changing requests must echo the value in an X-OPNGMS-CSRF header, compared in constant time against the session's stored token. Sessions expire on both an absolute TTL (default 12 hours) and a sliding idle timeout (default 120 minutes).

Because the cookie is Secure, HTTPS is mandatory in production — browsers silently drop Secure cookies sent over plain HTTP on a real domain, which breaks login entirely.

Behind a reverse proxy

When OPNGMS runs behind a TLS-terminating proxy (the recommended Model 1 in Installation), the upstream must forward the original scheme so the backend knows the request arrived over HTTPS and keeps issuing Secure cookies:

X-Forwarded-Proto: https

The stack honours this at two layers: uvicorn runs with --proxy-headers --forwarded-allow-ips "*", and the bundled nginx preserves the upstream X-Forwarded-Proto end-to-end. Without this header, logins succeed via curl but loop back to the sign-in page in a browser. See Installation for the per-model TLS setup and Troubleshooting for the login-loop symptom.


Multi-factor authentication (MFA)

OPNGMS supports two second factors — TOTP (RFC 6238 authenticator apps) and WebAuthn passkeys (platform authenticators like Face/Touch ID or Windows Hello, and roaming security keys like a YubiKey). A user may enrol either or both; the login challenge is satisfied by a passkey or a TOTP code.

Enrolment

Enrolment is a password-re-authenticated flow under Two-factor auth:

  1. TOTPPOST /api/me/mfa/setup (re-auth with the account password) generates a random base32 secret, stores it Fernet-encrypted (MASTER_KEY), and returns an otpauth:// provisioning URI (issuer OPNGMS, account = the user's email) plus the raw secret for the QR code; POST /api/me/mfa/confirm verifies a 6-digit code, marks MFA enabled, and issues recovery codes.
  2. PasskeyPOST /api/me/mfa/webauthn/register/{begin,complete} (also password-re-authenticated) runs the WebAuthn registration ceremony; OPNGMS stores only the public key + a signature counter (no secret material). Each passkey is named and individually removable, and the last remaining factor can't be removed while the policy requires MFA.

TOTP verification allows ±1 step (30-second period) of clock skew, compares in constant time, and enforces anti-replay: a code's time-step must be strictly greater than the last one used. WebAuthn verifies the challenge + RP ID + origin server-side and rejects an authentication whose signature counter does not strictly increase (cloned-authenticator detection); the per-ceremony challenge is server-generated, single-use, and bound to the session.

Passkeys need a configured domain. Set the relying-party config first (System → WebAuthn: rp_id / origin, or env WEBAUTHN_RP_ID / WEBAUTHN_ORIGIN) — WebAuthn requires HTTPS + a stable domain, so passkey registration is refused until it is set. TOTP is unaffected.

Enforcement policy

An admin sets the org-wide policy (Admin → MFA policy, PUT /api/admin/mfa-policy) to one of:

Mode Effect
off MFA optional (default).
all Every user must enrol and pass MFA.
privileged Only privileged users (superadmins) must enrol and pass MFA.

Two-step login

When MFA applies, login is two requests, tracked by a short-lived partial session distinguished by a kind column on the session row:

  1. POST /api/login — verifies the password. If the user has MFA enrolled it returns status: "mfa_required" and a mfa_pending session (TTL 5 minutes). If policy requires MFA but the user is not yet enrolled it returns mfa_setup_required with a mfa_setup session. Otherwise it returns a normal full session and status: "ok".
  2. POST /api/login/mfa — accepted only for an mfa_pending session; verifies the TOTP code (or a recovery code), then deletes the pending session and mints a fresh full session (anti-fixation rotation) with new cookies.

The session cookie is the only carrier of this intermediate state — there is no separate bearer token — and the request dependencies reject partial sessions from protected endpoints with a 403 carrying mfa_required / mfa_setup_required.

Remember this device (trusted devices)

Added in v0.21.0. Lets a user skip the second factor on a device they choose to trust — the password is always still required.

At the MFA step the user can tick "Trust this device for N days". On success, OPNGMS mints a random 256-bit token (secrets.token_urlsafe(32)), stores only its HMAC-SHA256(SESSION_SECRET) hash in a new trusted_devices row (with the user id, an expiry, and display-only UA/IP), and sets the raw token in a separate opngms_trusted_device cookie (HttpOnly + Secure + SameSite=Lax). As with the session cookie, a DB dump yields no usable tokens and rotating SESSION_SECRET invalidates every trusted device.

On a later POST /api/login, after the password verifies, if the org allows the feature, the user is MFA-enrolled, and a valid trusted-device cookie for that same user is presented, OPNGMS mints a full session directly and returns status: "ok" — the second factor is skipped (audited auth.login.trusted_device).

Fail-closed safety rails:

  • The password is always required — the skip only ever bypasses the second factor.
  • Trust is bound to one user: the lookup matches token_hash and user_id (in SQL and re-checked after fetch), so a cookie minted for user A can never skip MFA for user B.
  • A trusted cookie can never bypass mandatory enrolment — a user the policy forces into mfa_setup is still forced, even with a cookie.
  • Expired, revoked, unknown, or empty cookies are ignored (never grant a skip). Expired rows are purged by the session-cleanup cron.

Management & revocation:

  • Users see and revoke their trusted devices under Two-factor authGET /api/me/trusted-devices, DELETE /api/me/trusted-devices/{id} (revoke one), DELETE /api/me/trusted-devices (revoke all). Mutations require a full session + CSRF.
  • Auto-revoke drops all of a user's trusted devices on disable-MFA (/api/me/mfa/disable), an admin MFA reset (/api/users/{id}/mfa/reset), and log out everywhere (/api/logout-all). A plain /api/logout intentionally keeps the trust so the feature survives a normal sign-out.

Org controls (superadmin):

  • Remember-this-device on/off toggle — GET/PUT /api/admin/trusted-device-enabled (env default TRUSTED_DEVICE_ENABLED=true); turning it off gates the skip at login time and hides the user-facing section. Audited auth.trusted_device.policy_change.
  • Trusted device lifetime — the runtime setting trusted_device_days (default 30, range 1–365) under System → Runtime settings.

Recovery codes

Confirmation issues 10 one-time recovery codes (formatted XXXXX-XXXXX from an unambiguous alphabet). Only argon2 hashes are stored; the clear codes are shown once. A recovery code can substitute for the TOTP code at the /api/login/mfa step, and consumption is atomic (guarded by used_at IS NULL) so a code can be spent only once.

Break-glass reset

If the last superadmin loses their authenticator and recovery codes, reset MFA from the host with the CLI (connects as the owner via ADMIN_DATABASE_URL):

docker compose -f docker-compose.prod.yml exec api \
  python -m app.cli mfa-reset --email admin@example.com

This deletes the user's TOTP secret and recovery codes and writes an audit-log entry (mfa.cli_reset). There is also an in-app admin reset (POST /api/users/{user_id}/mfa/reset, requires user-management permission) for resetting other users.


Audit log

OPNGMS records every state-changing and privileged action in an append-only audit_log table (shipped in v0.10.0). Writes go through a single AuditService, and each row captures:

  • When — the timestamp.
  • Who — the acting user, together with their IP address.
  • Where — the tenant the action targeted.
  • What — the action name, the target's type and id, and a JSONB details payload with action-specific context.

Coverage is broad by design: authentication, device lifecycle, configuration changes, RBAC (groups and memberships), MFA, log forwarding, profiles, templates, reports and schedules, SMTP delivery, system settings, firmware actions, retention changes, and the CLI break-glass MFA reset all leave a trail.

Note: No secrets are stored in details. Credential operations record an empty (or redacted) payload, the same discipline used everywhere else secrets are handled (see Secrets at rest).

Completeness is CI-enforced

A guard test in the backend suite enumerates every mutating route (POST / PUT / PATCH / DELETE) and fails the build if any of them ships without an audit record — or an explicit, declared read-only exemption. A new write endpoint therefore cannot merge silently un-audited; coverage stays complete as the API grows, rather than depending on a developer remembering to log.

The superadmin Audit viewer

A superadmin-only Audit page (/admin/audit) browses the ledger in the console. It supports:

  • Filters — actor email, tenant, action, and a date range.
  • Offset pagination through the matching rows.
  • CSV export of the current result set.

It is backed by GET /api/admin/audit and GET /api/admin/audit/export.csv, gated by an org-level AUDIT_VIEW permission.

Note: Unlike the per-tenant tables, audit_log is a global, non-RLS table — it deliberately spans all tenants so an operator can see the whole fleet's activity in one place. Because there is no RLS policy backing it, the superadmin code-gate (AUDIT_VIEW) is the only access control on the ledger; treat that permission as highly sensitive.


Fail-closed guards

OPNGMS prefers to refuse rather than run insecurely:

  • change-me secret guard. At startup (in the FastAPI lifespan, before any traffic is served) the API calls assert_secure_secrets() and refuses to start if DATABASE_URL, ADMIN_DATABASE_URL, SESSION_SECRET, MASTER_KEY, or APP_ROLE_PASSWORD still contain the shipped change-me placeholder. You cannot accidentally run on default secrets.
  • One-time /api/setup. The first-superadmin bootstrap endpoint returns 409 Conflict if any user already exists, so it cannot be used to inject an extra admin after the system is live. The first user it creates is the superadmin.
  • Reserved-TLD email rejection. Every email input is validated by pydantic EmailStr (the RFC-compliant email-validator library), which rejects addresses on special-use/reserved TLDs such as .local, .internal, and .test. (This is an emergent property of the validator library, not a bespoke deny-list.)

Transport security

Console TLS

The SPA carries Secure session cookies, so the console must be served over HTTPS. Installation documents four mutually-exclusive TLS models — terminate at your own proxy/LB (Model 1), built-in nginx with your certificate (Model 2), or automatic Let's Encrypt via Caddy or Traefik (Models 3a/3b). Refer to that page's TLS-model table rather than re-choosing here.

Outbound to a managed firewall

Every call to a managed box goes through the SSRF-guarded OpnsenseClient (HTTPS only, no redirects, loopback/link-local blocked, connection pinned to the resolved IP to defeat DNS rebinding). Certificate handling is a per-device decision, because OPNsense appliances ship with a self-signed certificate:

  • Verify TLS on (the default) — the device certificate is validated against the system CA store, as for any HTTPS client. Use this once the box presents a certificate from a CA you trust.
  • Verify TLS off + a pinned fingerprint — the connector retrieves the device's leaf certificate and compares its SHA-256 against the pinned value using a constant-time comparison, before any credential is transmitted, on every request. A mismatch fails closed: the request is refused and the device is reported unreachable. This is the recommended setting for a stock self-signed box.
  • Verify TLS off with no fingerprint — permissive mode. Nothing authenticates the certificate, so the device's API credentials are exposed to an on-path attacker. It exists because self-signed deployments must keep working, and the device form warns about it explicitly.

Both fields are set when adding a device (Devices → Add device); the fingerprint field and the warning appear as soon as verification is switched off. The fingerprint is accepted as a hex SHA-256 digest, with or without colons and with an optional sha256: prefix.

Note (honestly stated): changing the TLS trust settings of an already-onboarded device is not yet possible from the console — the API supports it (PATCH /api/tenants/{tenant_id}/devices/{device_id}), but no UI exposes it. Until it does, re-add the device to change its pinning.

Log-lake mTLS

The optional log lake ingests device logs over mutual TLS (client certificate) on port 6514. The syslog-ng receiver is configured peer-verify(required-trusted), so it accepts only certificates signed by the OPNGMS CA. Tenant and device identity are taken straight from the verified client certificate — O = tenant id, CN = device id — which makes attribution unspoofable; logs that do not resolve to a tenant are dropped. OpenSearch itself is internal-only (plain HTTP, not published); the mTLS hop on 6514 is the trust boundary. The CA and per-device client certs are issued and managed by OPNGMS:

  • Per-device client certs are short-lived (90-day default, clientAuth EKU) and auto-renewed by the worker within a configurable window.
  • Operator Rotate (re-issue + swap on the box) and soft Revoke (deprovision + record the serial in an RLS-scoped revocation ledger) are implemented.

Note (honestly deferred): Hard CRL enforcement at the receiver is a tracked follow-up — the current syslog-ng build cannot enforce a CRL, which is precisely why device certs are deliberately short-lived and auto-renewed to bound the window of a stolen key. A soft revoke records the serial in the ledger but the receiver does not yet consult a CRL. Multi-node OpenSearch HA and an inter-node-TLS hardening pass are likewise staging bring-up items.

See Log-Lake for the full bring-up, network requirements, and the certificate lifecycle.


Supply-chain & CI security

Security scanning runs in GitHub Actions, and the default branch is protected.

Scan Tool Scope Runs on
Static analysis (SAST) CodeQL Python, JavaScript/TypeScript, and GitHub Actions workflows — default setup's extended suite, i.e. security-extended push to main, PRs, weekly
Container CVEs Trivy Backend + frontend images, HIGH/CRITICAL (SARIF → Security tab) push to main, PRs, weekly
Committed secrets gitleaks Repo + full git history push to main, PRs, weekly
PR dependency gate dependency-review New deps in a PR (blocks on high) PRs only
Dependency audit pip-audit + npm audit Backend Python + frontend prod deps PRs (CI) + weekly

The dependency audit is driven by scripts/security_audit.sh: pip-audit on the backend and npm audit --omit=dev on the frontend, exiting non-zero on any finding. Dependabot is configured for ongoing dependency updates.

The Trivy jobs are a baseline, not a gate. They run with --exit-code 0, so the Trivy (backend) / Trivy (frontend) checks stay green whatever they find — the findings are published to the Security tab via the SARIF upload instead. A green Trivy check therefore does not mean the image is clean; read the Security tab, and read it after the final push rather than before.

The backend image ships no pip. The Dockerfile uninstalls it once the application is installed. pip is not needed at runtime — the image only ever runs uvicorn, arq or alembic — and its vendored copies of msgpack and setuptools were the only source of HIGH findings against the image, with no pip release shipping patched versions. Run python -m ensurepip inside a container if you need pip back for debugging.

Note: CodeQL runs through GitHub's default setup, so there is deliberately no codeql.yml in .github/workflows/ — an empty workflows directory is not evidence that CodeQL is absent. Its three Analyze (...) jobs are required checks on main. One consequence worth knowing: default setup does not run those jobs on Dependabot-authored pull requests, which leaves such PRs reported as blocked on "expected" checks even when the rest of CI is green.

Protected main and pinned catalogs

The main branch is protected by a strict ruleset: no direct pushes, and every change merges via PR with green required checks. Config-template catalogs are distributed with SHA-256 integrity verification — catalogs ship as GitHub Release assets alongside a manifest.json mapping each edition/version to its SHA-256, and the backend re-hashes the downloaded bytes and rejects anything that does not match (a manifest that drops the key is also rejected, fail-closed). The verified hash is persisted in the catalog cache. See Configuration and Configuration-Editor for how catalogs feed the version-aware config editor.


Operator hardening checklist

  • Generate fresh secrets. Create unique SESSION_SECRET and MASTER_KEY values; never reuse the .env.example placeholders. The change-me guard will refuse to start otherwise.
  • Serve over HTTPS. Pick a TLS model from Installation; never expose the plain-HTTP frontend port to the internet.
  • Forward X-Forwarded-Proto: https from your reverse proxy/LB so the backend issues Secure cookies.
  • Bind the base frontend to localhost. Keep FRONTEND_BIND=127.0.0.1 unless a TLS terminator sits directly in front of that port.
  • Use distinct, strong DB passwords and confirm both password pairs match (DATABASE_URLAPP_ROLE_PASSWORD, ADMIN_DATABASE_URLPOSTGRES_PASSWORD).
  • Keep the API on opngms_app. Never repoint DATABASE_URL at the owner role — doing so silently disables RLS for request traffic.
  • Enforce MFA. Set the policy to all (or at least privileged) and have admins enrol TOTP; store recovery codes securely.
  • Protect MASTER_KEY. Back it up out-of-band — losing it makes encrypted device and SMTP credentials unrecoverable. Rotate via MASTER_KEY_OLD_KEYS (see Upgrading).
  • Restrict outbound reach. The host needs HTTPS to managed OPNsense boxes and to your SMTP relay; limit egress otherwise.
  • Lock down port 6514 (if you run the log lake) to managed devices only; rely on mTLS peer-verify and the short-lived auto-renewed device certs.
  • Keep dependencies current. Watch the weekly Trivy / gitleaks / audit runs and Dependabot PRs; do not merge with red required checks.
  • Rotate SESSION_SECRET to force-log-out all sessions after a suspected compromise.

For deployment see Installation; for environment variables Configuration; for day-two operations and key rotation Upgrading; for the log lake Log-Lake; for the broader system design Architecture; and for diagnostics Troubleshooting.

Clone this wiki locally