8 min read

S3 Access Points: a cleaner boundary for multi-tenant storage

A shared S3 bucket does not have to mean shared access. Use one Access Point per tenant to keep permissions small, changes isolated, and data boundaries clear.

Multi-tenant SaaS securityAWSS3SecurityMulti-tenancyArchitecture

A shared S3 bucket is cheap to operate and easy to provision. It is also easy to get wrong.

Consider a SaaS application that stores every organization’s files in one bucket. Each organization has its own prefix, but a flaw in the access rules could let one tenant read another tenant’s objects. Prefixes organize data. They do not create a security boundary by themselves.

A team could keep adding tenant statements to the bucket policy. That fixes the immediate problem, until the policy becomes the system’s next scaling limit. An S3 bucket policy is capped at 20 KB, and every tenant-specific statement makes one shared document harder to review and riskier to change. AWS’s January 2025 multi-tenant S3 guidance describes this exact pressure: shared buckets can serve thousands of tenants, while large, shared policies become difficult to manage.

S3 Access Points offer a better shape for the problem. They keep the bucket shared, but put a small, explicit access boundary in front of each tenant’s prefix.

The real problem is authorization, not folder structure

A key such as tenants/acme/invoices/2026-09.pdf looks isolated. S3 does not treat that prefix as a tenant boundary. It is only part of an object key.

Your IAM identity policy, bucket policy, access-point policy, and explicit denies decide whether a request succeeds. If a role can call s3:GetObject against the bucket broadly enough, knowing another tenant’s object key may be all it takes to cross the boundary.

Why a single bucket policy starts to hurt

A per-tenant bucket-policy statement repeats the same structure: a principal, a short list of actions, and one tenant prefix. It works for a small, static set of tenants. At scale, it creates three problems.

  1. The policy has a hard 20 KB ceiling. You can compact JSON, but that only delays the limit.
  2. Every tenant change touches a shared document. A mistake in one edit can affect every organization.
  3. Reviews become harder. You have to reason about the whole bucket to understand one tenant’s access.

IAM policy variables can reduce repetition when an identity maps neatly to a prefix. They are useful, but they do not cover every role, service integration, or tenant-specific exception. They also do not make the operational boundary clearer.

Separate storage from the access boundary

A bucket can remain the durable storage layer. An Access Point becomes the named route that a tenant, service, or team uses to reach a constrained part of it.

AWS calls Access Points “network endpoints attached to buckets.” Each one has its own resource policy and network controls. Requests made through that Access Point must satisfy its policy as well as the rest of the applicable S3 and IAM policy evaluation. See the AWS documentation on Access Point policies for the full evaluation model.

Give each tenant its own Access Point

The pattern is simple:

