Skip to content

Production Checklist

Use this checklist to verify your Mantis deployment is ready for production.

  • Changed default admin password
  • JWT secret is cryptographically random (32+ characters)
  • JWT private key is secured (file permissions 400)
  • Session tokens have appropriate expiry
  • Valid TLS certificates installed (not self-signed)
  • mTLS enabled for client authentication
  • Registration tokens created with auto_approve disabled (new clients reviewed before approval)
  • Certificate expiration monitoring configured
  • CA private key stored securely (HSM or secrets manager)
  • MANTIS_ENCRYPTION_KEY is a secure 32-byte base64 value
  • Encryption key stored in secrets manager (not in .env)
  • Key rotation procedure documented
  • RBAC roles configured appropriately
  • Principle of least privilege applied
  • Admin accounts limited and audited
  • SSO/OIDC configured (if applicable)
  • Log level set to info for all services (not debug/trace): RUST_LOG=info for Mandible, MANTIS_LOG_LEVEL=info for Thorax, TARSUS__LOG_LEVEL=info for Tarsus
  • Lens production build served (no dev server, source maps disabled)
  • CORS origins restricted to actual domains
  • All default passwords changed
  • External/managed PostgreSQL 16
  • Migration role can CREATE ROLE and owns the audit tables — migrations create the mantis_audit_* roles and apply FORCE ROW LEVEL SECURITY, and fail outright on a managed instance that grants neither
  • Connection pooling configured appropriately — database.max_connections (default 10) sizes each Mandible replica’s connection pool; a replica also holds two dedicated connections outside it, one per change listener (entity_changes, notification_delivery_queued)
  • SSL/TLS enabled for database connections
  • Database credentials in secrets manager

RabbitMQ is off by default — Mantis falls back to database polling. Skip this section unless you set queue.enabled = true.

  • External/managed RabbitMQ (not containerized)
  • TLS enabled for all connections (port 5671)
  • Queue credentials secured
  • External/managed Redis (not containerized)
  • AUTH password configured
  • TLS enabled (if supported)
  • Only Lens, Mandible, and Thorax exposed externally
  • Database/RabbitMQ/Redis on internal network only
  • Firewall rules configured
  • Metrics ports closed to the internet. Both default to loopback (Mandible 127.0.0.1:9091, Thorax 127.0.0.1:9090); compose widens both to 0.0.0.0 inside the containers, behind MANTIS_METRICS_TOKEN
  • Rate limiting enabled
  • TLS termination configured
  • X-Forwarded headers passed correctly
  • DDoS protection enabled
  • Request size limits configured
  • Multiple Thorax instances for HA (if needed)
  • Load balancer configured
  • Health checks configured
  • Health endpoints monitored
  • Prometheus metrics collected from both Mandible and Thorax. Mandible serves GET /metrics on MANDIBLE__METRICS__BIND_ADDR (default 127.0.0.1:9091; set it to the empty string to disable the listener entirely) and Thorax on THORAX__METRICS__BIND_ADDR (default 127.0.0.1:9090). Tarsus exports none
  • A random metrics.auth_token (openssl rand -hex 32) on both listeners, which a bind beyond loopback requires; Prometheus presents it from a credentials_file. Thorax’s listener also serves /health, which discloses cluster topology
  • Alerting configured for critical issues
  • Certificate expiration alerts
  • Centralized logging configured
  • Deployment-log retention configured at PUT /api/v1/admin/log-retention — off by default, so logs accumulate indefinitely until it is set
  • If archiving deployment-log output, a destination is configured and POST /api/v1/admin/log-retention/verify-archives returns every archive intact
  • Archive lifetime decided: the destination’s own lifecycle policy (the default), or archive_retention_days, after which Mantis deletes each archive under the configured destination, and its record
  • Operational history windows set on the same endpoint, if the database should not grow without bound: operational_history_days (settled notifications, mail, digest entries, and commands with their results, aged from their last result) and spent_record_days (revoked or expired registration tokens that issued no certificate, deregistered or long-offline Thorax instances nothing refers to, expired notification suppression). Both are off by default. Environment state history is on neither window and is kept for the life of the install: rollback and promotion resolve against it, and it adds at most one row per successful deployment to an environment. Deleting its environment clears the reference and keeps the row
  • Audit logs enabled and monitored
  • No sensitive data in logs
  • Database backup schedule configured
  • Backup restoration tested
  • CA certificate/key backed up securely
  • Disaster recovery plan documented
  • Audit logging enabled
  • Audit retention is never automatic — there is no schedule or purge job. Entries are immutable and the triggers block row deletion; ageing data out means archiving a completed quarterly partition and dropping it, which an administrator invokes deliberately. The chain is verified and the archive read back first, and the operation refuses if either fails
  • Audit chain verification run periodically, and its recorded history reviewed at GET /api/v1/audit/integrity/{tenant_id}/history
  • Runbooks for common operations
  • Incident response procedures
  • Contact information documented

./docker.sh health is a local docker-compose helper: it runs pg_isready and rabbitmq-diagnostics inside compose containers, so it cannot reach the external PostgreSQL and RabbitMQ this checklist mandates, and it checks neither Thorax, Tarsus nor Redis. Verify the services individually instead:

Terminal window
# Mandible: database connectivity only -- this endpoint performs no
# security checks, despite the name suggesting otherwise
curl -s https://your-domain/api/v1/health | jq
# Readiness and the gRPC dependency
curl -s https://your-domain/api/v1/health/ready | jq
curl -s https://your-domain/api/v1/health/grpc | jq
# Mandible metrics (separate listener from the REST API port)
curl -s -H "Authorization: Bearer $METRICS_TOKEN" http://mandible-host:9091/metrics | head
# Thorax
curl -s -H "Authorization: Bearer $METRICS_TOKEN" http://thorax-host:9090/metrics | head