Authorization and bindings

Copy Markdown View Source

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

KMS.decrypt_alias(ciphertext, "tenant/acme/docs", aad: aad)

Pass session: when KMS should enforce authorization:

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:

ResourceActions
kekcreate, encrypt, decrypt, rewrap, manage_tags
principalcreate, read, update, delete
principal_factorcreate, read, update, delete
sessioncreate, read, delete
bindingcreate, read, update, delete
bindingscreate, read, update, delete
fallback_public_keycreate, read, rotate, delete
kek_fallbackcreate, read, unwrap
principal_fallbackcreate, read, unwrap

Wildcards:

kek.*
principal.*
*.*

kek.create is unscoped because the KEK does not exist yet.

Scopes

Supported scopes:

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

kek.alias:tenant/acme/docs

Grant encrypt/decrypt on one alias

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

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

{: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.