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.sqlite3Production default name is:
$KMS_DATA_DIR/kms.sqlite3Standalone 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-shmwhen 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, andbusy_timeoutenabled. - 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