Standalone KMS with SQLite

Copy Markdown View Source

Use this pattern when you want a dedicated KMS authority process without operating PostgreSQL for KMS itself.

KMS defaults to SQLite. A minimal standalone deployment needs:

  • one writable KMS data directory;
  • one RMK provider, local or external;
  • optional HTTP admin API when other apps call KMS remotely.

When to use

Good fit:

  • single-host KMS service;
  • embedded KMS inside one Elixir release;
  • development/staging environments;
  • small internal deployments where a separate PostgreSQL service is unnecessary;
  • remote KMS authority process backed by local disk and backups.

Poor fit:

  • multi-writer clustered KMS nodes sharing one database file;
  • high-write workloads across many hosts;
  • environments where local disk backup/restore is weak;
  • deployments that already standardize on managed PostgreSQL for all service state.

SQLite is still real persistence. Protect the database file, WAL/SHM sidecars, and RMK key material.

Minimal source checkout run

From the repository root:

export KMS_DATABASE_BACKEND=sqlite # optional; default
export KMS_DATA_DIR="$PWD/tmp/kms-standalone"
export KMS_RMK_LOCAL_KEY_PATH="$PWD/tmp/kms-standalone/rmk.key"

mix deps.get
mix ecto.migrate
iex -S mix

In IEx:

:ok = KMS.RMK.ensure_ready()
{:ok, _kek} = KMS.create_alias("standalone/docs", tags: ["standalone", "docs"])

{:ok, encrypted} =
  KMS.encrypt_alias(
    "standalone secret",
    "standalone/docs",
    aad: "standalone:v1:docs:example"
  )

{:ok, "standalone secret"} =
  KMS.decrypt_alias(encrypted, "standalone/docs", aad: "standalone:v1:docs:example")

The SQLite database is stored at:

$KMS_DATA_DIR/kms_dev.sqlite3

Production default name is:

$KMS_DATA_DIR/kms.sqlite3

See Database configuration.

Standalone remote service

When application servers should not have direct access to KMS tables/RMK material, run KMS as a remote service and use KMS.Client.Remote from apps.

Server config:

config :kms, :api,
  server: true,
  port: 4004,
  scheme: :http,
  api_token: System.fetch_env!("KMS_API_TOKEN")

Runtime env:

export KMS_DATABASE_BACKEND=sqlite
export KMS_DATA_DIR=/var/lib/kms
export KMS_RMK_LOCAL_KEY_PATH=/var/lib/kms/rmk.key
export KMS_SESSION_HASH_KEY=<high-entropy-secret>
export KMS_API_TOKEN=<high-entropy-admin-token>
export ERL_CRASH_DUMP_BYTES=0

Run migrations before serving traffic:

mix ecto.migrate

Health check:

curl -fsS http://localhost:4004/health

Encrypt through HTTP:

curl -fsS \
  -H "authorization: Bearer $KMS_API_TOKEN" \
  -H "content-type: application/json" \
  -d '{"alias":"standalone/docs","cleartext":"secret","opts":{"aad":"standalone:v1:docs:example"}}' \
  http://localhost:4004/api/encrypt

Application client config:

config :kms, :client, KMS.Client.Remote

config :kms, KMS.Client.Remote,
  base_url: "https://kms.internal.example.com",
  api_token: System.fetch_env!("KMS_REMOTE_API_TOKEN")

The HTTP API is an admin backend API. Put it behind TLS or a trusted private network. Do not expose it directly to browsers or the public internet.

Files to back up

For local RMK + SQLite, back up all required material together but store copies with separate access controls:

  • SQLite DB: $KMS_DATA_DIR/kms.sqlite3;
  • SQLite sidecars: $KMS_DATA_DIR/kms.sqlite3-wal, $KMS_DATA_DIR/kms.sqlite3-shm when present;
  • local RMK key file: KMS_RMK_LOCAL_KEY_PATH;
  • fallback private keys, if configured, from offline custody.

A DB backup without RMK material should not decrypt KMS secrets. RMK material without the DB is not enough to recover data. You need both for full restore.

Operational notes

  • Use one KMS writer process per SQLite database file.
  • Keep journal_mode: :wal, foreign_keys: :on, and busy_timeout enabled.
  • Store SQLite on durable local disk, not ephemeral container filesystem.
  • Protect directory permissions for DB and RMK key files.
  • Test restore before production.
  • For higher availability or multi-node writes, use PostgreSQL instead.

Evidence

SQLite support is covered by KMS adapter tests and the Phoenix example now uses KMS SQLite separately from the Phoenix app database.

Useful checks:

mix test.sqlite
cd example/phoenix_example && mix ecto.reset && mix test

Release validation:

mix test.sqlite
mix test
284 tests, 0 failures

cd example/phoenix_example && mix ecto.reset && mix test
7 tests, 0 failures