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/authz-bindings.md
# Authorization and bindings
KMS bindings let KMS enforce which principal can perform which operation on which scope.
Use bindings when KMS should decide decrypt/encrypt permission. If your app owns all authorization and calls KMS locally without `session:`, those calls are trusted/admin calls and bypass principal authorization.
## Model
A binding has:
- `principal_id`: principal receiving permission;
- `permission`: operation, e.g. `kek.decrypt`;
- `scope`: resource selector, e.g. `kek.alias:tenant/acme/docs`.
An operation is authorized when an authenticated session principal has a binding whose permission and scope match the requested operation.
Authorization is not key possession. [Session-protected aliases](use_cases/factor_bound_aliases.md) additionally require a recipient envelope that the session's private key can decrypt. RMK-only aliases use bindings alone.
## Important rule
Local calls without `session:` are trusted/admin execution for RMK-only aliases. They cannot decrypt session-protected aliases. Example for an RMK-only alias:
```elixir
KMS.decrypt_alias(ciphertext, "tenant/acme/docs", aad: aad)
```
Pass `session:` when KMS should enforce authorization:
```elixir
KMS.decrypt_alias(ciphertext, "tenant/acme/docs",
session: session.secret,
aad: aad
)
```
Remote HTTP API token authentication is separate. The API token authorizes backend access to the HTTP API; KMS sessions authorize principal-scoped KMS operations.
## Permissions
Exact permissions:
| Resource | Actions |
|---|---|
| `kek` | `create`, `encrypt`, `decrypt`, `rewrap`, `manage_tags` |
| `principal` | `create`, `read`, `update`, `delete` |
| `principal_factor` | `create`, `read`, `update`, `delete` |
| `session` | `create`, `read`, `delete` |
| `binding` | `create`, `read`, `update`, `delete` |
| `bindings` | `create`, `read`, `update`, `delete` |
| `fallback_public_key` | `create`, `read`, `rotate`, `delete` |
| `kek_fallback` | `create`, `read`, `unwrap` |
| `principal_fallback` | `create`, `read`, `unwrap` |
Wildcards:
```text
kek.*
principal.*
*.*
```
`kek.create` is unscoped because the KEK does not exist yet.
## Scopes
Supported scopes:
```text
*
kek.alias:<alias>
kek.tag:<tag>
principal.ref:<kind>/<ref>
principal.tag:<tag>
principal.self
principal_factor.id:<id>
principal_factor.principal_id:<principal_id>
principal_factor.principal:<kind>/<ref>
principal_factor.self
session.id:<id>
session.principal_id:<principal_id>
session.pending_factor_id:<factor_id>
session.principal:<kind>/<ref>
session.self
binding.id:<id>
binding.principal_id:<principal_id>
binding.permission:<permission>
binding.scope:<scope>
bindings.scope:<scope>
fallback_public_key.id:<id>
fallback_public_key.name:<name>
kek_fallback.id:<id>
kek_fallback.kek_id:<kek_id>
kek_fallback.public_key_id:<public_key_id>
principal_fallback.id:<id>
principal_fallback.principal_id:<principal_id>
principal_fallback.public_key_id:<public_key_id>
```
Use narrow scopes in production. Prefer alias scopes for KEK operations:
```text
kek.alias:tenant/acme/docs
```
## Grant encrypt/decrypt on one alias
```elixir
{:ok, principal} =
KMS.create_principal(%{
kind: "users",
ref: "alice",
username: "alice@example.com",
password: password
})
for permission <- ["kek.encrypt", "kek.decrypt"] do
{:ok, _binding} =
KMS.create_binding(principal.id, %{
permission: permission,
scope: "kek.alias:tenant/acme/docs"
})
end
```
Use session:
```elixir
{:ok, session} = KMS.create_session("users", "alice", %{password: password})
{:ok, encrypted} =
KMS.encrypt_alias("secret", "tenant/acme/docs",
session: session.secret,
aad: "document:123/body"
)
```
## Self scopes
Self scopes let a principal operate on its own resource. Example use cases:
- read/update own principal record;
- read/delete own sessions;
- advance own pending factor session.
Use self scopes when possible instead of broad principal/session scopes.
## Tag scopes
KEKs and principals can have tags. Tag scopes are useful when one binding should cover a group:
```elixir
{:ok, _binding} =
KMS.create_binding(principal.id, %{
permission: "kek.decrypt",
scope: "kek.tag:tenant:acme"
})
```
Tags are authorization grouping metadata. They are not AAD and not encryption context.
## Least privilege
Recommended:
- grant `kek.decrypt` only where needed;
- separate `kek.encrypt` and `kek.decrypt` for write-only producers;
- avoid `*.*` except break-glass/admin principals;
- keep binding management restricted;
- use MFA factors for high-value principals;
- rotate/delete sessions after use.
## Debugging authorization
If an operation returns `{:error, :not_allowed}`:
1. Confirm call passed `session:`.
2. Confirm session is authenticated and not expired.
3. Confirm principal id matches binding principal.
4. Confirm permission matches operation.
5. Confirm scope matches alias/tag/resource.
6. Confirm AAD is correct separately; AAD mismatch is a crypto failure, not authorization.
See [Multi-user encryption](use_cases/multi_user_encryption.md).