> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# KMS permissions

> Every IAM action the KMS service checks, which call needs it, and how to grant sealing without granting opening.

Every KMS endpoint checks one IAM action before it does anything. This is the
whole list — there are no others, and no endpoint skips the check.

<Info>
  The API reference shows the action on each endpoint's own page, so you do not
  have to come back here to look one up. Both come from the same place: the
  authorization call in the service, read at build time.
</Info>

## The actions

Every operation on a specific key is authorized against **that key's CRN**,
with the key's tags available as condition context. Only the two
collection-level operations are not.

| Action                    | Call                                       | Scope      |
| ------------------------- | ------------------------------------------ | ---------- |
| `kms:ListKeys`            | `GET /v1/keys`                             | collection |
| `kms:CreateKey`           | `POST /v1/keys`                            | collection |
| `kms:GetKey`              | `GET /v1/keys/{key_id}`                    | key CRN    |
| `kms:UpdateKey`           | `PATCH /v1/keys/{key_id}`                  | key CRN    |
| `kms:EnableKey`           | `POST /v1/keys/{key_id}/enable`            | key CRN    |
| `kms:DisableKey`          | `POST /v1/keys/{key_id}/disable`           | key CRN    |
| `kms:ScheduleKeyDeletion` | `POST /v1/keys/{key_id}/schedule-deletion` | key CRN    |
| `kms:CancelKeyDeletion`   | `POST /v1/keys/{key_id}/cancel-deletion`   | key CRN    |
| `kms:Encrypt`             | `POST /v1/keys/{key_id}/encrypt`           | key CRN    |
| `kms:Decrypt`             | `POST /v1/keys/{key_id}/decrypt`           | key CRN    |
| `kms:GenerateDataKey`     | `POST /v1/keys/{key_id}/generate-data-key` | key CRN    |
| `kms:Sign`                | `POST /v1/keys/{key_id}/sign`              | key CRN    |
| `kms:Verify`              | `POST /v1/keys/{key_id}/verify`            | key CRN    |

### Sealing and opening are different grants

`kms:Decrypt` and `kms:GenerateDataKey` are separate actions from `kms:GetKey`
precisely so the operations that return usable key material can be granted
narrowly. A component that only needs to *seal* data should hold `kms:Encrypt`
and `kms:GenerateDataKey` and nothing else — it can then write, and never read
back.

The same split appears on [certificates](/certificates/permissions), where
`certificate:GetCertificateMaterial` is separate from reading a certificate.

<Note>
  `kms:GenerateDataKey` returns the new data key **in plaintext as well as
  wrapped**, because the caller has to use it before throwing it away. Treat it
  as equivalent to `kms:Decrypt` in blast radius when you decide who holds it,
  even though the two guard different directions.
</Note>

### Listing cannot be narrowed

`kms:ListKeys` is authorized against the collection, not against individual
keys, so restricting it by CRN or tag has no effect. Scope the operations that
*use* a key; listing tells a caller that a key exists and nothing more.

## Resources

KMS actions are checked against one resource shape:

```
crn:kms:<region>:<account>:key/<name>
```

The region slot is **populated**, unlike DNS or certificates. A key exists in
one region and can only be used from there, so a policy written for one region
does not reach another region's keys even when the names match.

## Conditions

Create carries the tags from the request; every key-scoped action carries the
tags already on the key. So a policy can fence both what a caller may label a
key **as**, and which existing keys it may touch:

* `basalt:RequestTag/<key>` — at `kms:CreateKey`.
* `basalt:ResourceTag/<key>` — on every action scoped to a key CRN.

## Writing a policy

A write path that can seal but never open:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "IngestSealsOnly",
      "effect": "allow",
      "actions": ["kms:GenerateDataKey", "kms:Encrypt"],
      "resources": ["crn:kms:sa-saopaulo-1:my-account:key/prod-*"]
    }
  ]
}
```

Fencing a fleet by label rather than by name:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "StagingKeysOnly",
      "effect": "allow",
      "actions": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
      "resources": ["*"],
      "conditions": [
        {
          "operator": "equals",
          "key": "basalt:ResourceTag/env",
          "values": ["staging"]
        }
      ]
    }
  ]
}
```

<Warning>
  **`kms:*` includes decryption and deletion.** A wildcard grants
  `kms:Decrypt`, `kms:GenerateDataKey` and `kms:ScheduleKeyDeletion` along with
  everything else. List the actions when the credential belongs to a workload;
  a service that seals data has no reason to be able to schedule the key's
  destruction.
</Warning>

See [writing policies](/iam/policies) for the full document format and every
condition operator.

## What a denial looks like

A failed check answers `403`:

```json theme={null}
{
  "error": {
    "code": "ACCESS_DENIED",
    "message": "You don't have permission to perform this action",
    "request_id": "..."
  }
}
```

**It does not tell you which action was missing**, deliberately — the message
is the same for every denial, so it cannot be used to map out what a credential
can and cannot reach. Look up the call you made in the table above, and the
action it needs is the one to add.

<Note>
  A `404` is not a disguised `403`. Ownership is resolved before authorization:
  a key belonging to another account answers `KMS_KEY_NOT_FOUND` because it is
  not yours to see, and one you own but lack the action for answers `403`. If
  you are getting `404` on a key you believe you own, check the region and the
  account the credential belongs to before checking the policy.
</Note>
