Skip to content

JWT Configuration

Configure JSON Web Tokens (JWT) for API authentication in Mantis.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyLWlkIiwidGVuYW50IjoiYWJjMTIzIiwiZXhwIjoxNzA0MDY3MjAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
PartDescription
HeaderAlgorithm and token type
PayloadClaims (user, tenant, expiry)
SignatureCryptographic signature
AlgorithmTypeKeyUse Case
HS256SymmetricShared secretSimple deployments
RS256AsymmetricRSA key pairEnterprise, key separation

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 │
│ │
└─────────────────────────────────────────────────────────────┘

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 │
│ │
└─────────────────────────────────────────────────────────────┘
[jwt]
algorithm = "HS256"
secret = "your-256-bit-secret-key-here-at-least-32-chars"
access_token_expiry_secs = 3600 # 1 hour
refresh_token_expiry_secs = 2592000 # 30 days
issuer = "mandible"

Generate a secure secret:

Terminal window
# Generate 256-bit secret
openssl rand -base64 32
# Or using /dev/urandom
head -c 32 /dev/urandom | base64
[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 hour
refresh_token_expiry_secs = 2592000 # 30 days
issuer = "mandible"

Generate RSA key pair:

Terminal window
# Generate private key
openssl genrsa -out /etc/mantis/jwt/private.pem 2048
# Extract public key
openssl rsa -in /etc/mantis/jwt/private.pem \
-pubout -out /etc/mantis/jwt/public.pem
# Secure private key
chmod 600 /etc/mantis/jwt/private.pem
VariableDescriptionExample
MANDIBLE__JWT__ALGORITHMAlgorithmHS256 or RS256
MANDIBLE__JWT__SECRETHS256 secretyour-secret-key
MANDIBLE__JWT__PRIVATE_KEY_PATHRS256 private key/etc/mantis/jwt/private.pem
MANDIBLE__JWT__PUBLIC_KEY_PATHRS256 public key/etc/mantis/jwt/public.pem
MANDIBLE__JWT__ACCESS_TOKEN_EXPIRY_SECSToken lifetime (seconds)3600
MANDIBLE__JWT__REFRESH_TOKEN_EXPIRY_SECSRefresh lifetime (secs)604800
MANDIBLE__JWT__ISSUERToken issuermandible
MANDIBLE__JWT__AUDIENCEToken audiencelens
MANDIBLE__JWT__KEY_IDCurrent signing key IDk1

Short-lived token for API access:

SettingDefaultRecommended Range
access_token_expiry_secs3600900-86400 seconds

Longer-lived token to obtain new access tokens:

SettingDefaultRecommended Range
refresh_token_expiry_secs2592000 (30 days)86400-2592000 seconds
ClaimDescriptionExample
subSubject (user ID)usr_abc123
issIssuermandible
expExpiration time1704067200
iatIssued at1703980800
jtiToken IDtok_xyz789
ClaimDescriptionExample
tenant_idTenant identifierten_abc123
rolesUser roles (array)["operator"]
pending_2faAwaiting 2FAtrue
session_idSession identifiersess_abc123

Example payload:

{
"sub": "usr_abc123",
"iss": "mandible",
"aud": "lens",
"exp": 1704067200,
"iat": 1703980800,
"nbf": 1703980800,
"jti": "tok_xyz789",
"email": "user@example.com",
"username": "jsmith",
"tenant_id": "ten_abc123",
"roles": ["operator"],
"session_id": "sess_abc123"
}
AspectJWTAPI Key
LifetimeShort (hours)Long (months/years)
RevocationOn expiryManual
Use caseUser sessionsService accounts
Contains claimsYesNo (server lookup)

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.

API keys are created through the REST API (or the Lens UI), not a CLI subcommand:

Terminal window
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.

API keys use the Authorization: Bearer header with mnt_ prefix:

Terminal window
# API key authentication
curl -H "Authorization: Bearer mnt_7K4vBz9mQxNwR5pLy2HjC8dTfG6sAeWuJn3iX1kM4o" \
https://mantis.example.com/api/v1/targets

HS256 secrets:

Terminal window
# 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:

Terminal window
# Secure key file permissions
chmod 600 /etc/mantis/jwt/private.pem
chmod 644 /etc/mantis/jwt/public.pem
chown mantis:mantis /etc/mantis/jwt/*.pem
PracticeImplementation
Short expiry1-24 hours for access tokens
Secure transmissionHTTPS only
HttpOnly cookiesPrevent XSS access
SameSite cookiesPrevent CSRF
[cookie]
secure = true # HTTPS only
same_site = "Lax" # CSRF protection
domain = ".example.com" # Cookie domain
path = "/" # Cookie path

Mandible performs these checks:

  1. Signature verification - Token not tampered
  2. Expiration check - Token not expired
  3. Issuer check - Correct issuer claim
  4. Token ID check - Not revoked (if tracking enabled)

For immediate token invalidation, revoke the underlying sessions through the REST API. Logging out (POST /api/v1/auth/logout) blocklists the current access token in Redis and revokes all of the user’s OAuth2 refresh tokens, so no new access tokens can be minted from the refresh cookie. It does not directly revoke UserSession records — use DELETE /api/v1/auth/sessions to revoke those:

Terminal window
# Revoke a specific session by ID
curl -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 revoked and returns 401. This is a deliberate security 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:

MetricMeaning
mantis_jwt_blocklist_redis_available1 if the JWT blocklist Redis is reachable, 0 if not (set on the request path).
mantis_jwt_blocklist_redis_errors_totalCumulative blocklist Redis errors, labelled by operation.
mantis_sse_redis_upBackground-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.

Support multiple keys during rotation:

[jwt]
algorithm = "HS256"
# Current key (used for signing)
secret = "new-secret-key"
# Previous keys (used for verification only)
previous_secret = "old-secret-key"
key_id = "k2"
previous_key_id = "k1"

Rotation process:

  1. Add new secret as primary, move old secret to previous_secret
  2. Assign a new key_id and set previous_key_id to the old key ID
  3. Wait for old tokens to expire
  4. Remove previous_secret and previous_key_id
[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.

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.

ErrorCauseSolution
invalid signatureWrong secret/keyVerify key configuration
token expiredPast expirationRequest new token
invalid issuerWrong issuer claimCheck issuer configuration
invalid audienceWrong audience claimCheck audience configuration

Decode JWT (without verification):

Terminal window
# Decode header
echo "$JWT" | cut -d. -f1 | base64 -d 2>/dev/null | jq
# Decode payload
echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jq

Online tools (development only):

  • jwt.io - Decode and verify JWTs
  • jwt.ms - Microsoft JWT decoder

Token immediately expires:

Terminal window
# Check server time synchronization
timedatectl status
# Sync time if needed
timedatectl set-ntp true

Signature verification fails:

Terminal window
# Verify secret is correct
echo -n "test" | openssl dgst -sha256 -hmac "$SECRET"
# For RS256, verify key pair matches
openssl rsa -in private.pem -pubout | diff - public.pem

Token not accepted after key rotation:

Terminal window
# Ensure previous keys are configured
grep previous_secret /etc/mantis/mandible.toml
# Check token was signed with tracked key
[jwt]
algorithm = "RS256" # Prefer asymmetric
private_key_path = "/etc/mantis/jwt/private.pem"
public_key_path = "/etc/mantis/jwt/public.pem"
access_token_expiry_secs = 28800 # 8 hours
refresh_token_expiry_secs = 604800 # 7 days
issuer = "mandible"
[cookie]
secure = true
same_site = "Lax"

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 hour
refresh_token_expiry_secs = 86400 # Daily re-auth
issuer = "mandible"
audience = "lens"