shared-uploads-bucket
├── tenants/acme/*       ← acme-access-point
├── tenants/brighton/*   ← brighton-access-point
└── tenants/cascade/*    ← cascade-access-point

Each Access Point is attached to the same bucket. Its policy grants a specific role only the actions and object prefix that tenant needs. The application uses the Access Point alias or ARN, not the bucket name, for tenant storage requests.

A narrow policy is easier to trust

Here is the core of an Access Point policy for one tenant. The values are illustrative, not a drop-in production policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AcmeCanManageItsObjects",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/acme-storage-role"
      },
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:us-east-1:123456789012:accesspoint/acme-access/object/tenants/acme/*"
    }
  ]
}

That statement says exactly what you want it to say: the Acme role can manage objects under tenants/acme/ through acme-access. It cannot use that policy to reach tenants/brighton/.

If the application needs to list objects, add s3:ListBucket deliberately and restrict it with an s3:prefix condition. Do not add broad list access because it is convenient. Object names often reveal more about another tenant than you expect.

Keep the bucket policy as a guardrail

The Access Point policy is not a bypass around bucket security. It works with identity permissions and the underlying bucket policy.

A practical design delegates data access to Access Points in the bucket policy, then keeps tenant-specific permissions in the individual Access Point policies. This stops the bucket policy from becoming a tenant directory. It also lets you change Acme’s policy without editing Brighton’s.

Check for broad IAM grants during the migration. A role with permissive direct access to the bucket can still defeat the boundary you meant the Access Point to enforce. In other words, Access Points make least privilege easier to manage. They do not make an overly broad role safe.

What this changes as you grow

AWS’s 2025 design guidance says Access Points work well for static configurations in the tens of thousands of tenants. The point is not that every application needs that scale today. The point is that onboarding tenant 501 should look like onboarding tenant 5: create a prefix, create an Access Point, attach a narrow policy, and test it.

Safer changes

A tenant access change affects one Access Point policy rather than the shared bucket policy. That contains the blast radius and makes pull requests smaller. The same discipline behind deploying to AWS from GitHub Actions with OIDC applies here: make the identity, resource, and permission explicit.

Clearer audit trails

Give Access Points predictable names, such as tenant-{id}-access. Then include the tenant ID, access-point ARN, caller role, object key, and outcome in your application logs. Enable CloudTrail data events for the bucket and alert on direct bucket access that should have gone through an Access Point.

That level of visibility supports the operational work behind an uptime SLA. You cannot investigate a cross-tenant access event quickly if the logs do not tell you which tenant path and identity were involved.

A migration plan that does not break everyone

You do not need to replace every call to S3 at once. Move one tenant or one workload at a time.

1. Map current access before changing it

Inventory every S3 caller: application roles, Lambda functions, support tools, batch jobs, and third-party integrations. For each caller, record the actions it needs and the tenant prefixes it should reach. This is where hidden broad permissions surface.

2. Add one Access Point and prove the deny path

Create an Access Point for a non-production tenant with the narrowest useful policy. Update that tenant’s requests to use its alias or ARN. Then run two tests with the tenant role:

  1. Read and write an object in that tenant’s prefix. Both should succeed.
  2. Read an object in a different tenant’s prefix. It must fail with AccessDenied.

The second test matters more. A successful happy path does not prove isolation.

3. Remove direct bucket access, then repeat

After a tenant is working through its Access Point, remove the role’s broad bucket permissions. Roll out the same pattern through infrastructure as code so every new tenant receives the same controls. This pairs well with hosting a Next.js site on S3 and CloudFront: provision the resource and its security boundary together.

Where Access Points are not the whole answer

Access Points fit stable mappings such as one tenant role to one tenant prefix. They are not an application-level authorization engine.

Use an application authorization check too

Your API should still verify that the authenticated user belongs to the organization named in the request. Never let a client choose an arbitrary tenant ID and translate it into an Access Point ARN. Derive the tenant context from the authenticated session on the server.

Consider S3 Access Grants for dynamic end-user access

If users belong to several organizations, frequently change groups, or need short-lived data access based on an external identity provider, pre-created tenant policies may become awkward. AWS positions S3 Access Grants for that more dynamic, identity-aware case.

For a stable tenant-to-prefix mapping, Access Points are the simpler fit.

The takeaway

A shared S3 bucket can remain a sound design. The dangerous part is treating prefixes as isolation.

Give each tenant a dedicated Access Point, scope it to that tenant’s prefix, route application requests through it, and remove broad direct bucket permissions. You get a clear control plane for every organization without turning one bucket policy into a fragile list of every tenant you have ever onboarded.

For a full tenant architecture with edge routing and API isolation, see the multi-tenant SaaS case study.

Frequently asked questions

Do S3 Access Points create a new bucket for every tenant?

No. An Access Point attaches to an existing bucket. You can keep one shared bucket and create a distinct Access Point for each tenant or workload.

Do Access Points prevent all cross-tenant access by themselves?

No. The request must still be evaluated against IAM, the Access Point policy, the bucket policy, and any explicit denies. Remove or constrain direct bucket permissions, or they can undermine the boundary.

Are S3 Access Points a replacement for bucket policies?

No. Use the bucket policy as a shared guardrail and use Access Point policies for tenant-specific access. Both are part of the authorization decision.

How large can an S3 bucket policy be?

An S3 bucket policy has a 20 KB size limit. This is why one statement per tenant becomes a poor long-term control plane. AWS documents the limit in its S3 bucket-policy guidance.

Can I use an Access Point with an existing application?

Yes. Move incrementally. Create an Access Point, update one tenant’s S3 client to use its alias or ARN, prove both allowed and denied behavior, then remove that tenant’s direct bucket access.

When should I use S3 Access Grants instead?

Use S3 Access Grants when access depends on dynamic end-user identity or group membership, especially when you need short-lived, scoped credentials. For stable tenant-to-prefix mappings, Access Points are usually simpler.

Sources were paraphrased for compliance with licensing restrictions. The quoted AWS phrase is reproduced in abbreviated form.

Comments

    Leave a comment