JWT Configuration
JWT Configuration
Section titled “JWT Configuration”Configure JSON Web Tokens (JWT) for API authentication in Mantis.
Overview
Section titled “Overview”JWT Authentication Flow
Section titled “JWT Authentication Flow”JWT Structure
Section titled “JWT Structure”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLWlkIiwidGVuYW50IjoiYWJjMTIzIiwiZXhwIjoxNzA0MDY3MjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c| Part | Description |
|---|---|
| Header | Algorithm and token type |
| Payload | Claims (user, tenant, expiry) |
| Signature | Cryptographic signature |
Algorithm Selection
Section titled “Algorithm Selection”Supported Algorithms
Section titled “Supported Algorithms”| Algorithm | Type | Key | Use Case |
|---|---|---|---|
| HS256 | Symmetric | Shared secret | Simple deployments |
| RS256 | Asymmetric | RSA key pair | Enterprise, key separation |
HS256 (HMAC-SHA256)
Section titled “HS256 (HMAC-SHA256)”Uses a shared secret key:
┌─────────────────────────────────────────────────────────────┐│ HS256 Key Flow │├─────────────────────────────────────────────────────────────┤│ ││ Same secret key used for: ││ • Signing tokens (Mandible) ││ • Verifying tokens (Mandible) ││ ││ Pros: Simple, fast ││ Cons: Secret must be shared with all verifiers ││ │└─────────────────────────────────────────────────────────────┘RS256 (RSA-SHA256)
Section titled “RS256 (RSA-SHA256)”Uses public/private key pair:
┌─────────────────────────────────────────────────────────────┐│ RS256 Key Flow │├─────────────────────────────────────────────────────────────┤│ ││ Private key: Signs tokens (keep secret) ││ Public key: Verifies tokens (can be shared) ││ ││ Pros: Key separation, public verification ││ Cons: Slower, larger tokens ││ │└─────────────────────────────────────────────────────────────┘Configuration
Section titled “Configuration”HS256 Configuration
Section titled “HS256 Configuration”[jwt]algorithm = "HS256"secret = "your-256-bit-secret-key-here-at-least-32-chars"access_token_expiry_secs = 3600 # 1 hourrefresh_token_expiry_secs = 2592000 # 30 daysissuer = "https://mantis.example.com"Generate a secure secret:
# Generate 256-bit secretopenssl rand -base64 32
# Or using /dev/urandomhead -c 32 /dev/urandom | base64RS256 Configuration
Section titled “RS256 Configuration”[jwt]algorithm = "RS256"private_key_path = "/etc/mantis/jwt/private.pem"public_key_path = "/etc/mantis/jwt/public.pem"access_token_expiry_secs = 3600 # 1 hourrefresh_token_expiry_secs = 2592000 # 30 daysissuer = "https://mantis.example.com"Generate RSA key pair:
# Generate private keyopenssl genrsa -out /etc/mantis/jwt/private.pem 2048
# Extract public keyopenssl rsa -in /etc/mantis/jwt/private.pem \ -pubout -out /etc/mantis/jwt/public.pem
# Secure private keychmod 600 /etc/mantis/jwt/private.pemEnvironment Variables
Section titled “Environment Variables”| Variable | Description | Example |
|---|---|---|
MANDIBLE__JWT__ALGORITHM | Algorithm | HS256 or RS256 |
MANDIBLE__JWT__SECRET | HS256 signing secret, at least 32 characters | $(openssl rand -hex 32) |
MANDIBLE__JWT__PRIVATE_KEY_PATH | RS256 private key | /etc/mantis/jwt/private.pem |
MANDIBLE__JWT__PUBLIC_KEY_PATH | RS256 public key | /etc/mantis/jwt/public.pem |
MANDIBLE__JWT__ACCESS_TOKEN_EXPIRY_SECS | Token lifetime (seconds) | 3600 |
MANDIBLE__JWT__REFRESH_TOKEN_EXPIRY_SECS | Refresh lifetime (secs) | 604800 |
MANDIBLE__JWT__ISSUER | Token issuer | https://api.example.com |
MANDIBLE__JWT__AUDIENCE | Token audience | lens |
MANDIBLE__JWT__KEY_ID | Current signing key ID | k1 |
The credential lookup key is not a JWT setting
Section titled “The credential lookup key is not a JWT setting”MANDIBLE__SECURITY__TOKEN_LOOKUP_KEY is required under both RS256 and HS256. It is
listed here only because it used to be MANDIBLE__JWT__SECRET, which is why it is easy to
assume RS256 does not need it.
An API key and an OAuth2 token are stored as an Argon2 hash, which cannot be searched. To find the row for a presented token, Mantis also stores an HMAC of that token under this key and looks it up by that value. Keeping the key secret is what stops anyone who obtains a copy of the database from computing those values for themselves.
MANDIBLE__SECURITY__TOKEN_LOOKUP_KEY="$(openssl rand -base64 32)"Token Lifetimes
Section titled “Token Lifetimes”Access Token
Section titled “Access Token”Short-lived token for API access:
| Setting | Default | Recommended Range |
|---|---|---|
access_token_expiry_secs | 3600 | 900-86400 seconds |
Refresh Token
Section titled “Refresh Token”Longer-lived token to obtain new access tokens:
| Setting | Default | Recommended Range |
|---|---|---|
refresh_token_expiry_secs | 2592000 (30 days) | 86400-2592000 seconds |
Token Refresh Flow
Section titled “Token Refresh Flow”JWT Claims
Section titled “JWT Claims”Standard Claims
Section titled “Standard Claims”| Claim | Description | Example |
|---|---|---|
sub | Subject (user ID, a UUID) | 019b937d-4862-8e07-ac3a-1b8589d1b807 |
iss | Issuer | https://mantis.example.com |
aud | Audience | lens |
exp | Expiration time | 1704067200 |
iat | Issued at | 1703980800 |
nbf | Not before | 1703980800 |
jti | Token ID | 019b9450-0b91-8c10-86c3-5c2c3f6b9f04 |
Custom Claims
Section titled “Custom Claims”| Claim | Description | Example |
|---|---|---|
email | User email | user@example.com |
username | Username | jsmith |
tenant_id | Tenant the request acts in (UUID, or null) | 019b937d-4862-8387-9a86-df16bd76ca99 |
tenant_memberships | Every tenant the account belongs to; omitted for a platform-scoped account | ["019b937d-4862-8387-9a86-df16bd76ca99"] |
roles | User roles (array) | ["operator"] |
api_key_scopes | Scopes when authenticated via API key; gates the API-key scope check | ["deployments:read"] |
pending | The step a sign-in still owes: second_factor or password_change | "second_factor" |
session_id | Session identifier (UUID) | 019b9450-1234-8000-9abc-def012345678 |
credential_epoch | How many account-wide revocations the account had been through at sign-in | 0 |
api_key_scopes, pending, session_id and credential_epoch are omitted when they
do not apply.
sub, tenant_id and session_id must parse as UUIDs — decoding rejects any
other format.
Example payload:
{ "sub": "019b937d-4862-8e07-ac3a-1b8589d1b807", "iss": "https://mantis.example.com", "aud": "lens", "exp": 1704067200, "iat": 1703980800, "nbf": 1703980800, "jti": "019b9450-0b91-8c10-86c3-5c2c3f6b9f04", "email": "user@example.com", "username": "jsmith", "tenant_id": "019b937d-4862-8387-9a86-df16bd76ca99", "tenant_memberships": ["019b937d-4862-8387-9a86-df16bd76ca99"], "roles": ["operator"], "session_id": "019b9450-1234-8000-9abc-def012345678", "credential_epoch": 0}API Key Authentication
Section titled “API Key Authentication”API Keys vs JWT
Section titled “API Keys vs JWT”| Aspect | JWT | API Key |
|---|---|---|
| Lifetime | Short (hours) | Long (months/years) |
| Revocation | On expiry | Manual |
| Use case | User sessions | Service accounts |
| Contains claims | Yes | No (server lookup) |
API Key Storage
Section titled “API Key Storage”API keys are always Argon2-hashed before storage and identified by the mnt_ prefix.
There is no [api_keys] configuration section — hashing is built in and not configurable.
Creating API Keys
Section titled “Creating API Keys”API keys are created through the REST API (or the Lens UI), not a CLI subcommand:
curl -X POST "https://api.mantis.example.com/api/v1/auth/api-keys" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "ci-pipeline", "description": "CI/CD deployment key"}'
# Response includes the full key exactly once in the "full_key" field:# {"full_key": "mnt_7K4vBz9mQxNwR5pLy2HjC8dTfG6sAeWuJn3iX1kM4o", "api_key": { ... }, "warning": "..."}# Store it securely - it cannot be retrieved later.Using API Keys
Section titled “Using API Keys”API keys use the Authorization: Bearer header with mnt_ prefix:
# API key authenticationcurl -H "Authorization: Bearer mnt_7K4vBz9mQxNwR5pLy2HjC8dTfG6sAeWuJn3iX1kM4o" \ https://mantis.example.com/api/v1/targetsSecurity Best Practices
Section titled “Security Best Practices”Secret Management
Section titled “Secret Management”HS256 secrets:
# Generate strong secret (at least 256 bits)openssl rand -base64 32
# Store in environment variable (not in config file)export MANDIBLE__JWT__SECRET="$(cat /etc/mantis/secrets/jwt-secret)"RS256 keys:
# Secure key file permissionschmod 600 /etc/mantis/jwt/private.pemchmod 644 /etc/mantis/jwt/public.pemchown mantis:mantis /etc/mantis/jwt/*.pemToken Security
Section titled “Token Security”| Practice | Implementation |
|---|---|
| Short expiry | 1-24 hours for access tokens |
| Secure transmission | HTTPS only |
| HttpOnly cookies | Prevent XSS access |
| SameSite cookies | Prevent CSRF |
Cookie Configuration
Section titled “Cookie Configuration”[cookie]secure = true # HTTPS onlysame_site = "Lax" # CSRF protectiondomain = ".example.com" # Cookie domainpath = "/" # Cookie pathToken Validation
Section titled “Token Validation”Validation Steps
Section titled “Validation Steps”Mandible performs these checks:
- Signature verification - Token not tampered
- Expiration check - Token not expired
- Issuer check - Correct issuer claim
- Token ID check - Not revoked (if tracking enabled)
Token Revocation
Section titled “Token Revocation”For immediate token invalidation, revoke the underlying sessions through the REST API.
Logging out (POST /api/v1/auth/logout) revokes the session it is called from and
no other: a signed-out device stops appearing in the session list, and the user’s other
devices stay signed in. Use DELETE /api/v1/auth/sessions to revoke the user’s other
sessions.
Revoking a session takes effect at once, in both directions: the refresh token issued
for that session is bound to it and is removed with it, so the browser cannot mint a
replacement, and the session is added to a Redis blocklist that denies the access token
it already holds on that token’s next request. The blocklist entry is kept for
access_token_expiry_secs, after which no token from that session can still be valid.
Password changes, password resets (self-service and admin-initiated) and email changes
end the user’s sessions the same way. Every interactive sign-in has a session, including
SSO — so an SSO login is listed and revocable like any other.
They also end whatever was on its way to becoming a session. A sign-in that had passed the password step and was waiting on its second factor, and an OAuth authorization code issued but not yet redeemed, are both refused afterwards: the user signs in again.
Open event streams (/api/v1/sse/…) are covered too. A stream authorizes once, when it
is opened, and then runs for as long as the client holds it — longer than the token it
presented — so it rechecks while it serves and closes once its session is revoked,
instead of feeding a revoked session until it disconnects.
# Revoke a specific session by IDcurl -X DELETE "https://api.mantis.example.com/api/v1/auth/sessions/$SESSION_ID" \ -H "Authorization: Bearer $TOKEN"
# Revoke all of the current user's OTHER sessions (the current session is kept active)curl -X DELETE "https://api.mantis.example.com/api/v1/auth/sessions" \ -H "Authorization: Bearer $TOKEN"Redis availability dependency (fail-closed)
Section titled “Redis availability dependency (fail-closed)”The JWT revocation (blocklist) check is backed by Redis, and it fail-closes: if the blocklist Redis backend is unreachable, Mandible treats the token as denied with 503 Service Unavailable (not 401 — an auth-backend outage reported as a token problem would trigger a refresh-token storm). This is a deliberate choice — a Redis outage must not let a revoked token through — but it has an important availability consequence:
Mandible exports these Prometheus metrics for the dependency:
| Metric | Meaning |
|---|---|
mantis_jwt_blocklist_redis_available | 1 if the JWT blocklist Redis is reachable, 0 if not (set on the request path). |
mantis_jwt_blocklist_redis_errors_total | Cumulative blocklist Redis errors, labelled by operation. |
mantis_sse_redis_up | Background-probed (~10s) SSE/session Redis reachability, independent of traffic. |
The bundled alert rules (deploy/ansible/roles/monitoring/files/mandible-alerts.yaml)
page on mantis_jwt_blocklist_redis_available == 0 (critical) and warn on SSE
Redis being down. When this fires, follow the
Redis availability runbook:
restore Redis, or make a conscious decision (e.g. reduce JWT TTL) while it is
recovered.
Key Rotation
Section titled “Key Rotation”HS256 Key Rotation
Section titled “HS256 Key Rotation”Support multiple keys during rotation:
[jwt]algorithm = "HS256"# Current key (used for signing). At least 32 characters.secret = "9f4c1b7e2a8d6035f1c9b4e7a2d8065c3f1b9e4a7d206c8f"# Previous key (verification only). It verifies for the whole overlap window, so# it is held to the same minimum length.previous_secret = "2d8065c3f1b9e4a7d206c8f9f4c1b7e2a8d6035f1c9b4e7a"key_id = "k2"previous_key_id = "k1"Rotation process:
- Add new secret as primary, move old secret to
previous_secret - Assign a new
key_idand setprevious_key_idto the old key ID - Wait for old tokens to expire
- Remove
previous_secretandprevious_key_id
RS256 Key Rotation
Section titled “RS256 Key Rotation”[jwt]algorithm = "RS256"private_key_path = "/etc/mantis/jwt/private-new.pem"public_key_path = "/etc/mantis/jwt/public-new.pem"key_id = "k2"previous_public_key_path = "/etc/mantis/jwt/public-old.pem"previous_key_id = "k1"See Key Rotation for detailed procedures.
OIDC Integration
Section titled “OIDC Integration”The [jwt] table configures only the signing keys for Mantis’s own tokens. Accepting
tokens from external identity providers (OIDC/JWKS) is not configured under [jwt] —
there are no [jwt.external], [jwt.validation], jwks_uri, or jwks_refresh_interval
keys. External identity providers are configured at runtime through the admin
identity-providers API and stored in the database, where each provider carries its own
issuer/audience/JWKS settings.
See Identity Providers and OIDC Configuration for the full setup.
Troubleshooting
Section titled “Troubleshooting”Invalid Token Errors
Section titled “Invalid Token Errors”| Error | Cause | Solution |
|---|---|---|
invalid signature | Wrong secret/key | Verify key configuration |
token expired | Past expiration | Request new token |
invalid issuer | Wrong issuer claim | Check issuer configuration |
invalid audience | Wrong audience claim | Check audience configuration |
Debugging Tokens
Section titled “Debugging Tokens”Decode JWT (without verification):
# Decode headerecho "$JWT" | cut -d. -f1 | base64 -d 2>/dev/null | jq
# Decode payloadecho "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jqOnline tools (development only):
Common Issues
Section titled “Common Issues”Token immediately expires:
# Check server time synchronizationtimedatectl status
# Sync time if neededtimedatectl set-ntp trueSignature verification fails:
# Verify secret is correctecho -n "test" | openssl dgst -sha256 -hmac "$SECRET"
# For RS256, verify key pair matchesopenssl rsa -in private.pem -pubout | diff - public.pemToken not accepted after key rotation:
# Ensure previous keys are configuredgrep previous_secret /etc/mantis/mandible.toml
# Check token was signed with tracked keyProduction Configuration
Section titled “Production Configuration”Recommended Settings
Section titled “Recommended Settings”[jwt]algorithm = "RS256" # Prefer asymmetricprivate_key_path = "/etc/mantis/jwt/private.pem"public_key_path = "/etc/mantis/jwt/public.pem"access_token_expiry_secs = 28800 # 8 hoursrefresh_token_expiry_secs = 604800 # 7 daysissuer = "https://mantis.example.com"
[cookie]secure = truesame_site = "Lax"High Security Settings
Section titled “High Security Settings”Tighten security with short token lifetimes, RS256, and an explicit audience. These are the
only JWT knobs the server exposes — there is no require_token_binding field or
[jwt.validation] section:
[jwt]algorithm = "RS256"private_key_path = "/etc/mantis/jwt/private.pem"public_key_path = "/etc/mantis/jwt/public.pem"access_token_expiry_secs = 3600 # 1 hourrefresh_token_expiry_secs = 86400 # Daily re-authissuer = "https://mantis.example.com"audience = "lens"Next Steps
Section titled “Next Steps”- OIDC Configuration - External identity providers
- Key Rotation - Rotating JWT keys
- Security Overview - Overall security architecture
