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 authz-bindings.md
Raw

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).