Skip to content

Encryption at Rest

Configure encryption for sensitive data stored in the Mantis database.

Mantis encrypts sensitive data stored in PostgreSQL:

Data TypeEncryptionStorage
Storage credentials (S3, Git)AES-256-GCMDatabase
Sensitive variablesAES-256-GCMDatabase
TOTP secrets & backup codesAES-256-GCMDatabase
CA and certificate private keysAES-256-GCMDatabase
Identity-provider client secretsAES-256-GCMDatabase
External-identity tokensAES-256-GCMDatabase
SMTP / Slack / webhook credentialsAES-256-GCMDatabase
Execution plans & prompted valuesAES-256-GCMDatabase
Config-repo webhook secretsAES-256-GCMDatabase
AI analysis provider keysAES-256-GCMDatabase
API keysArgon2 hash (not reversible)Database

Each context is bound to its row with distinct additional authenticated data, so a ciphertext cannot be moved between rows. Losing the master key makes all of the above unreadable — including TOTP secrets and CA/certificate private keys.

Terminal window
# Generate 256-bit master key: 44 base64 characters ending in "="
openssl rand -base64 32

The encryption configuration is a single base64-encoded 32-byte key under the [encryption] table. There is no enabled toggle or algorithm selector — encryption is always AES-256-GCM, and supplying a valid key is what enables credential encryption.

Generate the key on the host that will hold it; never reuse one from documentation:

Terminal window
openssl rand -base64 32
[encryption]
key = "<output of openssl rand -base64 32>"

Store the key as an environment variable instead of in the config file:

Terminal window
export MANTIS_ENCRYPTION_KEY="$(openssl rand -base64 32)"

Mantis uses the configured 32-byte key directly as the AES-256-GCM key — there is no key-derivation (HKDF) step and no per-record or per-data-type key hierarchy. A fresh random 96-bit nonce is generated for each encryption operation, so the same key safely encrypts many records. Rotating the key re-encrypts all existing records (see Key Rotation).

Mantis uses AES-256-GCM (Galois/Counter Mode):

PropertyValue
AlgorithmAES-256
ModeGCM (authenticated encryption)
Key size256 bits
IV size96 bits (12 bytes)
Tag size128 bits (16 bytes)

Benefits:

  • Authenticated encryption (integrity + confidentiality)
  • Parallelizable encryption/decryption
  • Hardware acceleration support (AES-NI)

Encrypted values are stored as:

Base64(IV || Ciphertext || Tag)
ComponentSizePurpose
IV12 bytesInitialization vector
CiphertextVariableEncrypted data
Tag16 bytesAuthentication tag

Mark a variable as sensitive when creating it through the REST API (or the Lens UI). A variable created with variable_type: "sensitive" is encrypted at rest and its value is masked on read:

Terminal window
curl -X POST "https://api.mantis.example.com/api/v1/variable-sets/$SET_ID/variables" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "DB_PASSWORD", "value": "supersecret123", "variable_type": "sensitive"}'
┌─────────────────────────────────────────────────────────────┐
│ Create Variable │
├─────────────────────────────────────────────────────────────┤
│ │
│ Name * │
│ [DB_PASSWORD ] │
│ │
│ Value * │
│ [•••••••••••••••• ] │
│ │
│ ☑ Sensitive (encrypt value at rest) │
│ │
│ Scope: │
│ ○ Global ● Environment ○ Target │
│ │
│ [Cancel] [Create Variable] │
│ │
└─────────────────────────────────────────────────────────────┘

Storage backends are database-managed resources created through the API, not a config section — Mandible’s config is deny_unknown_fields and has no [storage] table, so putting one in mandible.toml fails startup. secret_access_key is encrypted on write and never returned by a read:

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "artifacts",
"bucket": "mantis-artifacts",
"region": "us-east-1",
"auth_type": "static",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}' \
"https://api.mantis.local/api/v1/storage/s3"

In addition to application-level encryption, consider:

LevelMethodProtection
ApplicationAES-256-GCMData at rest in DB
Databasepgcrypto extensionAdditional layer
StorageLUKS/dm-cryptDisk encryption
CloudAWS RDS encryptionManaged encryption

Enable for additional database-level encryption:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

For cloud deployments:

ProviderFeatureConfiguration
AWS RDSEncryption at restEnable in RDS console
AzureTDEEnable in Azure portal
GCPCloud SQL encryptionEnabled by default

