Packages
Key management service with embedded OTP runtime, optional HTTP API, SQLite/PostgreSQL storage, and external RMK providers
Current section
Files
Jump to
Current section
Files
docs/quickstart.md
# Quickstart: KMS in a Phoenix app
This guide assumes you are an Elixir/Phoenix developer who wants field-level encryption in a normal Phoenix application.
It starts with a vanilla Phoenix app in `example/phoenix_example`, adds `:kms` as a dependency, configures KMS to start with the app, and encrypts one Ecto schema field through owner-scoped KMS aliases and AAD.
KMS uses SQLite by default. The Phoenix app can keep its normal PostgreSQL app database while KMS stores its own key tables in `tmp/kms/kms_dev.sqlite3`.
Working example in this repo:
```sh
example/phoenix_example
```
Evidence this example works:
```sh
cd example/phoenix_example
mix ecto.reset
mix test
# 7 tests, 0 failures
```
## 1. Start with Phoenix
Generate a normal Phoenix application:
```sh
mkdir -p example
cd example
mix phx.new phoenix_example --no-install
cd phoenix_example
```
The committed example was generated this way, then patched to act like a standalone app inside this repository.
## 2. Add KMS as a dependency
Add KMS to your application's `mix.exs`:
```elixir
defp deps do
[
{:kms, "~> 0.1.0"},
{:phoenix, "~> 1.8.1"},
# ... normal Phoenix deps
]
end
```
The committed `example/phoenix_example` application uses `{:kms, path: "../.."}` so it exercises the repository checkout directly.
Start the KMS OTP application with the Phoenix app:
```elixir
def application do
[
mod: {PhoenixExample.Application, []},
extra_applications: [:logger, :runtime_tools, :kms]
]
end
```
The KMS OTP application starts:
- the configured KMS repo, delegating to SQLite by default
- `KMS.Cache`
- session cleanup
- optional fallback bootstrap
## 3. Configure Phoenix and KMS repos
The example uses two persistence stores:
- `PhoenixExample.Repo` for application tables.
- KMS SQLite repo for KMS key tables.
In `config/config.exs`:
```elixir
config :phoenix_example,
ecto_repos: [PhoenixExample.Repo, KMS.Repo.SQLite]
config :kms, ecto_repos: [KMS.Repo.SQLite]
config :kms, KMS.Repo, repo: KMS.Repo.SQLite
config :kms, :client, KMS.Client.Local
config :kms, :rmk_provider, KMS.RMK.Local
config :kms, KMS.RMK.Local, key_path: Path.expand("../priv/kms_rmk_#{config_env()}.key", __DIR__)
```
In `config/dev.exs`, keep Phoenix on PostgreSQL and KMS on SQLite:
```elixir
config :phoenix_example, PhoenixExample.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "phoenix_example_dev"
config :kms, KMS.Repo.SQLite,
database: Path.expand("../tmp/kms/kms_dev.sqlite3", __DIR__),
journal_mode: :wal,
foreign_keys: :on,
busy_timeout: 5_000,
pool_size: 10,
priv: "priv/repo",
migration_primary_key: false,
migration_foreign_key: [type: :text],
migration_timestamps: [type: :utc_datetime_usec]
```
KMS can use PostgreSQL too, but SQLite is the simplest embedded/default path. See [Database configuration](database.md).
For production, configure a high-entropy `KMS_SESSION_HASH_KEY`, explicit RMK key path or external RMK provider, SQLite database backups, TLS, and crash dump policy. See [Security](security.md) and [Root master keys](rmk.md).
## 4. Create an encrypted Ecto field
The example stores documents with encrypted body text:
```elixir
defmodule PhoenixExample.Documents.Document do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: false}
@foreign_key_type :binary_id
schema "documents" do
field :owner_id, :string
field :title, :string
field :encrypted_body, :string, redact: true
field :body, :string, virtual: true, redact: true
timestamps(type: :utc_datetime)
end
def changeset(document, attrs) do
document
|> cast(attrs, [:id, :owner_id, :title, :body])
|> maybe_put_id()
|> validate_required([:id, :owner_id, :title, :body])
|> validate_length(:owner_id, min: 1, max: 255)
|> validate_length(:title, min: 1, max: 255)
|> encrypt_body()
|> validate_required([:encrypted_body])
end
defp maybe_put_id(changeset) do
case get_field(changeset, :id) do
nil -> put_change(changeset, :id, Ecto.UUID.generate())
_id -> changeset
end
end
defp encrypt_body(%Ecto.Changeset{valid?: true} = changeset) do
owner_id = get_field(changeset, :owner_id)
document_id = get_field(changeset, :id)
body = get_field(changeset, :body)
with _kek <- ensure_alias!(owner_id),
{:ok, encrypted_body} <- encrypt_body(body, owner_id, document_id) do
put_change(changeset, :encrypted_body, encrypted_body)
else
{:error, reason} -> add_error(changeset, :body, "encryption failed: #{inspect(reason)}")
end
end
defp encrypt_body(changeset), do: changeset
def decrypt_body(%__MODULE__{encrypted_body: encrypted_body, owner_id: owner_id, id: document_id}) do
KMS.decrypt_alias(encrypted_body, alias_name(owner_id), aad: aad(document_id))
end
def alias_name(owner_id), do: "owner:#{owner_id}"
def aad(document_id), do: "document:#{document_id}/body"
defp ensure_alias!(owner_id) do
case KMS.create_alias(alias_name(owner_id)) do
{:ok, kek} -> kek
{:error, reason} -> raise "failed to ensure KMS alias: #{inspect(reason)}"
end
end
defp encrypt_body(body, owner_id, document_id) do
KMS.encrypt_alias(body, alias_name(owner_id), aad: aad(document_id))
end
end
```
Migration:
```elixir
def change do
create table(:documents, primary_key: false) do
add :id, :binary_id, primary_key: true
add :owner_id, :string, null: false
add :title, :string, null: false
add :encrypted_body, :text, null: false
timestamps(type: :utc_datetime)
end
end
```
The plaintext `body` field is virtual. The changeset encrypts it into `encrypted_body`; `body` is never persisted in the Phoenix application table.
## 5. Understand alias and AAD policy
For this small example, KMS policy lives directly in the `Document` module.
Each owner gets one KEK alias, e.g. `owner:alice`. Many documents for the same owner share that KEK.
AAD stays field-specific: `document:#{document_id}/body`. It binds encryption/decryption to the `body` field of that document row. Copying `encrypted_body` to another document id does not decrypt.
See [Additional authenticated data](aad.md).
## 6. Encrypt transparently in the changeset
The changeset owns encryption. The Phoenix context stays boring:
```elixir
def create_document(attrs) do
Repo.transaction(fn ->
case %Document{} |> Document.changeset(attrs) |> Repo.insert() do
{:ok, document} -> document
{:error, changeset} -> Repo.rollback(changeset)
end
end)
end
```
Reads decrypt before returning to caller:
```elixir
def get_document!(id) do
Document
|> Repo.get!(id)
|> with_body!()
end
```
Application code passes `body` to the changeset and gets `body` back from the context. The database stores only `encrypted_body`.
## 7. Prove plaintext stays out of the app table
The example test proves encryption, owner-scoped aliasing, and AAD binding:
```elixir
test "encrypts document body through KMS and decrypts transparently" do
assert {:ok, document} =
Documents.create_document(%{
owner_id: "alice",
title: "Launch notes",
body: "Keep this plaintext out of the application table"
})
stored = Repo.get!(Document, document.id)
assert stored.body == nil
refute stored.encrypted_body =~ "Keep this plaintext"
assert Documents.get_document!(document.id).body == document.body
assert %KMS.Kek{} = KMS.Keks.get_by_alias(Document.alias_name("alice"))
end
```
Run it:
```sh
cd example/phoenix_example
mix deps.get
mix ecto.reset
mix test
```
Current evidence from this worktree:
```text
7 tests, 0 failures
```
## 8. Use KMS directly from IEx
After setup:
```sh
iex -S mix
```
Create and read an encrypted document:
```elixir
{:ok, document} =
PhoenixExample.Documents.create_document(%{
owner_id: "alice",
title: "Private note",
body: "only plaintext in app memory"
})
PhoenixExample.Documents.get_document!(document.id).body
```
Inspect stored ciphertext:
```elixir
stored = PhoenixExample.Repo.get!(PhoenixExample.Documents.Document, document.id)
stored.encrypted_body
stored.body
```
`stored.body` is `nil`; `stored.encrypted_body` is ciphertext.
## Next steps
- Use [AAD](aad.md) consistently for encrypted fields.
- Choose and back up an [RMK provider](rmk.md).
- Read [Database configuration](database.md) for SQLite/PostgreSQL options.
- Read [Security](security.md) before production.
- Explore [use cases](use_cases.md), including [standalone KMS](use_cases/standalone.md).