Skip to main content
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.
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.

Volumes, snapshots and policies

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.

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

Anonymous callers

The bucket policy alone decides. No policy means no access.
2

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.
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.
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.
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:
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:
A service that reads one prefix and writes nothing:
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.
See writing policies for the document format and every condition operator.

What a denial looks like

A failed check answers 403:
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.
Requests on the S3 protocol endpoint answer in S3’s own error format rather than this one — see the S3 error codes. The decision is the same; only the envelope differs.