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

# Storage permissions

> Every IAM action across volumes, snapshots, buckets and objects — plus the bucket policy layer that sits beside IAM on the object side.

Storage is two services under one name, and they are guarded differently.
Volumes and snapshots are gated by IAM alone. Buckets and objects are gated by
IAM **and** by the bucket's own policy, and either can decide a request.

<Info>
  The API reference shows the action on each endpoint's own page. Both come
  from the same place: the authorization call in the service, read at build
  time.
</Info>

## Volumes, snapshots and policies

| Action                         | Call                                       |
| ------------------------------ | ------------------------------------------ |
| `storage:ListVolumes`          | `GET /v1/volumes`                          |
| `storage:CreateVolume`         | `POST /v1/volumes`                         |
| `storage:GetVolume`            | `GET /v1/volumes/{volume_id}`              |
| `storage:UpdateVolume`         | `PATCH /v1/volumes/{volume_id}`            |
| `storage:DeleteVolume`         | `DELETE /v1/volumes/{volume_id}`           |
| `storage:ExtendVolume`         | `POST /v1/volumes/{volume_id}/extend`      |
| `storage:ListVolumeTypes`      | `GET /v1/volume-types`                     |
| `storage:ListSnapshots`        | `GET /v1/snapshots`                        |
| `storage:CreateSnapshot`       | `POST /v1/snapshots`                       |
| `storage:GetSnapshot`          | `GET /v1/snapshots/{snapshot_id}`          |
| `storage:UpdateSnapshot`       | `PATCH /v1/snapshots/{snapshot_id}`        |
| `storage:DeleteSnapshot`       | `DELETE /v1/snapshots/{snapshot_id}`       |
| `storage:ListSnapshotPolicies` | `GET /v1/snapshot-policies`                |
| `storage:CreateSnapshotPolicy` | `POST /v1/snapshot-policies`               |
| `storage:GetSnapshotPolicy`    | `GET /v1/snapshot-policies/{policy_id}`    |
| `storage:UpdateSnapshotPolicy` | `PATCH /v1/snapshot-policies/{policy_id}`  |
| `storage:DeleteSnapshotPolicy` | `DELETE /v1/snapshot-policies/{policy_id}` |

These follow their endpoint names, and the resource is the CRN of the volume,
snapshot or policy being acted on.

## Buckets and objects

The object side takes its action names from S3, so several of them do **not**
match the endpoint they guard. These are the ones worth reading before writing
a policy.

| Action                                     | Call                                                    |
| ------------------------------------------ | ------------------------------------------------------- |
| `storage:ListAllMyBuckets`                 | `GET /v1/buckets`                                       |
| `storage:CreateBucket`                     | `POST /v1/buckets`                                      |
| `storage:HeadBucket`                       | `HEAD /v1/buckets/{bucket}`                             |
| `storage:DeleteBucket`                     | `DELETE /v1/buckets/{bucket}`                           |
| `storage:RestoreBucket`                    | `POST /v1/buckets/{bucket}/restore`                     |
| `storage:PutBucketDeletionProtection`      | `PUT /v1/buckets/{bucket}/deletion-protection`          |
| `storage:ListBucket`                       | `GET /v1/buckets/{bucket}/objects`                      |
| `storage:ListBucket`                       | `GET /v1/buckets/{bucket}/object-versions`              |
| `storage:ListBucket`                       | `GET /v1/buckets/{bucket}/multipart-uploads`            |
| `storage:GetObject`                        | `GET /v1/buckets/{bucket}/objects/{key}`                |
| `storage:HeadObject`                       | `HEAD /v1/buckets/{bucket}/objects/{key}`               |
| `storage:PutObject`                        | `PUT /v1/buckets/{bucket}/objects/{key}`                |
| `storage:DeleteObject`                     | `DELETE /v1/buckets/{bucket}/objects/{key}`             |
| `storage:PutObject`                        | every multipart upload call                             |
| `storage:GetBucketPolicy`                  | `GET /v1/buckets/{bucket}/policy`                       |
| `storage:PutBucketPolicy`                  | `PUT /v1/buckets/{bucket}/policy`                       |
| `storage:DeleteBucketPolicy`               | `DELETE /v1/buckets/{bucket}/policy`                    |
| `storage:GetBucketCORS`                    | `GET /v1/buckets/{bucket}/cors`                         |
| `storage:PutBucketCORS`                    | `PUT` **and** `DELETE /v1/buckets/{bucket}/cors`        |
| `storage:GetBucketVersioning`              | `GET /v1/buckets/{bucket}/versioning`                   |
| `storage:PutBucketVersioning`              | `PUT /v1/buckets/{bucket}/versioning`                   |
| `storage:GetLifecycleConfiguration`        | `GET /v1/buckets/{bucket}/lifecycle`                    |
| `storage:PutLifecycleConfiguration`        | `PUT` **and** `DELETE /v1/buckets/{bucket}/lifecycle`   |
| `storage:GetEncryptionConfiguration`       | `GET /v1/buckets/{bucket}/encryption`                   |
| `storage:PutEncryptionConfiguration`       | `PUT` **and** `DELETE /v1/buckets/{bucket}/encryption`  |
| `storage:GetBucketTagging`                 | `GET /v1/buckets/{bucket}/tagging`                      |
| `storage:PutBucketTagging`                 | `PUT` **and** `DELETE /v1/buckets/{bucket}/tagging`     |
| `storage:GetBucketObjectLockConfiguration` | `GET /v1/buckets/{bucket}/object-lock`                  |
| `storage:PutBucketObjectLockConfiguration` | `PUT` **and** `DELETE /v1/buckets/{bucket}/object-lock` |
| `storage:GetBucketLocation`                | S3 protocol only                                        |
| `storage:BypassGovernanceRetention`        | overriding a `governance` retention                     |

