- API keys are stored as peppered HMAC-SHA-256 hashes. Keys are random 192-bit keys with the
mantis_live_prefix (matches GitHub’s secret-scanning format, so a leaked key in a public repo will trigger their alerting).MANTIS_API_KEY_PEPPERis required for new mints; legacy pre-pepper SHA-256 rows are still accepted and are upgraded opportunistically on next use. Dashboard cookies carry opaquemantis_sess_...tokens; the database stores only a SHA-256 hash of each session token. - Disabled or expired keys still return the configured response — they just don’t record a hit or fire notifications. This prevents an attacker from probing for valid key IDs by looking for differential responses.
- Notifications use a Postgres-backed queue. Public trigger requests enqueue notification rows after the response path; a worker claims pending rows with
FOR UPDATE SKIP LOCKED, sends them, and retries failures with backoff. The worker starts automatically on non-Vercel deployments. On Vercel or other serverless hosts, use/api/cron/notificationswithCRON_SECRET. - Operator secrets can be encrypted at rest. Set
MANTIS_SECRET_KEY(AES-256-GCM,openssl rand -base64 32) to envelope-encrypt webhook HMAC signing secrets and the Apple Wallet auth secret + cert passphrase in the database, so a DB-only leak yields no usable secrets. Unset = today’s plaintext-at-rest behavior; existing rows are read transparently and migrate to ciphertext the next time a signing secret is rotated (a legacy DB wallet config can’t be re-saved — clear it from/settings/walletinstead). See Configuration. - API-key auth failures are rate-limited. Repeated failed bearer auth is throttled per client IP via a Postgres-backed fixed-window limiter (60/min) and returns
429instead of401to blunt brute force; a successful auth never touches the limiter. The dashboard login action applies the same per-IP throttle to failed logins only (10/min), and only when a trusted client IP is available — so a flood of bad attempts can never lock out a valid key or the operator. The sharedrate_limitstable makes the cap hold across instances and serverless cold starts; the limiter fails open on a DB error. - Alert payloads escape attacker-controlled values. The hit IP, User-Agent, key memo, and the
host_contextparsed fromX-Mantis-*headers are escaped before they’re interpolated into Slack / Discord / Teams messages, so a canary tripper can’t inject phishing links, channel mentions, or spoofed formatting into your alerts. Long values are truncated, so an oversized header cannot make a chat platform reject the alert. Chat and email alerts link the key’s dashboard page and never the trigger URL, because following that link would fire the key again. The edge worker carries the same escapers and names the canary by a short URL fragment instead of a link. - Outbound webhooks are SSRF-guarded. Webhook / Slack / Discord / Teams deliveries enforce
http(s)-only and refuse targets that resolve to RFC1918, loopback, link-local, or cloud-metadata (169.254.169.254) addresses, don’t follow redirects, and re-validate the resolved address at connect time to close the DNS-rebinding window. A destination that points back at the Mantis instance itself is refused too. Every refusal reports the same message; the reason is in the server log. SetALLOW_PRIVATE_WEBHOOKS=1only when your webhook target is intentionally on the same LAN.
Operating
Operational notes
Implementation notes for key hashing, disabled-key behavior, and worker mode.