Object storage

S3 access policies: Designing least-privilege access

Broad keys accumulate faster than anyone reviews them. How to scope by prefix, action and condition without breaking apps.

7 min read
Narrow gated channels directing streams of glowing data blocks in a violet-lit data center

Most on-premises S3 deployments start with one access key per application and a policy granting full access to a bucket, because that is what gets the first workload running. The key then outlives the project that created it, is copied into a second application, and ends up in a configuration file nobody owns. Years later there are a handful of keys that can do everything, and no record of which are still used.

The obvious response is to write tighter policies, which is not sufficient on its own. A tighter policy applied to a key whose actual usage is unknown breaks something, usually at an inconvenient hour, and the fastest remedy is to widen it again. The organization then learns that tightening access causes outages, which is the wrong lesson to teach.

Least privilege in object storage is an evidence problem before it is a policy problem. The policy language is not hard. Knowing which actions an application actually issues, against which keys, under which conditions, is the work, and it determines whether a restriction can be applied safely.

How over-broad keys accumulate

The first cause is convenience under time pressure. An application fails with an access denied response, the error does not say which action was refused in a form the developer can act on quickly, and a wildcard resolves it immediately. Nothing forces a return visit. Reading S3 error codes carefully is what makes a narrow fix possible, and it is slower.

The second is key sharing. A key issued to a backup product ends up in a reporting script because the credential was already available and a new one involved a ticket. The policy now has to satisfy both consumers, so it grows to the union of their needs and never shrinks. Attribution is lost at the same moment, since the audit log shows one identity doing two unrelated jobs.

The third is the absence of a lifecycle for credentials. Applications are decommissioned, teams change, and keys remain valid because nothing expires them. A key with no owner is not merely unused. It is a credential nobody will notice being used.

What a scoped policy actually names

An S3 policy statement names a principal, a set of actions, a set of resources and optionally conditions. Scoping usefully means tightening all four rather than one. A statement allowing s3:GetObject on a whole bucket is narrower in action than full access and no narrower in resource, which matters when the bucket holds several tenants' data under different prefixes.

Resource scoping uses ARNs with prefixes, and one detail catches people. The object ARN and the bucket ARN are different resources. Listing is a bucket-level action, so s3:ListBucket must name the bucket itself, and restricting which prefixes an identity can list requires the s3:prefix condition key rather than a path in the resource. An application that reads one prefix but enumerates the whole bucket has been given a map of everything it cannot read.

Conditions are where policies stop being generic. Restricting by source network keeps a stolen key from being useful outside the segment it was issued for, and requiring TLS on every request is a one-line condition that closes a class of exposure. Conditions on object tags let one policy cover data whose sensitivity is expressed as metadata rather than as position in the key namespace.

Why deletion belongs to a different identity

Read, write and delete are granted together because they arrive together in the managed policies people copy. Their consequences differ entirely. A compromised read key discloses data. A write key can corrupt or encrypt it. A delete key removes it, and without versioning, removes it permanently.

Separating them is mostly a matter of insisting on it. An ingest identity needs PutObject and rarely needs DeleteObject, since applications that overwrite objects do so with a PUT. A restore identity needs GetObject alone. Cleanup belongs in a lifecycle rule, which is configuration under change control, rather than in an application credential with delete rights and a bug in its retention logic.

Object Lock adds one action worth treating as separate from all others. Deleting a version under governance-mode retention requires the s3:BypassGovernanceRetention permission, and any identity holding it can remove data the retention policy was meant to preserve. That belongs to a named administrative identity, not to an application. The same reasoning drives backup deletion protection, where immutability is worth only as much as the list of who can override it.

IdentityActions it genuinely needsOver-grant to remove first
Backup software writing to an immutable targetPutObject, PutObjectRetention, GetObject, ListBucket on its own prefixDeleteObject and any bypass permission on retained versions
Restore or recovery toolingGetObject and ListBucket, scoped to the prefixes it restores fromWrite actions inherited from a shared backup credential
Analytics or reporting consumerGetObject on a defined prefix, with s3:prefix limiting what it can listBucket-wide ListBucket that exposes keys outside its scope
Application performing its own cleanupPutObject, GetObjectDeleteObject, replaced by a lifecycle rule under change control

Reviewing access without breaking applications

The safe sequence is observe, model, then restrict. Audit records give the actual action set per identity over a period long enough to include monthly and quarterly work, which means a full quarter rather than a week. An identity logging only GetObject and ListBucket for a quarter is a candidate for a read-only policy, with evidence behind it.

The period has to cover rare paths deliberately. Restore operations, year-end jobs and disaster recovery tests use permissions invisible in a normal month. Restricting a policy on thirty days of evidence removes exactly the permissions that matter during an incident, and the failure then surfaces at the worst moment.

Where the logs are ambiguous, a deny statement is the reversible experiment. An explicit deny for one action on one identity, with monitoring in place, tests the hypothesis without rewriting the allow set, and removing it restores the previous behavior at once. Separating identities by team or project up front avoids most of this work, which is the argument in separating teams and projects.

Where Scality RING fits

RING implements a multi-tenant model with accounts, users, buckets and IAM-style policies, so the separation described above can be expressed at two levels. Tenant boundaries isolate accounts from each other, including quotas, while policies scope what an identity inside an account can do. Putting unrelated workloads in separate accounts rather than separate prefixes of one bucket removes a class of policy mistake, since cross-account access must be granted explicitly.

RING supports S3 Object Lock and versioning, which makes the deletion path above enforceable rather than advisory. Retention applies at the object version level and bypass is a distinct grant, so immutability and delete authority can sit in different hands.

Because RING runs on the operator's own hardware in the operator's own facility, the identity system, the audit records and the review process stay inside the environment, and the set of people with operational access is determined locally. That is the same control question examined in who can change the system, applied to storage access rather than to the platform itself.

What to review and when

Keep a register of every access key with four fields: the application it belongs to, a named owner, the actions it is permitted, and the date its usage was last reviewed against logs. A key that cannot be attributed to an application and an owner should be disabled rather than investigated indefinitely, since disabling it produces an answer within a day.

Review quarterly, and review the identities holding delete or bypass permissions every time, whatever else is on the list. That set is short enough to enumerate, and the consequence of one being wrong differs in kind from an over-broad read grant.

Rotate keys on a schedule the applications are known to survive, and prove it by rotating one in a controlled window rather than assuming. Record which applications required a restart, since that list is also the list of systems that will fail when a key has to be revoked during an incident.

See Scality in action

Exabyte-scale object storage for AI data and cyber resilience. Talk to our team about what it can do for yours.

Request a demo