### Four things the names do not tell you

**Listing objects needs `storage:ListBucket`, not a `ListObjects` action.** The
same action covers listing object versions and listing multipart uploads. This
is S3's naming: the action is named for the thing you are listing *inside*, not
for what comes back.

**Listing your buckets is `storage:ListAllMyBuckets`.** It is checked against
the account, so it cannot be scoped to particular buckets — granting it lets a
caller see that every bucket exists, and nothing more.

**Removing a bucket sub-resource takes the `Put` action, not a delete action.**
`DELETE /v1/buckets/{bucket}/cors` requires `storage:PutBucketCORS`; the same
holds for lifecycle, encryption, tagging and object-lock. Clearing a
configuration is writing it to empty.

**`DELETE` on an object key has two answers.** Deleting the object needs
`storage:DeleteObject`, but `DELETE /v1/buckets/{bucket}/objects/{key}?tagging`
— which removes the object's tags and leaves the object — needs
`storage:PutObject`. Removing tags is a mutation of the object, not a deletion
of it. A credential granted `storage:DeleteObject` alone will find the tagging
form refused.

## Two layers on the object side

Every object request is decided by IAM **and** by the bucket policy, in this
order:

<Steps>
  <Step title="Anonymous callers">
    The bucket policy alone decides. No policy means no access.
  </Step>

  <Step title="Authenticated callers">
    An explicit `deny` in **either** layer refuses the request outright. Failing
    that, an explicit `allow` from **either** layer is enough to proceed. With
    neither, the request is refused — an implicit deny.
  </Step>
</Steps>

The bucket policy is the layer that reaches principals outside your account,
including anonymous ones. IAM cannot grant to somebody else's principal; the
bucket policy can.

<Warning>
  **A bucket policy allowing anonymous `storage:GetObject` publishes those
  objects to the internet.** There is no second switch behind it — the policy is
  the switch.
</Warning>

Both layers stop applying the moment the bucket owner's organization is
suspended, cross-account grantees and anonymous readers included.

## Resources

Storage CRNs are name-based and carry the region and account:

```
crn:storage:sa-saopaulo-1:my-account:volume/app-data-01
crn:storage:sa-saopaulo-1:my-account:snapshot/pre-upgrade
crn:storage:sa-saopaulo-1:my-account:snapshot-policy/nightly
crn:storage:sa-saopaulo-1:my-account:bucket/reports
crn:storage:sa-saopaulo-1:my-account:bucket/reports/2026/q1.csv
```

An object's CRN is the bucket's with the key appended, so a policy can scope to
a prefix — `bucket/reports/2026/*` — the same way it scopes to a naming
convention on volumes: `crn:storage:*:my-account:volume/prod-*`.

## Conditions

Volumes, snapshots and policies take a `tags` map, and both tag context keys
apply:

* `basalt:RequestTag/<key>` — what a caller may label a resource **as**, which
  is what makes a create authorizable before the resource exists.
* `basalt:ResourceTag/<key>` — which existing resources an action may touch.

Objects carry their own, named for S3 rather than for us:

* `s3:RequestObjectTag/<key>` — what a write is trying to set.
* `s3:ExistingObjectTag/<key>` — what is already on the object.
* `basalt:TagKeys` — the set of tag keys a request carries.

Production volumes only, fenced on the tag rather than the name:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "ProductionVolumesOnly",
      "effect": "allow",
      "actions": ["storage:CreateVolume", "storage:ExtendVolume"],
      "resources": ["crn:storage:*:my-account:volume/*"],
      "conditions": [
        {
          "operator": "equals",
          "key": "basalt:RequestTag/env",
          "values": ["production"]
        }
      ]
    }
  ]
}
```

A service that reads one prefix and writes nothing:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "ReportsReadOnly",
      "effect": "allow",
      "actions": ["storage:ListBucket", "storage:GetObject"],
      "resources": [
        "crn:storage:sa-saopaulo-1:my-account:bucket/reports",
        "crn:storage:sa-saopaulo-1:my-account:bucket/reports/2026/*"
      ]
    }
  ]
}
```

<Warning>
  **Both resource lines are needed.** `bucket/reports` does not cover the
  objects inside it — an object's CRN is a longer path, and a wildcard match
  stops at the resource it names. `storage:ListBucket` is checked against the
  bucket, `storage:GetObject` against the object.
</Warning>

See [writing policies](/iam/policies) for the 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. Look up the
call you made in the tables above, and the action it needs is the one to add.

<Note>
  Requests on the S3 protocol endpoint answer in S3's own error format rather
  than this one — see [the S3 error codes](/storage/troubleshooting). The
  decision is the same; only the envelope differs.
</Note>
