Root master keys (RMK)

Copy Markdown View Source

KMS stores encrypted application data with key-encryption keys (KEKs). The root master key (RMK) protects KMS secret material before it is persisted.

RMK-protected material includes:

  • KEK master keys;
  • principal private-key envelopes;
  • session private-key envelopes;
  • TOTP seeds in factor config;
  • RMK PDK payloads created by KMS.encrypt_rmk_pdk/3.

The RMK provider is the highest-level encryption boundary for the KMS database. A database backup or database-only compromise should not be enough to decrypt stored KMS secret material unless the attacker also has the RMK key material or access to the RMK provider.

Examples

Most applications should use higher-level KMS APIs such as KMS.encrypt_alias/3. Direct RMK calls are useful when application code needs to protect its own internal secret material with the configured KMS RMK provider.

Encrypt and decrypt with the configured provider:

{:ok, encrypted} = KMS.RMK.encrypt("secret material")
{:ok, "secret material"} = KMS.RMK.decrypt(encrypted)

Bind encrypted data to context with authenticated data:

{:ok, encrypted} = KMS.RMK.encrypt("secret material", aad: "tenant:123")
{:ok, "secret material"} = KMS.RMK.decrypt(encrypted, aad: "tenant:123")

Check that the configured provider is reachable before serving traffic:

:ok = KMS.RMK.ensure_ready()

Use KMS-provided password-derived-key helpers when callers provide their own high-entropy secret:

{:ok, encrypted} = KMS.encrypt_rmk_pdk("secret material", high_entropy_secret)
{:ok, "secret material"} = KMS.decrypt_rmk_pdk(encrypted, high_entropy_secret)

Provider selection

Configure one provider for the KMS authority process:

config :kms, :rmk_provider, KMS.RMK.Local
# config :kms, :rmk_provider, KMS.RMK.StackIT
# config :kms, :rmk_provider, KMS.RMK.AWS
# config :kms, :rmk_provider, KMS.RMK.GCP

Application config overrides environment defaults. Per-call opts override application config.

Provider docs:

All external RMK providers use Req directly. No cloud SDKs are required.

Encrypting and decrypting with RMK

Most applications should use the higher-level KMS APIs (KMS.encrypt_alias/3, sessions, factors). KMS calls the RMK provider internally when it must protect or unwrap stored KMS secret material.

Direct RMK calls are available for internal/provider checks and RMK PDK flows:

{:ok, encrypted} = KMS.RMK.encrypt("secret material")
{:ok, "secret material"} = KMS.RMK.decrypt(encrypted)

Provider-specific options can be passed per call:

{:ok, encrypted} = KMS.RMK.encrypt("secret material", aad: "tenant:123")
{:ok, "secret material"} = KMS.RMK.decrypt(encrypted, aad: "tenant:123")

External providers map :aad into provider-native authenticated data where supported:

  • AWS: encryption context entry "kms:rmk:aad".
  • GCP: additionalAuthenticatedData.
  • StackIT: provider API does not expose a separate AAD field in the current client.
  • Local: AES-GCM AAD, defaulting to "kms-rmk-local" when none is supplied.

Changing AAD/context between encrypt and decrypt can make ciphertext undecryptable.

Readiness check:

:ok = KMS.RMK.ensure_ready()

Recommendations

Use KMS.RMK.Local by default

KMS.RMK.Local is the default provider. It is a good fit when:

  • KMS is the sole authority for the encrypted data;
  • the KMS host, KMS database, and local RMK key file are in the same operational trust boundary;
  • DB-only disclosure is the main scenario to defend against;
  • a full break-in to the KMS host itself is acceptable, negligible, or handled by other controls.

Local RMK still helps: a leaked DB dump, DB snapshot, or read-only database compromise does not include the local RMK key file when storage/backup boundaries are separate.

Local RMK does not help enough if an attacker can read both the database and the RMK key file, or can execute code as the KMS process.

Use an external RMK provider for stronger separation

Use StackIT, AWS KMS, or Google Cloud KMS when:

  • a KMS database compromise cannot be tolerated;
  • you want DB data protected by a separate key service, IAM policy, audit log, or HSM boundary;
  • you want to reduce blast radius of leaked DB backups or replicated database storage;
  • KMS operators and key-custody operators should be separated.

An external RMK provider mainly reduces DB-only blast radius. It does not make a fully compromised KMS authority process harmless: that process normally has credentials or workload identity that can ask the RMK provider to decrypt.

Bring your own offline key material when you need migratability

If you may need to move away from a cloud/provider RMK later, prefer provider keys backed by your own offline key material when the provider supports it:

  • imported key material / BYOK for symmetric KMS keys;
  • external key origin / external key manager setups;
  • provider-specific offline public/private material where supported.

When the provider import flow uses public/private key wrapping, keep the private half offline and import/wrap with the provider-side public material only. Keep all offline material outside the KMS database and outside normal KMS hosts. Test restore and provider migration before production.

If you use provider-generated, non-exportable key material, you may still be secure, but you are less migratable. Migration then requires the old provider/key to stay available until every persisted RMK ciphertext has been decrypted and re-encrypted under the new provider.

Ciphertext format

External providers store new RMK ciphertexts as envelopes:

kms-rmk:v1:<provider>:<key_ref_b64url>:<ciphertext_b64url>
  • provider: stackit, aws, or gcp.
  • key_ref: provider key reference used for audit/routing.
  • ciphertext: provider ciphertext bytes.

Legacy raw ciphertext is still passed to the configured provider for backwards compatibility.

Provider mismatch fails closed. Do not switch providers for existing data without a rewrap/migration plan.

The local provider currently stores its local AES-GCM ciphertext directly for compatibility.

Caching

External RMK calls can be slow and may cost money. KMS caches successful RMK decrypt results to avoid excessive provider calls.

Cached items include:

  • plaintext KEK master keys;
  • decrypted principal private-key payloads;
  • decrypted session private-key payloads;
  • decrypted RMK PDK payloads.

Default RMK cache TTL is 5 minutes:

config :kms, :rmk_cache_ttl_ms, :timer.minutes(5)

Per-call override:

KMS.decrypt_alias(encrypted, "alias", rmk_cache_ttl_ms: :timer.seconds(30))

Disable RMK decrypt caching for one call:

KMS.decrypt_alias(encrypted, "alias", rmk_cache_ttl_ms: 0)

Some internal callers also honor ttl_ms or expires_at. Session private-key payload caching is capped by session expiry.

Caching tradeoff:

  • Higher TTL: fewer external KMS calls, lower latency, lower provider cost.
  • Lower TTL or 0: less plaintext key material resident in KMS process memory.

The cache process is marked sensitive, but process memory is still in scope for a KMS host compromise. Treat RMK cache TTL as a security/performance decision.

Operational rules

  • Configure external RMK credentials only in the KMS authority process, never in remote-client-only apps.
  • Do not log plaintext, ciphertext, RMK key files, service account JSON, bearer tokens, AWS signed headers, or request bodies.
  • Back up local RMK key files and offline BYOK material separately from database backups.
  • Test KMS.RMK.ensure_ready/1 during deployment readiness.
  • Plan provider/key rotation before changing :rmk_provider or key IDs.