Skip to content

Encryption at Rest

IndentiaDB encrypts its write-ahead logs, checkpoints, Raft state, and backups at the application layer with AES-256-GCM (via a FIPS-capable cryptography backend), using envelope encryption rooted in HashiCorp Vault Transit. Encryption is off by default and can be enabled on an existing deployment without downtime.


Key Hierarchy

Vault Transit KEK (never leaves Vault)
        │  unwraps once at boot
        ▼
Root key (per deployment / tenant, in-memory only, zeroized)
        │  wraps locally
        ▼
Per-file DEK (random 256-bit, one per file)
        │
        ▼
AES-256-GCM sealed file
  • Vault is contacted once per boot (one transit/decrypt call) to unwrap the root key — not per file.
  • Every sealed file gets a fresh random Data Encryption Key; the wrapped DEK travels in the sealed file's header.
  • The GCM authentication tag is bound to additional authenticated data (format version, cipher, key ID, tenant, file identity), so a sealed file cannot be silently swapped or replayed into a different context.

What Is Encrypted

Surface Encrypted
RDF write-ahead log (WAL), shard WALs, meta WAL ✅ AES-256-GCM
RDF checkpoints ✅ AES-256-GCM
Raft log entries ✅ AES-256-GCM (per-node DEK)
Raft snapshots ✅ AES-256-GCM (node-agnostic — restorable on any node)
Backups (indentiagraph backup create) ✅ AES-256-GCM, chunked with an authenticated footer
Memory-mapped index files (permutations, vocabulary, vectors) and the :9200 mirror store ❌ Rely on volume-level encryption (LUKS / cloud CMK)

Why the mmap indexes use volume encryption

The query-critical index files are accessed via zero-copy memory mapping below the page cache. Application-level encryption there would forfeit that performance model, so full-volume encryption (LUKS, Azure/AWS CMK-encrypted disks) is the intended layer for them. A production deployment uses both: app-level sealing for WAL/checkpoint/Raft/backups, volume encryption for everything.


Configuration

Variable Default Effect
IG_ENCRYPTION_ENABLED off Master switch. Off means plaintext.
IG_ENCRYPTION_REQUIRED off (tri-state) Fail closed: refuse to boot on key-provider failure, a static dev key, or plaintext files where sealed ones are expected. See below.
IG_FIPS_REQUIRED off Assert the FIPS-validated crypto backend at startup; fails closed on a non-FIPS build (build with --features fips).
IG_CRYPTO_TENANT_ID default Tenant slug bound into key derivation and authenticated data.
IG_CLUSTER_ID — Stable cluster UUID; mandatory whenever encryption is active. Raft snapshot authentication binds to it, so record it with your recovery secrets.
IG_CRYPTO_VAULT_ADDR — Vault address (HTTPS-only in production).
IG_CRYPTO_VAULT_MOUNT transit Vault Transit mount.
IG_CRYPTO_VAULT_KEY ig-tenant-<tenant> Transit key name.
IG_CRYPTO_VAULT_WRAPPED_ROOT[_FILE] — The vault:v1:… wrapped root key.
IG_CRYPTO_VAULT_CA_FILE — Explicit Vault trust root; required under IG_PRODUCTION.
IG_VAULT_TOKEN[_FILE] — Vault token.
IG_CRYPTO_KEYRING_FILE — JSON multi-scope keyring (system / per-tenant / per-group keys) replacing the single-key variables.
IG_CRYPTO_KEY_FILE / IG_CRYPTO_STATIC_KEY — Dev/test static keys. Rejected when encryption is required — node-held key custody is not KMS custody.
IG_CRYPTO_MIGRATE off One-boot sanctioned plaintext→sealed re-seal pass (atomic, bounded, resumable).

The IG_ENCRYPTION_REQUIRED Tri-State

Value Behavior
1 / true / yes / on Required — fail closed.
0 / false / no / off Explicitly not required — the deliberate opt-out during a migration window, even under IG_PRODUCTION=true.
Unset, empty, or unrecognized Falls through to IG_PRODUCTION: production implies required. A typo can never silently disable the policy.

Vault Setup

# One-time provisioning
vault secrets enable transit
vault write -f transit/keys/ig-tenant-acme

# Wrap a random 32-byte root key; store the returned vault:v1:… value
vault write transit/encrypt/ig-tenant-acme \
  plaintext="$(openssl rand -base64 32)"

Grant IndentiaDB a least-privilege policy:

path "transit/decrypt/ig-tenant-*" {
  capabilities = ["update"]
}

Vault-side KEK rotation (vault write -f transit/keys/ig-tenant-acme/rotate) requires no data re-encryption — the wrapped root key is simply re-wrapped under the new KEK version.


Enabling Encryption on an Existing Deployment

Readers accept both plaintext and sealed file formats, so encryption can be introduced with rolling upgrades and no flag-day:

  1. Enable dark: set IG_ENCRYPTION_ENABLED=true plus the Vault variables, keep IG_ENCRYPTION_REQUIRED=false. New WAL segments, checkpoints, and Raft state are written sealed; existing plaintext files remain readable.
  2. Re-seal history: boot once with IG_CRYPTO_MIGRATE=true. Existing plaintext files are re-sealed atomically before the server starts serving; then remove the flag.
  3. Fail closed: flip IG_ENCRYPTION_REQUIRED=true (or simply run under IG_PRODUCTION=true and drop the explicit false). From now on a missing key provider or unexpected plaintext file refuses boot.

Record IG_CLUSTER_ID with your recovery secrets

Encrypted Raft snapshots authenticate against the cluster UUID. A disaster-recovery restore on fresh nodes must reuse the original IG_CLUSTER_ID — a different value fails restore authentication by design (a stolen snapshot cannot be used to fork a replacement cluster).


Multi-Tenant Keyring

For deployments that need separate keys per tenant (crypto-shred, per-customer KMS custody), IG_CRYPTO_KEYRING_FILE points at a JSON registry of scoped keys:

{
  "default_scope": "system",
  "missing_scope_policy": "reject",
  "keys": [
    { "scope": "system",       "vault_key": "ig-system",      "wrapped_root_file": "/etc/ig/keys/system.wrapped" },
    { "scope": "tenant:acme",  "vault_key": "ig-tenant-acme", "wrapped_root_file": "/etc/ig/keys/acme.wrapped" }
  ]
}

Every root key is unwrapped and canary-tested at boot; one unavailable or duplicate key aborts startup. When encryption is required, missing_scope_policy must be reject — an unknown tenant scope never silently falls back to the default key.


See Also