Key Rotation
Key Rotation
Section titled “Key Rotation”Procedures for rotating cryptographic keys and certificates in Mantis.
Overview
Section titled “Overview”What Needs Rotation
Section titled “What Needs Rotation”| Key Type | Purpose | Recommended Interval |
|---|---|---|
| JWT signing key | API authentication | 90 days |
| Encryption master key | Data at rest | 365 days |
| TLS certificates | Transport security | 365 days |
| Agent certificates | mTLS authentication | 365 days |
| CA certificate | Certificate authority | 5-10 years |
Rotation Triggers
Section titled “Rotation Triggers”| Trigger | Action | Urgency |
|---|---|---|
| Scheduled rotation | Plan and execute | Normal |
| Key compromise | Immediate rotation | Critical |
| Personnel change | Review and rotate | High |
| Certificate expiry | Renew before expiry | High |
| Compliance audit | Rotate as required | Normal |
JWT Key Rotation
Section titled “JWT Key Rotation”HS256 Key Rotation
Section titled “HS256 Key Rotation”For HMAC-based JWT:
Step-by-step:
- Generate new key:
NEW_JWT_SECRET=$(openssl rand -base64 32)- Set the new key, keeping the old one for verification. Supply these as
environment variables, not TOML —
MANDIBLE__JWT__*overrides the file, so editing[jwt]while the env var is set leaves the old secret in force and the rotation silently does nothing:
MANDIBLE__JWT__SECRET="$NEW_JWT_SECRET"MANDIBLE__JWT__KEY_ID="k2" # new id for the new keyMANDIBLE__JWT__PREVIOUS_SECRET="<old secret>"MANDIBLE__JWT__PREVIOUS_KEY_ID="k1" # required, or the old key is dropped- Restart Mandible:
systemctl restart mandible-
Wait for old tokens to expire. Access tokens last 1 hour by default; wait at least that long so no live token is still signed with
k1. -
Remove the old key:
MANDIBLE__JWT__SECRET="$NEW_JWT_SECRET"MANDIBLE__JWT__KEY_ID="k2"# PREVIOUS_SECRET and PREVIOUS_KEY_ID removed- Restart again:
systemctl restart mandibleRS256 Key Rotation
Section titled “RS256 Key Rotation”For RSA-based JWT:
- Generate new key pair:
openssl genrsa -out /etc/mantis/jwt/private-new.pem 2048openssl rsa -in /etc/mantis/jwt/private-new.pem \ -pubout -out /etc/mantis/jwt/public-new.pem- Point at the new pair and keep the old public key for verification. As
with HS256,
key_idmust change and the outgoing id must move toprevious_key_id:
MANDIBLE__JWT__PRIVATE_KEY_PATH="/etc/mantis/jwt/private-new.pem"MANDIBLE__JWT__PUBLIC_KEY_PATH="/etc/mantis/jwt/public-new.pem"MANDIBLE__JWT__KEY_ID="k2" # newMANDIBLE__JWT__PREVIOUS_PUBLIC_KEY_PATH="/etc/mantis/jwt/public-old.pem"MANDIBLE__JWT__PREVIOUS_KEY_ID="k1" # outgoingLeaving key_id at k1 while also setting previous_key_id = "k1" resolves
pre-rotation tokens to the new key, so they all fail verification — the
opposite of the overlap this procedure exists to provide.
- Follow the same restart and cleanup process as HS256
Zero-Downtime JWT Rotation
Section titled “Zero-Downtime JWT Rotation”For high-availability deployments:
Encryption Key Rotation
Section titled “Encryption Key Rotation”Master Key Rotation
Section titled “Master Key Rotation”Rotating the encryption master key requires re-encrypting all data:
Step-by-step:
- Generate new master key:
NEW_MASTER_KEY=$(openssl rand -base64 32)- Stop the services that hold the key:
systemctl stop mandible thorax- Run the rotation.
mantisctl key rotateconnects directly to the database and reads the connection string fromDATABASE_URL(or the config file) — there is no--database-urlflag, so export it first:
export DATABASE_URL="postgres://mantis:...@db-host:5432/mantis"
mantisctl key rotate \ --old-key "$MANTIS_ENCRYPTION_KEY" \ --new-key "$NEW_MASTER_KEY" \ --batch-size 100- Monitor progress:
# Key Rotation## Counting encrypted records...# storages_s3: 120 records# storages_git_auth: 40 records# identity_providers: 5 records# ...# Total: 165 records to rotate## Rotating storages_s3...# ✓ storages_s3: 120 processed, 120 successful, 0 failed# Rotating storages_git_auth...# ✓ storages_git_auth: 40 processed, 40 successful, 0 failed# ...# ✓ 165 records rotated successfully!# Checkpoint removed.Rotation covers the platform root and any tenant certificate authority alongside the other encrypted columns, so the signing keys move to the new encryption key with everything else.
- Set the new key in the services’ environment:
# In the systemd unit env file, not just this shellMANTIS_ENCRYPTION_KEY="$NEW_MASTER_KEY"- Start the services:
systemctl start mandible thorax- Verify:
mantisctl key verify --key "$NEW_MASTER_KEY"# ✓ storages_s3: 120 records OK# ✓ storages_git_auth: 40 records OK# ✓ identity_providers: 5 records OK# ...# ✓ 165 records verified successfully!Partial Rotation Recovery
Section titled “Partial Rotation Recovery”If rotation fails mid-process, it writes a checkpoint. Resume from where it stopped by
re-running the same command with --resume:
mantisctl key rotate \ --old-key "$MANTIS_ENCRYPTION_KEY" \ --new-key "$NEW_MASTER_KEY" \ --resumeTLS Certificate Rotation
Section titled “TLS Certificate Rotation”Server Certificate Renewal
Section titled “Server Certificate Renewal”For Mandible and Thorax server certificates:
- Generate new certificate:
mantisctl cert generate-server \ --name mantis --sans mantis.example.com --output-dir /etc/mantis/certsBy hand, the SAN is required — rustls verifies the hostname against it, and a certificate without one is rejected:
openssl req -new -key /etc/mantis/certs/server.key \ -out /etc/mantis/certs/server-new.csr \ -subj "/CN=mantis.example.com"
cat > server.ext <<'EOF'basicConstraints = CA:FALSEkeyUsage = digitalSignature, keyEnciphermentextendedKeyUsage = serverAuthsubjectAltName = DNS:mantis.example.comEOF
openssl x509 -req -in server-new.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out /etc/mantis/certs/server-new.crt \ -days 365 -sha256 -extfile server.ext- Verify new certificate:
openssl verify -CAfile ca.crt /etc/mantis/certs/server-new.crt- Replace certificate:
cp /etc/mantis/certs/server-new.crt /etc/mantis/certs/server.crt- Reload service:
# Graceful reload (if supported)systemctl restart mandible
# Or restartsystemctl restart mandibleZero-Downtime Certificate Rotation
Section titled “Zero-Downtime Certificate Rotation”With load balancer:
#!/bin/bashSERVERS="mandible1 mandible2"
for server in $SERVERS; do echo "Rotating certificate on $server..."
# Drain from load balancer curl -X POST "http://lb/api/drain/$server" sleep 10
# Update certificate scp server-new.crt "$server:/etc/mantis/certs/server.crt" ssh "$server" "systemctl restart mandible"
# Wait for health check sleep 5
# Re-enable in load balancer curl -X POST "http://lb/api/enable/$server"
echo "$server rotated successfully"doneAgent Certificate Renewal
Section titled “Agent Certificate Renewal”For Tarsus agent certificates:
Thumbprint mode:
Use tarsus rotate-certificate. It swaps the certificate in place and keeps the
agent’s registration and approved status, so the agent never returns to the
pending queue:
# On the agent: issue the replacement (both EKUs + SAN, see Certificate Rotation)mantisctl cert generate-server \ --name "$(hostname)" \ --sans "$(hostname -f)" \ --output-dir /tmp/rotate
tarsus rotate-certificate \ --new-cert /tmp/rotate/$(hostname)-cert.pem \ --new-key /tmp/rotate/$(hostname)-key.pem \ --reason "scheduled rotation"
systemctl restart tarsus@<instance>CA-signed mode:
# Generate new CSRopenssl req -new -key /etc/mantis/certs/tarsus-key.pem \ -out /tmp/agent-new.csr \ -subj "/CN=$(hostname)"
# Submit to CA for signing# ... CA signing process ...
# Install new certificatecp agent-new.crt /etc/mantis/certs/tarsus-cert.pem
# Restart agentsystemctl restart tarsusCA Certificate Rotation
Section titled “CA Certificate Rotation”Planning CA Rotation
Section titled “Planning CA Rotation”CA rotation is complex and requires careful planning:
┌─────────────────────────────────────────────────────────────┐│ CA Rotation Timeline │├─────────────────────────────────────────────────────────────┤│ ││ T-90 days: Generate new CA ││ T-60 days: Begin issuing certs from new CA ││ T-30 days: All new certs use new CA ││ T-0: Old CA expires ││ ││ During overlap: ││ - Thorax trusts both CAs ││ - New agents get new CA certs ││ - Old agents continue working ││ │└─────────────────────────────────────────────────────────────┘CA Rotation Steps
Section titled “CA Rotation Steps”- Generate new CA:
openssl genrsa -out /etc/mantis/ca/new-ca.key 4096openssl req -x509 -new -nodes \ -key /etc/mantis/ca/new-ca.key \ -out /etc/mantis/ca/new-ca.crt \ -days 3650 \ -subj "/CN=Mantis CA v2"- Configure trust for both CAs:
# Concatenate CA certificatescat /etc/mantis/ca/ca.crt /etc/mantis/ca/new-ca.crt > \ /etc/mantis/ca/combined-ca.crt[tls]ca_cert_path = "/etc/mantis/ca/combined-ca.crt"-
Replace the stored root. Mandible signs from the certificate authority stored in its database, not from a file, so copying a new certificate over
ca.crtchanges what Thorax trusts without changing what Mandible signs with. Until the stored root is replaced, every certificate issued still comes from the old one.Replacing it is disruptive by design.
mantisctl cert import-carefuses while a root is stored, and a root with tenant certificate authorities beneath it cannot be removed until those are removed first — the database restricts it, so that removing a root cannot silently strand the tenants that chain to it.Terminal window # Tenant authorities first, then the root, then the replacement.mantisctl cert import-ca --cert /etc/mantis/ca/new-ca.crt \--key /etc/mantis/ca/new-ca.key -
Re-enroll every agent. Certificates issued by the old root keep verifying only while Thorax still trusts it, which is what the combined trust file in step 2 is for. Each agent needs a certificate from the new root before that trust is withdrawn.
-
Withdraw trust in the old CA once no agent presents its certificates:
cp /etc/mantis/ca/new-ca.crt /etc/mantis/ca/ca.crtEmergency Rotation
Section titled “Emergency Rotation”Key Compromise Response
Section titled “Key Compromise Response”When a key is suspected compromised:
Immediate actions:
- JWT key compromise:
# Generate new key immediatelyNEW_SECRET=$(openssl rand -base64 32)
# Update and restart (accepts service interruption)sed -i "s/secret = .*/secret = \"$NEW_SECRET\"/" /etc/mantis/mandible.tomlsystemctl restart mandible
# All existing tokens invalidated- Master encryption key compromise:
# Rotate immediately (may take time for large datasets)mantisctl key rotate \ --old-key "$COMPROMISED_KEY" \ --new-key "$(openssl rand -base64 32)" \ --force- Agent certificate compromise:
# Revoke the compromised registration immediately (by ID or thumbprint prefix)mantisctl cert revoke "<REGISTRATION_UUID>" \ --reason "Certificate compromised"Post-Incident
Section titled “Post-Incident”After emergency rotation:
- Document the incident
- Review access logs
- Identify root cause
- Update security procedures
- Conduct security review
Automation
Section titled “Automation”Rotation Reminder Script
Section titled “Rotation Reminder Script”#!/bin/bash# Check JWT key ageJWT_KEY_DATE=$(stat -c %Y /etc/mantis/jwt/private.pem 2>/dev/null || echo 0)NOW=$(date +%s)AGE_DAYS=$(( (NOW - JWT_KEY_DATE) / 86400 ))
if [ $AGE_DAYS -gt 80 ]; then echo "WARNING: JWT key is $AGE_DAYS days old (rotation recommended at 90)"fi
# Check certificate expiryCERT_EXPIRY=$(openssl x509 -enddate -noout -in /etc/mantis/certs/server.crt | cut -d= -f2)EXPIRY_EPOCH=$(date -d "$CERT_EXPIRY" +%s)DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then echo "WARNING: Server certificate expires in $DAYS_LEFT days"fiAutomated Certificate Renewal
Section titled “Automated Certificate Renewal”Using cert-manager (Kubernetes):
apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: mantis-serverspec: secretName: mantis-tls duration: 8760h # 1 year renewBefore: 720h # 30 days before issuerRef: name: mantis-ca kind: ClusterIssuer dnsNames: - mantis.example.comRotation Schedule
Section titled “Rotation Schedule”Recommended Schedule
Section titled “Recommended Schedule”| Key/Certificate | Rotation Frequency | Method |
|---|---|---|
| JWT signing key | 90 days | Overlap rotation |
| Encryption master key | 365 days | Re-encryption |
| Server TLS certificates | 365 days | Reload |
| Agent certificates | 365 days | Per-agent |
| CA certificate | 5-10 years | Planned migration |
Calendar Template
Section titled “Calendar Template”┌─────────────────────────────────────────────────────────────┐│ Annual Rotation Calendar │├─────────────────────────────────────────────────────────────┤│ ││ Q1 (Jan-Mar): ││ - JWT key rotation ││ - Review certificate expiry dates ││ ││ Q2 (Apr-Jun): ││ - JWT key rotation ││ - Agent certificate batch renewal ││ ││ Q3 (Jul-Sep): ││ - JWT key rotation ││ - Encryption master key rotation ││ ││ Q4 (Oct-Dec): ││ - JWT key rotation ││ - Server certificate renewal ││ - Annual security review ││ │└─────────────────────────────────────────────────────────────┘Verification
Section titled “Verification”Post-Rotation Checks
Section titled “Post-Rotation Checks”After any key rotation:
# Verify JWT authenticationcurl -X POST https://mantis.example.com/api/v1/auth/login \ -d '{"username":"test","password":"test"}' \ -H "Content-Type: application/json"
# Verify encryptionmantisctl key verify --key "$MANTIS_ENCRYPTION_KEY"
# Verify TLS# Point at whatever terminates TLS -- usually the reverse proxy on 443.# Mandible's own port 3000 is plain HTTP unless [server.tls] is configured.openssl s_client -connect mantis.example.com:443 < /dev/null
# Verify agent connectivitymantisctl cert list --status approvedRollback Procedure
Section titled “Rollback Procedure”If rotation causes issues:
- JWT keys: Restore previous key to
secretand restart - Encryption keys: Keep old key, investigate issue
- Certificates: Restore from backup, restart
# Example: Restore JWT keycp /backup/jwt-secret /etc/mantis/secrets/jwt-secretexport MANDIBLE__JWT__SECRET=$(cat /etc/mantis/secrets/jwt-secret)systemctl restart mandibleBest Practices
Section titled “Best Practices”1. Plan Ahead
Section titled “1. Plan Ahead”- Schedule rotations during low-traffic periods
- Notify teams before rotation
- Have rollback plan ready
2. Test First
Section titled “2. Test First”- Test rotation in staging environment
- Verify application functionality
- Check monitoring for errors
3. Document Everything
Section titled “3. Document Everything”- Record rotation date and time
- Document any issues encountered
- Update runbooks as needed
4. Monitor
Section titled “4. Monitor”- Watch error rates during rotation
- Check authentication success rates
- Monitor agent connectivity
5. Automate Where Possible
Section titled “5. Automate Where Possible”- Use cert-manager for certificates
- Implement rotation scripts
- Set up expiry alerts
Next Steps
Section titled “Next Steps”- JWT Configuration - JWT settings details
- TLS Certificates - Certificate management
- Encryption at Rest - Encryption configuration
