Packages

Key management service with embedded OTP runtime, optional HTTP API, SQLite/PostgreSQL storage, and external RMK providers

Current section

Files

Jump to
kms docs use_cases standalone.md
Raw

docs/use_cases/standalone.md

# Standalone KMS with SQLite

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:

```sh
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:

```elixir
: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:

```text
$KMS_DATA_DIR/kms_dev.sqlite3
```

Production default name is:

```text
$KMS_DATA_DIR/kms.sqlite3
```

See [Database configuration](../database.md).

## 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:

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

Runtime env:

```sh
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:

```sh
mix ecto.migrate
```

Health check:

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

Encrypt through HTTP:

```sh
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:

```elixir
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:

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

Release validation:

```text
mix test.sqlite
mix test
284 tests, 0 failures

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