API keys are hashed with Argon2 before storage, using the library’s default Argon2 parameters. These parameters are not configurable — there is no [api_keys] configuration section.

Rotate the master key annually or if compromised:

Stop the services first — they hold the key in memory and would write undecryptable records if left running during rotation. Export DATABASE_URL too; mantisctl key rotate connects to the database directly and has no --database-url flag.

Terminal window
systemctl stop mandible
export DATABASE_URL="postgres://mantis:...@db-host:5432/mantis"
# Both keys come from the environment; passed as arguments they would be visible
# in the process list for the whole rotation.
export MANTIS_ENCRYPTION_KEY="$OLD_KEY"
export MANTIS_NEW_ENCRYPTION_KEY=$(openssl rand -base64 32)
mantisctl key rotate
# Set MANTIS_ENCRYPTION_KEY to the new key in Mandible's env, then:
systemctl start mandible

See Key Rotation for the full procedure and why the stop is mandatory.

See Key Rotation for detailed procedures.

Database backups contain encrypted data:

Backup TypeEncryption Status
pg_dumpData remains encrypted
File backupData remains encrypted
SnapshotData remains encrypted

Backup the master key securely:

  • Store in separate secure location
  • Use secrets management (Vault, AWS Secrets Manager)
  • Document key recovery procedure
  • Test key recovery periodically

For disaster recovery, you need:

  1. Database backup
  2. Master key
  3. Configuration files
Terminal window
# Restore database
pg_restore -d mantis mantis_backup.dump
# Set master key (from secure backup)
export MANTIS_ENCRYPTION_KEY="backed-up-key"
# Start services
systemctl start mandible thorax

Problem: Decryption failed: authentication error (wrong key, AAD, or tampered data)

Causes:

  • Wrong master key
  • Corrupted data
  • Data modified in database

Solution:

Terminal window
# Verify that all encrypted records decrypt with the configured key
# (reads MANTIS_ENCRYPTION_KEY, or pass --key explicitly)
mantisctl key verify

Problem: SECURITY ERROR: Encryption key is not configured (missing key) or SECURITY ERROR: Encryption key must be exactly 32 bytes (256 bits), got N bytes (wrong length) or SECURITY ERROR: Encryption key is not valid base64 (encoding error).

Diagnosis:

Terminal window
# Check key format (should be base64)
echo "$MANTIS_ENCRYPTION_KEY" | base64 -d | wc -c
# Should output: 32

Solution:

Terminal window
# Ensure key is properly formatted
export MANTIS_ENCRYPTION_KEY="$(cat /path/to/key | tr -d '\n')"

Encryption adds minimal overhead:

AES-256-GCM on a single field is microseconds of CPU; the dominant cost is the database round trip. Rotation processes records in batches (--batch-size, default 100), sequentially — not in parallel.

If performance issues occur:

Terminal window
# Check for hardware acceleration
grep -m1 'aes' /proc/cpuinfo
# Should show: aes (AES-NI support)

Confirm that every encrypted record can be decrypted with the configured key. This is the primary integrity check after a key change or restore:

Terminal window
# Uses MANTIS_ENCRYPTION_KEY, or pass --key "<base64 key>"
mantisctl key verify

To generate a fresh key (for example before a rotation), use:

Terminal window
mantisctl key generate
Terminal window
# Good: Environment variable
export MANTIS_ENCRYPTION_KEY="..."
# Works, but leaves the key in a file on disk; prefer the environment in production
# [encryption]
# key = "..."

Use a secrets manager:

ToolIntegration
HashiCorp VaultKV secrets engine
AWS Secrets ManagerIAM integration
Azure Key VaultManaged identity
GCP Secret ManagerService account

Example with Vault:

Terminal window
export MANTIS_ENCRYPTION_KEY=$(vault kv get -field=key secret/mantis/encryption)
Key TypeRotation Period
Master keyAnnually
After compromiseImmediately
After personnel changeAs needed

Enable multiple layers:

[encryption]
key = "<base64 32-byte key>" # Application-level AES-256-GCM
[database]
url = "postgres://...?sslmode=require" # TLS in transit
# Plus: Disk encryption on database server
# Plus: Cloud provider encryption (RDS, etc.)

Maintain documentation for:

  • Key storage location
  • Key recovery procedure
  • Rotation schedule
  • Emergency contacts