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

# Secrets permissions

> Every IAM action the secrets service checks, which call needs it, and how to grant reading a value separately from everything else.

Every secrets 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

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

| Action                   | Call                                   | Scope      |
| ------------------------ | -------------------------------------- | ---------- |
| `secrets:ListSecrets`    | `GET /v1/secrets`                      | collection |
| `secrets:CreateSecret`   | `POST /v1/secrets`                     | collection |
| `secrets:DescribeSecret` | `GET /v1/secrets/{secret_id}`          | secret CRN |
| `secrets:UpdateSecret`   | `PATCH /v1/secrets/{secret_id}`        | secret CRN |
| `secrets:DeleteSecret`   | `DELETE /v1/secrets/{secret_id}`       | secret CRN |
| `secrets:RestoreSecret`  | `POST /v1/secrets/{secret_id}/restore` | secret CRN |
| `secrets:GetSecretValue` | `GET /v1/secrets/{secret_id}/value`    | secret CRN |
| `secrets:PutSecretValue` | `PUT /v1/secrets/{secret_id}/value`    | secret CRN |
| `secrets:ListVersions`   | `GET /v1/secrets/{secret_id}/versions` | secret CRN |

### Describing a secret does not read it

`secrets:GetSecretValue` is a different action from `secrets:DescribeSecret`,
and the split runs all the way through the API: the describe response has no
value field at all, so there is no shape in which metadata and plaintext travel
together. Grant the read action on its own, to the few principals that need it,
on the few secrets they need.

The same split appears on [KMS](/kms/permissions) with `kms:Decrypt` and on
[certificates](/certificates/permissions) with
`certificate:GetCertificateMaterial`.

### Writing does not imply reading

`secrets:PutSecretValue` adds a version without returning anything of the old
one, so a rotation job can hold it alone. That is worth doing: a component that
only ever writes new material has no reason to be able to read what is already
there.

### Listing cannot be narrowed

`secrets:ListSecrets` is authorized against the collection rather than against
individual secrets, so restricting it by CRN or tag has no effect. Scope
`secrets:GetSecretValue` — that is the grant that matters.

## Resources

Secrets actions are checked against one resource shape:

```
crn:secrets:<region>:<account>:secret/<name>
```

The region slot is populated: a secret lives in one region and is read from
there.

A secret's name may contain `/`, and that is what makes the CRN worth scoping
by. Naming secrets `prod/payments/stripe-key` rather than
`prod-payments-stripe-key` lets one statement cover a whole service's secrets
and nothing else. Names match `^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,255}$`.

## Conditions

Create carries the tags from the request; every secret-scoped action carries
the tags already on the secret:

* `basalt:RequestTag/<key>` — what a caller may label a secret **as**, at
  create.
* `basalt:ResourceTag/<key>` — which existing secrets an action may touch.

## Writing a policy

One service reading exactly the secrets under its own path prefix:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "PaymentsReadsItsOwnSecrets",
      "effect": "allow",
      "actions": ["secrets:DescribeSecret", "secrets:GetSecretValue"],
      "resources": ["crn:secrets:sa-saopaulo-1:my-account:secret/prod/payments/*"]
    }
  ]
}
```

A rotation job that may write but never read:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "RotateOnly",
      "effect": "allow",
      "actions": ["secrets:PutSecretValue", "secrets:ListVersions"],
      "resources": ["crn:secrets:sa-saopaulo-1:my-account:secret/prod/*"]
    }
  ]
}
```

<Warning>
  **`secrets:*` includes reading every value.** A wildcard on the action grants
  `secrets:GetSecretValue` along with everything else, which is rarely what is
  meant by "let this team manage secrets". List the actions when the credential
  belongs to a person or a CI job.
</Warning>

See [writing policies](/iam/policies) for the document format, the tag
condition keys, and how a `deny` guardrail survives a broad allow.

## 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 secret belonging to another account answers `404` because it is not yours
  to see, and one you own but lack the action for answers `403`. A secret
  inside its [recovery window](/secrets/deletion) is a third case again — it
  answers `409 SECRET_DELETED`, which means the secret is there and the
  credential is fine.
</Note>
